Skip to content

Commit

Permalink
feat(simple): add no-default-bound feature
Browse files Browse the repository at this point in the history
For those who cannot easily implement `Default` (ex. a large struct),
a version of the simple dropper that relies on an inner `Option<T>`
can be more convenient to write.

This commit adds a feature flag `no-default-bound` that removes the
`Default` bound requirement.

Co-authored-by: Linken Quy Dinh <[email protected]>
  • Loading branch information
t3hmrman and beckend committed Jan 31, 2024
1 parent 98191c7 commit 8370b48
Show file tree
Hide file tree
Showing 5 changed files with 666 additions and 160 deletions.
2 changes: 2 additions & 0 deletions crates/async-dropper-simple/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ A simple struct-wrapper (i.e. AsyncDropper<T>) based implementation of AsyncDrop
default = []
tokio = ["dep:tokio", "dep:async-scoped", "async-scoped/use-tokio"]
async-std = ["dep:async-std", "dep:async-scoped", "async-scoped/use-async-std"]
no-default-bound = []

[dependencies]
async-scoped = { workspace = true, optional = true }
Expand All @@ -32,6 +33,7 @@ async-trait.workspace = true
rustc_version = "0.4.0"

[dev-dependencies]
async-std = { workspace = true, features = [ "attributes", "tokio1" ] }
tokio = { workspace = true, features = [
"time",
"macros",
Expand Down
8 changes: 8 additions & 0 deletions crates/async-dropper-simple/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,11 @@
- `async_dropper::derive` provides a trait called `AsyncDrop` and corresponding [derive macro][rust-derive-macro], which try to use `Default` and `PartialEq` to determine when to async drop.

The code in this crate powers `async_dropper::simple`. See the `async_dropper` crate for more details.

## Feature flags

| Flag | Description |
|--------------------|-------------------------------------------------------------------------------------|
| `tokio` | Use the [`tokio`][tokio] async runtime |
| `async-std` | use the [`async-std`][async-std] async runtime |
| `no-default-bound` | Avoid requiring the `Default` bound by wrapping the interior data in an `Option<T>` |
308 changes: 308 additions & 0 deletions crates/async-dropper-simple/src/default.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,308 @@
//! Implementation of simple AsyncDropper that does not require `Default`
//!
//! This implementation might be preferrable for people who cannot reasonably
//! implement `Default` for their struct, but have easily accessible `Eq`,`PartialEq`,
//! `Hash`, and/or `Clone` instances.
#![cfg(not(feature = "no-default-bound"))]

use std::time::Duration;

use crate::AsyncDrop;

/// Wrapper struct that enables `async_drop()` behavior.
///
/// This version requires a `Default` implementation.
#[derive(Default)]
#[allow(dead_code)]
pub struct AsyncDropper<T: AsyncDrop + Default + Send + 'static> {
dropped: bool,
timeout: Option<Duration>,
inner: T,
}

impl<T: AsyncDrop + Default + Send + 'static> AsyncDropper<T> {
pub fn new(inner: T) -> Self {
Self {
dropped: false,
timeout: None,
inner,
}
}

pub fn with_timeout(timeout: Duration, inner: T) -> Self {
Self {
dropped: false,
timeout: Some(timeout),
inner,
}
}

/// Get a reference to the inner data
pub fn inner(&self) -> &T {
&self.inner
}

/// Get a mutable refrence to inner data
pub fn inner_mut(&mut self) -> &mut T {
&mut self.inner
}
}

impl<T> Deref for AsyncDropper<T>
where
T: AsyncDrop + Send + Default,
{
type Target = T;

fn deref(&self) -> &T {
self.inner()
}
}

impl<T> DerefMut for AsyncDropper<T>
where
T: AsyncDrop + Send + Default,
{
fn deref_mut(&mut self) -> &mut T {
self.inner_mut()
}
}

#[cfg(all(not(feature = "tokio"), not(feature = "async-std")))]
impl<T: AsyncDrop + Default + Send + 'static> Drop for AsyncDropper<T> {
fn drop(&mut self) {
compile_error!(
"either 'async-std' or 'tokio' features must be enabled for the async-dropper crate"
)
}
}

#[cfg(all(feature = "async-std", feature = "tokio"))]
impl<T: AsyncDrop + Default + Send + 'static> Drop for AsyncDropper<T> {
fn drop(&mut self) {
compile_error!(
"'async-std' and 'tokio' features cannot both be specified for the async-dropper crate"
)
}
}

#[cfg(all(feature = "tokio", not(feature = "async-std")))]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))]
impl<T: AsyncDrop + Default + Send + 'static> Drop for AsyncDropper<T> {
fn drop(&mut self) {
if !self.dropped {
use async_scoped::TokioScope;

// Set the original instance to be dropped
self.dropped = true;

// Save the timeout on the original instance
let timeout = self.timeout;

// Swap out the current instance with default
// (i.e. `this` is now original instance, and `self` is a default instance)
let mut this = std::mem::take(self);

// Set the default instance to note that it's dropped
self.dropped = true;

// Create task
match timeout {
// If a timeout was specified, use it when performing async_drop
Some(d) => {
TokioScope::scope_and_block(|s| {
s.spawn(tokio::time::timeout(d, async move {
this.inner.async_drop().await;
}))
});
}
// If no timeout was specified, perform async_drop() indefinitely
None => {
TokioScope::scope_and_block(|s| {
s.spawn(async move {
this.inner.async_drop().await;
})
});
}
}
}
}
}

#[cfg(all(feature = "async-std", not(feature = "tokio")))]
#[cfg_attr(docsrs, doc(cfg(feature = "async-std")))]
impl<T: AsyncDrop + Default + Send + 'static> Drop for AsyncDropper<T> {
fn drop(&mut self) {
if !self.dropped {
use async_scoped::AsyncStdScope;

// Set the original instance to be dropped
self.dropped = true;

// Save the timeout on the original instance
let timeout = self.timeout;

// Swap out the current instance with default
// (i.e. `this` is now original instance, and `self` is a default instance)
let mut this = std::mem::take(self);

// Set the default instance to note that it's dropped
self.dropped = true;

match timeout {
// If a timeout was specified, use it when performing async_drop
Some(d) => {
AsyncStdScope::scope_and_block(|s| {
s.spawn(async_std::future::timeout(d, async move {
this.inner.async_drop().await;
}))
});
}
// If no timeout was specified, perform async_drop() indefinitely
None => {
AsyncStdScope::scope_and_block(|s| {
s.spawn(async move {
this.inner.async_drop().await;
})
});
}
}
}
}
}

#[cfg(test)]
mod tests {
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
use std::time::Duration;

use async_trait::async_trait;

use crate::{AsyncDrop, AsyncDropper};

/// Testing struct
struct Test {
// This counter is used as an indicator of how far async_drop() gets
// - 0 means async_drop() never ran
// - 1 means async_drop() started but did not complete
// - 2 means async_drop() completed
counter: Arc<AtomicU32>,
}

impl Default for Test {
fn default() -> Self {
Self {
counter: Arc::new(AtomicU32::new(0)),
}
}
}

#[async_trait]
impl AsyncDrop for Test {
async fn async_drop(&mut self) {
self.counter.store(1, Ordering::SeqCst);
tokio::time::sleep(Duration::from_secs(1)).await;
self.counter.store(2, Ordering::SeqCst);
}
}

/// Ensure that non-`Default`-bounded dropper works with tokio
#[cfg(feature = "tokio")]
#[tokio::test(flavor = "multi_thread")]
async fn tokio_works() {
let start = std::time::Instant::now();
let counter = Arc::new(AtomicU32::new(0));

// Create and perform drop
let wrapped_t = AsyncDropper::new(Test {
counter: counter.clone(),
});
drop(wrapped_t);

assert!(
start.elapsed() > Duration::from_millis(500),
"two seconds have passed since drop"
);
assert_eq!(
counter.load(Ordering::SeqCst),
2,
"async_drop() ran to completion"
);
}

// TODO: this test is broken *because* of the timeout bug
// see: https://github.com/t3hmrman/async-dropper/pull/17
/// Ensure that non-`Default`-bounded dropper works with tokio with a timeout
#[cfg(feature = "tokio")]
#[tokio::test(flavor = "multi_thread")]
async fn tokio_works_with_timeout() {
let start = std::time::Instant::now();
let counter = Arc::new(AtomicU32::new(0));
let wrapped_t = AsyncDropper::with_timeout(
Duration::from_millis(500),
Test {
counter: counter.clone(),
},
);
drop(wrapped_t);
assert!(
start.elapsed() > Duration::from_millis(500),
"two seconds have passed since drop"
);
assert_eq!(
counter.load(Ordering::SeqCst),
1,
"async_drop() did not run to completion (should have timed out)"
);
}

/// Ensure that non-`Default`-bounded dropper works with async-std
#[cfg(feature = "async-std")]
#[async_std::test]
async fn async_std_works() {
let start = std::time::Instant::now();
let counter = Arc::new(AtomicU32::new(0));

let wrapped_t = AsyncDropper::new(Test {
counter: counter.clone(),
});
drop(wrapped_t);

assert!(
start.elapsed() > Duration::from_millis(500),
"two seconds have passed since drop"
);
assert_eq!(
counter.load(Ordering::SeqCst),
2,
"async_drop() ran to completion"
);
}

// TODO: this test is broken *because* of the timeout bug
// see: https://github.com/t3hmrman/async-dropper/pull/17
/// Ensure that non-`Default`-bounded dropper works with async-std with a timeout
#[cfg(feature = "async-std")]
#[async_std::test]
async fn async_std_works_with_timeout() {
let start = std::time::Instant::now();
let counter = Arc::new(AtomicU32::new(0));
let wrapped_t = AsyncDropper::with_timeout(
Duration::from_millis(500),
Test {
counter: counter.clone(),
},
);
drop(wrapped_t);
assert!(
start.elapsed() > Duration::from_millis(500),
"two seconds have passed since drop"
);
assert_eq!(
counter.load(Ordering::SeqCst),
1,
"async_drop() did not run to completion (should have timed out)"
);
}
}
Loading

0 comments on commit 8370b48

Please sign in to comment.