From 29bda657fab2d61142d729c6c8f9b6f141e9366c Mon Sep 17 00:00:00 2001 From: tison Date: Fri, 13 Nov 2026 11:02:16 +0800 Subject: [PATCH 1/4] refactor(once): replace ValueCell with a first-party implementation --- LICENSE | 29 ++-- asyncband/src/internal/value_cell.rs | 232 +++++++++++++++++---------- asyncband/src/once/lazy_cell/mod.rs | 12 +- asyncband/src/once/once_cell/mod.rs | 21 +-- 4 files changed, 178 insertions(+), 116 deletions(-) diff --git a/LICENSE b/LICENSE index 643e18a4..61092a2b 100644 --- a/LICENSE +++ b/LICENSE @@ -274,25 +274,22 @@ read guards use separate borrowed and owned types rather than changing a type parameter on the original read guard. All mapped guards use filter_map naming, and mutable mapped guards carry explicit invariance. -Portions of the following files originated from Tokio 1.47.0's OnceCell. Each -local path is followed by its upstream source path: - - asyncband/src/once/once_cell/mod.rs - tokio/src/sync/once_cell.rs - asyncband/src/internal/value_cell.rs - tokio/src/sync/once_cell.rs - -The exact Tokio 1.47.0 source is: +Portions of asyncband/src/once/once_cell/mod.rs originated from Tokio 1.47.0's +OnceCell: https://github.com/tokio-rs/tokio/blob/3911cb8523f190142f61c64b66881c07c0d3e7be/tokio/src/sync/once_cell.rs -Asyncband substantially changed this implementation. OnceCell uses a permit -only to serialize initialization and does not close its semaphore when a value -is published. It can remove a value and later initialize the cell again, and -its set operation waits for an in-progress initializer instead of reporting a -closed or initializing state. ValueCell factors storage out of OnceCell, -contains no semaphore or closed-state invariant, supports mutation, removal, -and reinitialization, and is shared by OnceCell and LazyCell. +Asyncband substantially changed this implementation. OnceCell delegates value +storage to Asyncband's own ValueCell, uses permits only to serialize +initialization, and does not close its semaphore when a value is published. It +can remove a value and later initialize the cell again, and its set operation +waits for an in-progress initializer instead of reporting a closed or +initializing state. + +The standard ASF source header on asyncband/src/once/once_cell/mod.rs applies +to the substantial modifications contributed to Apache Asyncband. It does not +replace the Tokio copyright notice or MIT License applicable to the incorporated +portions. Portions of the following files originated from Tokio 1.42.0's public and internal semaphore implementations: diff --git a/asyncband/src/internal/value_cell.rs b/asyncband/src/internal/value_cell.rs index 85f046a3..8c3f63be 100644 --- a/asyncband/src/internal/value_cell.rs +++ b/asyncband/src/internal/value_cell.rs @@ -15,136 +15,206 @@ // specific language governing permissions and limitations // under the License. -// This file was factored out of Asyncband's Tokio-derived OnceCell implementation. -// Copyright (c) Tokio Contributors -// The Tokio-derived portions remain licensed under the MIT License. -// Unlike Tokio's storage, ValueCell has no semaphore or closed-state invariant. It provides -// reusable value publication, mutation, removal, and reinitialization for OnceCell and LazyCell. -// Upstream source: -// https://github.com/tokio-rs/tokio/blob/3911cb8523f190142f61c64b66881c07c0d3e7be/tokio/src/sync/once_cell.rs - use std::cell::UnsafeCell; +use std::mem; +use std::mem::ManuallyDrop; use std::mem::MaybeUninit; use std::sync::atomic::AtomicBool; use std::sync::atomic::Ordering; -/// Storage for a value published exactly once. +/// Single-value storage with atomic publication and unsynchronized exclusive access. /// -/// Synchronizing initialization is deliberately left to the containing primitive. +/// Concurrently racing writers must be serialized by the caller before calling [`Self::publish`]. pub struct ValueCell { - initialized: AtomicBool, - value: UnsafeCell>, + published: AtomicBool, + slot: UnsafeCell>, } -// SAFETY: Shared access only exposes `&T`, and publication is synchronized by the initialized -// flag. The containing primitive is responsible for serializing writes. -unsafe impl Sync for ValueCell {} - -// SAFETY: Ownership of the cell and its value may be transferred when `T` is `Send`. +// SAFETY: Moving or dropping `ValueCell` transfers ownership of any stored `T`. unsafe impl Send for ValueCell {} +// SAFETY: `publish` transfers `T` across threads via `&self`, and `get` exposes `&T` concurrently. +unsafe impl Sync for ValueCell {} + impl ValueCell { - pub const fn new() -> Self { + pub const fn empty() -> Self { Self { - initialized: AtomicBool::new(false), - value: UnsafeCell::new(MaybeUninit::uninit()), + published: AtomicBool::new(false), + slot: UnsafeCell::new(MaybeUninit::uninit()), } } - pub const fn from_value(value: T) -> Self { + pub const fn with_value(value: T) -> Self { Self { - initialized: AtomicBool::new(true), - value: UnsafeCell::new(MaybeUninit::new(value)), + published: AtomicBool::new(true), + slot: UnsafeCell::new(MaybeUninit::new(value)), } } - pub fn is_initialized(&self) -> bool { - self.initialized.load(Ordering::Acquire) + pub fn is_published(&self) -> bool { + self.published.load(Ordering::Acquire) } - pub fn is_initialized_mut(&mut self) -> bool { - *self.initialized.get_mut() + pub fn has_value_mut(&mut self) -> bool { + *self.published.get_mut() } pub fn get(&self) -> Option<&T> { - if self.is_initialized() { - // SAFETY: The acquire load above observed publication of the value. - Some(unsafe { self.get_unchecked() }) - } else { - None + if !self.published.load(Ordering::Acquire) { + return None; } + // SAFETY: The Acquire load observed a completed `publish`, and shared references never + // mutate or clear an already published slot. + Some(unsafe { (*self.slot.get()).assume_init_ref() }) } pub fn get_mut(&mut self) -> Option<&mut T> { - if self.is_initialized_mut() { - // SAFETY: Exclusive access rules out concurrent reads or writes. - Some(unsafe { self.get_unchecked_mut() }) + if *self.published.get_mut() { + // SAFETY: `published` is true and `&mut self` guarantees exclusive access. + Some(unsafe { self.slot.get_mut().assume_init_mut() }) } else { None } } - pub fn into_inner(mut self) -> Option { - self.take() + /// Publishes `value` into an empty cell and returns a shared reference to it. + /// + /// # Safety + /// + /// The slot must currently be unpublished, and no concurrent operation may read, write, or + /// mutably borrow the slot until `publish` returns; no shared operation may clear or overwrite + /// the slot while the returned borrow is live. + pub unsafe fn publish(&self, value: T) -> &T { + debug_assert!(!self.published.load(Ordering::Relaxed)); + let slot = self.slot.get(); + // SAFETY: Caller guarantees exclusive write access while `published` is false. + // `MaybeUninit` is `#[repr(transparent)]` over `T`, so `slot.cast::()` has valid + // provenance and alignment for writing `T` without creating an intermediate `&mut`. + unsafe { slot.cast::().write(value) }; + self.published.store(true, Ordering::Release); + // SAFETY: `slot` now holds an initialized `T` that remains immutable while `&self` is live. + unsafe { (*slot).assume_init_ref() } + } + + pub fn write_mut(&mut self, value: T) -> &mut T { + if *self.published.get_mut() { + // SAFETY: `published` indicates an initialized value in `slot`. + unsafe { self.slot.get_mut().assume_init_drop() }; + } + *self.published.get_mut() = true; + self.slot.get_mut().write(value) } pub fn take(&mut self) -> Option { - if self.is_initialized_mut() { - *self.initialized.get_mut() = false; - // SAFETY: The value was initialized and the flag was cleared so it will not be - // dropped a second time. - Some(unsafe { self.value.get_mut().assume_init_read() }) + if !mem::replace(self.published.get_mut(), false) { + return None; + } + // SAFETY: `published` was true and was cleared before moving the value out. + Some(unsafe { self.slot.get_mut().assume_init_read() }) + } + + pub fn into_inner(self) -> Option { + let mut this = ManuallyDrop::new(self); + if *this.published.get_mut() { + // SAFETY: `this` is wrapped in `ManuallyDrop` and `published` is true. + Some(unsafe { this.slot.get_mut().assume_init_read() }) } else { None } } +} - /// Publishes a value after the containing primitive has won initialization. - /// - /// # Safety - /// - /// The cell must be uninitialized, and no other initialization may read or write it. - pub unsafe fn set(&self, value: T) -> &T { - debug_assert!(!self.is_initialized()); - let value_ptr = self.value.get(); - unsafe { value_ptr.write(MaybeUninit::new(value)) }; +impl Drop for ValueCell { + fn drop(&mut self) { + if *self.published.get_mut() { + // SAFETY: `published` is true and `drop` has exclusive access. + unsafe { self.slot.get_mut().assume_init_drop() }; + } + } +} - // Publish the initialized value to readers performing an acquire load. - self.initialized.store(true, Ordering::Release); +#[cfg(test)] +mod tests { + use std::cell::Cell; + use std::thread; - // SAFETY: The value was initialized and published above. - unsafe { self.get_unchecked() } - } + use super::*; - pub fn set_mut(&mut self, value: T) -> &mut T { - debug_assert!(!self.is_initialized_mut()); - let value = self.value.get_mut().write(value); - *self.initialized.get_mut() = true; - value - } + struct DropCounter<'a>(&'a Cell); - /// # Safety - /// - /// The cell must be initialized. - unsafe fn get_unchecked(&self) -> &T { - debug_assert!(self.is_initialized()); - unsafe { (&*self.value.get()).assume_init_ref() } + impl Drop for DropCounter<'_> { + fn drop(&mut self) { + self.0.set(self.0.get() + 1); + } } - /// # Safety - /// - /// The cell must be initialized and exclusively borrowed. - unsafe fn get_unchecked_mut(&mut self) -> &mut T { - debug_assert!(self.is_initialized_mut()); - unsafe { (&mut *self.value.get()).assume_init_mut() } + #[test] + fn concurrent_readers_observe_published_reference() { + let cell: ValueCell> = ValueCell::empty(); + + let published = thread::scope(|scope| { + let mut readers = Vec::with_capacity(2); + for _ in 0..2 { + readers.push(scope.spawn(|| { + loop { + if let Some(values) = cell.get() { + return values.as_slice(); + } + thread::yield_now(); + } + })); + } + + // SAFETY: No other writer exists and readers only observe via `get`. + let published = unsafe { cell.publish(vec![10, 20, 30]) }; + for reader in readers { + assert_eq!(reader.join().unwrap(), &[10, 20, 30]); + } + published.as_slice() + }); + + assert_eq!(published, &[10, 20, 30]); } -} -impl Drop for ValueCell { - fn drop(&mut self) { - if self.is_initialized_mut() { - // SAFETY: The value is initialized and exclusive access rules out other users. - unsafe { self.value.get_mut().assume_init_drop() }; - } + #[test] + fn exclusive_lifecycle_preserves_drop_counts() { + let drops = Cell::new(0); + + let mut cell = ValueCell::empty(); + assert!(!cell.is_published()); + assert!(!cell.has_value_mut()); + assert!(cell.get().is_none()); + assert!(cell.get_mut().is_none()); + assert!(cell.take().is_none()); + + cell.write_mut(DropCounter(&drops)); + assert!(cell.has_value_mut()); + assert_eq!(drops.get(), 0); + + // Overwriting an already populated slot must drop the prior value. + cell.write_mut(DropCounter(&drops)); + assert_eq!(drops.get(), 1); + + let taken = cell.take(); + assert!(taken.is_some()); + assert!(!cell.has_value_mut()); + assert_eq!(drops.get(), 1); + drop(taken); + assert_eq!(drops.get(), 2); + + // Re-publishing after `take` works and transfers ownership to `into_inner`. + // SAFETY: `cell` is empty and accessed from a single thread. + unsafe { cell.publish(DropCounter(&drops)) }; + assert!(cell.is_published()); + let extracted = cell.into_inner(); + assert!(extracted.is_some()); + assert_eq!(drops.get(), 2); + drop(extracted); + assert_eq!(drops.get(), 3); + + // `with_value` and `Drop` drop the stored value once. + let cell = ValueCell::with_value(DropCounter(&drops)); + drop(cell); + assert_eq!(drops.get(), 4); } } diff --git a/asyncband/src/once/lazy_cell/mod.rs b/asyncband/src/once/lazy_cell/mod.rs index a0418765..37c49e10 100644 --- a/asyncband/src/once/lazy_cell/mod.rs +++ b/asyncband/src/once/lazy_cell/mod.rs @@ -161,7 +161,7 @@ impl LazyCell { Fut: Future, { Self { - value: ValueCell::new(), + value: ValueCell::empty(), state: Mutex::new(State::Initializer(Some(initializer))), poisoned: AtomicBool::new(false), } @@ -299,7 +299,7 @@ where // SAFETY: The state mutex serializes initialization, and the double check above verified // that the value was not initialized by another caller. - unsafe { this_ref.value.set(value) } + unsafe { this_ref.value.publish(value) } } /// Initializes a pinned cell and returns mutable access to its value. @@ -328,7 +328,7 @@ where // SAFETY: We do not move the structurally pinned future through this reference. The stored // value is not structurally pinned and may be accessed normally. let this: &'a mut Self = unsafe { Pin::get_unchecked_mut(this) }; - if this.value.is_initialized_mut() { + if this.value.has_value_mut() { return this .value .get_mut() @@ -341,7 +341,7 @@ where // SAFETY: Exclusive access to the pinned cell pins its state in place for this call. let state = unsafe { Pin::new_unchecked(this.state.get_mut()) }; let value = drive_pinned_attempt(state, &this.poisoned).await; - this.value.set_mut(value) + this.value.write_mut(value) } } @@ -425,7 +425,7 @@ where /// before forcing it when `Fut` is not `Unpin`. pub fn from_future(future: Fut) -> Self { Self { - value: ValueCell::new(), + value: ValueCell::empty(), state: Mutex::new(State::Attempt(future)), poisoned: AtomicBool::new(false), } @@ -438,7 +438,7 @@ impl LazyCell> { /// Creates a new `LazyCell` that already contains `value`. pub const fn from_value(value: T) -> Self { Self { - value: ValueCell::from_value(value), + value: ValueCell::with_value(value), state: Mutex::new(State::Complete), poisoned: AtomicBool::new(false), } diff --git a/asyncband/src/once/once_cell/mod.rs b/asyncband/src/once/once_cell/mod.rs index 8c71874c..e6cc9403 100644 --- a/asyncband/src/once/once_cell/mod.rs +++ b/asyncband/src/once/once_cell/mod.rs @@ -19,8 +19,8 @@ // Copyright (c) Tokio Contributors // The Tokio-derived portions remain licensed under the MIT License. // Asyncband substantially changed the synchronization model: initialization does not close the -// semaphore, the cell can be emptied and initialized again, and value storage is delegated to a -// reusable ValueCell shared with LazyCell. +// semaphore, the cell can be emptied and initialized again, and value storage is delegated to +// Asyncband's own ValueCell shared with LazyCell. // Upstream source: // https://github.com/tokio-rs/tokio/blob/3911cb8523f190142f61c64b66881c07c0d3e7be/tokio/src/sync/once_cell.rs @@ -74,7 +74,7 @@ impl OnceCell { /// Creates a new empty `OnceCell`. pub const fn new() -> Self { Self { - value: ValueCell::new(), + value: ValueCell::empty(), semaphore: Semaphore::new(3), } } @@ -82,7 +82,7 @@ impl OnceCell { /// Creates a new `OnceCell` initialized with the provided value. pub const fn from_value(value: T) -> Self { Self { - value: ValueCell::from_value(value), + value: ValueCell::with_value(value), semaphore: Semaphore::new(3), } } @@ -92,12 +92,7 @@ impl OnceCell { // not need the internal helper. #[allow(dead_code)] pub(crate) fn initialized(&self) -> bool { - self.value.is_initialized() - } - - /// Returns whether the cell contains a value. - pub(crate) fn initialized_mut(&mut self) -> bool { - self.value.is_initialized_mut() + self.value.is_published() } /// Returns the stored value. @@ -246,7 +241,7 @@ impl OnceCell { { // Workaround if let Some(v) = self.get_mut() { return Ok(v); } // @see https://github.com/rust-lang/rust/issues/51545 - if self.initialized_mut() { + if self.value.has_value_mut() { return Ok(self .value .get_mut() @@ -384,11 +379,11 @@ impl OnceCell { fn set_value(&self, value: T, _permit: SemaphorePermit<'_>) -> &T { // SAFETY: Holding two of the three semaphore permits serializes initialization. - unsafe { self.value.set(value) } + unsafe { self.value.publish(value) } } fn set_value_mut(&mut self, value: T) -> &mut T { - self.value.set_mut(value) + self.value.write_mut(value) } } From e53c16dbd48b9a62576ef94dabd832a727a10d1c Mon Sep 17 00:00:00 2001 From: tison Date: Fri, 13 Nov 2026 11:05:37 +0800 Subject: [PATCH 2/4] docs: rewrite remaining third-party comments, examples, and tests --- asyncband/src/condvar/mod.rs | 40 +-- asyncband/src/internal/semaphore.rs | 29 +- asyncband/src/mpmc/mod.rs | 2 +- asyncband/src/mpsc/bounded/mod.rs | 3 +- asyncband/src/mpsc/unbounded/mod.rs | 3 +- asyncband/src/mutex/mod.rs | 53 ++-- asyncband/src/once/lazy_cell/mod.rs | 6 +- asyncband/src/once/once/mod.rs | 17 +- asyncband/src/once/once_cell/mod.rs | 190 ++++++------- asyncband/src/oneshot/mod.rs | 37 ++- asyncband/src/oneshot/receiver.rs | 18 +- asyncband/src/oneshot/sender.rs | 14 +- asyncband/src/semaphore/mod.rs | 309 +++++++++------------- tests-integration/tests/semaphore_test.rs | 99 +++---- 14 files changed, 364 insertions(+), 456 deletions(-) diff --git a/asyncband/src/condvar/mod.rs b/asyncband/src/condvar/mod.rs index d270776c..796c794e 100644 --- a/asyncband/src/condvar/mod.rs +++ b/asyncband/src/condvar/mod.rs @@ -39,17 +39,17 @@ //! use asyncband::condvar::Condvar; //! use asyncband::mutex::Mutex; //! -//! let pair = Arc::new((Mutex::new(false), Condvar::new())); -//! let notifier_pair = pair.clone(); +//! let state = Arc::new((Mutex::new(false), Condvar::new())); +//! let notifier_state = state.clone(); //! //! let notifier = tokio::spawn(async move { -//! let (lock, cvar) = &*notifier_pair; +//! let (lock, cvar) = &*notifier_state; //! let mut ready = lock.lock().await; //! *ready = true; //! cvar.notify_one(); //! }); //! -//! let (lock, cvar) = &*pair; +//! let (lock, cvar) = &*state; //! let ready = cvar.wait_while(lock.lock().await, |ready| !*ready).await; //! assert!(*ready); //! drop(ready); @@ -210,9 +210,11 @@ impl Condvar { guard } - /// Yields the current task until this condition variable receives a notification and the - /// provided condition becomes false. Spurious wake-ups are ignored and this function will only - /// return once the condition has been met. + /// Waits on this condition variable while `condition` returns `true`, returning the guard once + /// `condition` evaluates to `false`. + /// + /// `condition` is checked under the lock before waiting and after every wakeup; if it is + /// already `false` on entry, this method returns immediately without waiting. /// /// ``` /// # #[tokio::main] @@ -222,17 +224,17 @@ impl Condvar { /// use asyncband::condvar::Condvar; /// use asyncband::mutex::Mutex; /// - /// let pair = Arc::new((Mutex::new(false), Condvar::new())); - /// let pair_clone = pair.clone(); + /// let state = Arc::new((Mutex::new(false), Condvar::new())); + /// let state_clone = state.clone(); /// /// let notifier = tokio::spawn(async move { - /// let (lock, cvar) = &*pair_clone; + /// let (lock, cvar) = &*state_clone; /// let mut started = lock.lock().await; /// *started = true; /// cvar.notify_one(); /// }); /// - /// let (lock, cvar) = &*pair; + /// let (lock, cvar) = &*state; /// let guard = cvar /// .wait_while(lock.lock().await, |started| !*started) /// .await; @@ -260,9 +262,11 @@ impl Condvar { guard } - /// Yields the current task until this condition variable receives a notification and the - /// provided condition becomes false. Spurious wake-ups are ignored and this function will only - /// return once the condition has been met. + /// Waits on this condition variable while `condition` returns `true`, returning the owned + /// guard once `condition` evaluates to `false`. + /// + /// `condition` is checked under the lock before waiting and after every wakeup; if it is + /// already `false` on entry, this method returns immediately without waiting. /// /// ``` /// # #[tokio::main] @@ -272,17 +276,17 @@ impl Condvar { /// use asyncband::condvar::Condvar; /// use asyncband::mutex::Mutex; /// - /// let pair = (Arc::new(Mutex::new(false)), Arc::new(Condvar::new())); - /// let pair_clone = pair.clone(); + /// let state = (Arc::new(Mutex::new(false)), Arc::new(Condvar::new())); + /// let state_clone = state.clone(); /// /// let notifier = tokio::spawn(async move { - /// let (lock, cvar) = pair_clone; + /// let (lock, cvar) = state_clone; /// let mut started = lock.lock_owned().await; /// *started = true; /// cvar.notify_one(); /// }); /// - /// let (lock, cvar) = pair; + /// let (lock, cvar) = state; /// let guard = cvar /// .wait_while_owned(lock.lock_owned().await, |started| !*started) /// .await; diff --git a/asyncband/src/internal/semaphore.rs b/asyncband/src/internal/semaphore.rs index 2964757a..66424751 100644 --- a/asyncband/src/internal/semaphore.rs +++ b/asyncband/src/internal/semaphore.rs @@ -43,7 +43,7 @@ use crate::internal::waker_batch::WakerBatch; /// The internal semaphore that provides low-level async primitives. #[derive(Debug)] pub struct Semaphore { - /// The current number of available permits in the semaphore. + /// Permits currently available without waiting. available_permits: AtomicUsize, /// Set when a release holding the queue lock loses a CAS race, and cleared after it /// successfully updates the available count. Releases that observe this flag skip the @@ -70,14 +70,14 @@ impl Semaphore { } } - /// Returns the current number of available permits. + /// Reads the count of permits presently available. pub fn available_permits(&self) -> usize { self.available_permits.load(Ordering::Acquire) } - /// Tries to acquire `n` permits from the semaphore. + /// Claims `n` permits immediately if enough are available. /// - /// Returns `true` if the permits were acquired, `false` otherwise. + /// Returns `false` without mutating the count when fewer than `n` permits are present. pub fn try_acquire(&self, n: usize) -> bool { let mut available = self.available_permits.load(Ordering::Acquire); loop { @@ -98,9 +98,7 @@ impl Semaphore { } } - /// Drains up to `up_to` permits that are currently available. - /// - /// Returns the number of permits that were actually drained. + /// Removes at most `up_to` immediately available permits and returns how many were taken. pub fn drain_permits(&self, up_to: usize) -> usize { if up_to == 0 { return 0; @@ -338,7 +336,6 @@ impl Acquire<'_> { } } None => { - // not yet enqueued let requested = *requested_permits; if acquired_or_enqueue(semaphore, requested, Some(index), Some(waker), true) { @@ -385,13 +382,9 @@ fn acquired_or_enqueue( }; if needed > 0 && lock.is_none() { - // No permits were immediately available, so this permit will - // (probably) need to wait. We'll need to acquire a lock on the - // wait queue before continuing. We need to do this _before_ the - // CAS that sets the new value of the semaphore's `available_permits` - // counter. Otherwise, if we subtract the permits and then - // acquire the lock, we might miss additional permits being - // added while waiting for the lock. + // Lock the wait queue before zeroing `available_permits`. If the counter reached zero + // first, a concurrent release could observe an empty queue and publish permits without + // waking the waiter we are about to link. lock = Some(sem.waiters.lock()); } @@ -401,19 +394,15 @@ fn acquired_or_enqueue( Ordering::AcqRel, Ordering::Acquire, ) { - // other thread changed the permits; retry available = actual; continue; } - // all requested permits were acquired if needed == 0 { return true; } - // all available permits were acquired, but more are needed; - // enqueue a waiter for the permits still needed - + // Link the waiter for the remaining shortfall under the lock held above. let mut waiters = lock.take().unwrap_or_else(|| { unreachable!("lock must be acquired when needed {needed} > 0"); }); diff --git a/asyncband/src/mpmc/mod.rs b/asyncband/src/mpmc/mod.rs index ba484949..3f784cfe 100644 --- a/asyncband/src/mpmc/mod.rs +++ b/asyncband/src/mpmc/mod.rs @@ -15,7 +15,7 @@ // specific language governing permissions and limitations // under the License. -//! A multi-producer, multi-consumer queue for sending values between asynchronous tasks. +//! Multi-producer, multi-consumer channels that distribute each message to one receiver. //! //! Receivers compete for values: each value accepted by a sender is delivered to exactly one //! receiver while a receiver remains. Clone a receiver to distribute work across multiple diff --git a/asyncband/src/mpsc/bounded/mod.rs b/asyncband/src/mpsc/bounded/mod.rs index 00cddfd3..d9accbc5 100644 --- a/asyncband/src/mpsc/bounded/mod.rs +++ b/asyncband/src/mpsc/bounded/mod.rs @@ -15,8 +15,7 @@ // specific language governing permissions and limitations // under the License. -//! A bounded multi-producer, single-consumer queue for sending values between asynchronous -//! tasks with backpressure control. +//! Bounded multi-producer, single-consumer channel with capacity backpressure. use std::collections::VecDeque; use std::sync::Arc; diff --git a/asyncband/src/mpsc/unbounded/mod.rs b/asyncband/src/mpsc/unbounded/mod.rs index 086f1807..b1c6c1f3 100644 --- a/asyncband/src/mpsc/unbounded/mod.rs +++ b/asyncband/src/mpsc/unbounded/mod.rs @@ -15,8 +15,7 @@ // specific language governing permissions and limitations // under the License. -//! An unbounded multi-producer, single-consumer queue for sending values between asynchronous -//! tasks. +//! Unbounded multi-producer, single-consumer channel. use std::sync::Arc; use std::task::Waker; diff --git a/asyncband/src/mutex/mod.rs b/asyncband/src/mutex/mod.rs index da8e81c2..74b3bd0d 100644 --- a/asyncband/src/mutex/mod.rs +++ b/asyncband/src/mutex/mod.rs @@ -132,9 +132,9 @@ impl Mutex { /// ``` /// use asyncband::mutex::Mutex; /// - /// let mutex = Mutex::new(1); - /// let n = mutex.into_inner(); - /// assert_eq!(n, 1); + /// let protected = Mutex::new(String::from("ready")); + /// let value = protected.into_inner(); + /// assert_eq!(value, "ready"); /// ``` pub fn into_inner(self) -> T { self.c.into_inner() @@ -155,10 +155,11 @@ impl Mutex { /// # async fn main() { /// use asyncband::mutex::Mutex; /// - /// let mutex = Mutex::new(1); + /// let entries = Mutex::new(vec![10]); /// - /// let mut n = mutex.lock().await; - /// *n = 2; + /// let mut guard = entries.lock().await; + /// guard.push(20); + /// assert_eq!(*guard, [10, 20]); /// # } /// ``` pub async fn lock(&self) -> MutexGuard<'_, T> { @@ -173,10 +174,13 @@ impl Mutex { /// ``` /// use asyncband::mutex::Mutex; /// - /// let mutex = Mutex::new(1); - /// let mut guard = mutex.try_lock().expect("mutex is locked"); - /// *guard += 1; - /// assert_eq!(2, *guard); + /// let count = Mutex::new(5); + /// { + /// let mut guard = count.try_lock().unwrap(); + /// *guard += 3; + /// assert!(count.try_lock().is_none()); + /// } + /// assert_eq!(count.into_inner(), 8); /// ``` pub fn try_lock(&self) -> Option> { if self.s.try_acquire(1) { @@ -205,10 +209,17 @@ impl Mutex { /// /// use asyncband::mutex::Mutex; /// - /// let mutex = Arc::new(Mutex::new(1)); + /// let shared = Arc::new(Mutex::new(String::from("init"))); + /// let worker_shared = shared.clone(); + /// + /// tokio::spawn(async move { + /// let mut guard = worker_shared.lock_owned().await; + /// guard.push_str("-done"); + /// }) + /// .await + /// .unwrap(); /// - /// let mut n = mutex.clone().lock_owned().await; - /// *n = 2; + /// assert_eq!(shared.lock().await.as_str(), "init-done"); /// # } /// ``` pub async fn lock_owned(self: Arc) -> OwnedMutexGuard { @@ -227,10 +238,12 @@ impl Mutex { /// /// use asyncband::mutex::Mutex; /// - /// let mutex = Arc::new(Mutex::new(1)); - /// let mut guard = mutex.clone().try_lock_owned().expect("mutex is locked"); - /// *guard += 1; - /// assert_eq!(2, *guard); + /// let shared = Arc::new(Mutex::new(vec!["a"])); + /// let mut guard = shared.clone().try_lock_owned().unwrap(); + /// guard.push("b"); + /// assert!(shared.clone().try_lock_owned().is_none()); + /// drop(guard); + /// assert_eq!(shared.try_lock_owned().unwrap().len(), 2); /// ``` pub fn try_lock_owned(self: Arc) -> Option> { if self.s.try_acquire(1) { @@ -251,9 +264,9 @@ impl Mutex { /// ``` /// use asyncband::mutex::Mutex; /// - /// let mut mutex = Mutex::new(1); - /// let n = mutex.get_mut(); - /// *n = 2; + /// let mut protected = Mutex::new(String::from("draft")); + /// protected.get_mut().push_str("-final"); + /// assert_eq!(protected.into_inner(), "draft-final"); /// ``` pub fn get_mut(&mut self) -> &mut T { self.c.get_mut() diff --git a/asyncband/src/once/lazy_cell/mod.rs b/asyncband/src/once/lazy_cell/mod.rs index 37c49e10..8e6fc19d 100644 --- a/asyncband/src/once/lazy_cell/mod.rs +++ b/asyncband/src/once/lazy_cell/mod.rs @@ -124,7 +124,7 @@ enum State { } impl LazyCell { - /// Creates a new lazy value with the given asynchronous initializer. + /// Creates an uninitialized cell that runs `initializer` on first forced access. /// /// The initializer is not called until the first forcing future is polled. Its returned future /// is stored as-is; this method never boxes it. @@ -167,7 +167,7 @@ impl LazyCell { } } - /// Returns a reference to the value if initialized. + /// Returns a shared reference to the initialized value, if available. /// /// This method never starts initialization or waits for an active attempt. It returns `None` /// when the cell is uninitialized, initializing, or poisoned. @@ -175,7 +175,7 @@ impl LazyCell { this.value.get() } - /// Returns a mutable reference to the value if initialized. + /// Returns an exclusive reference to the initialized value, if available. /// /// This method never starts initialization. It returns `None` when the cell is uninitialized /// or poisoned. diff --git a/asyncband/src/once/once/mod.rs b/asyncband/src/once/once/mod.rs index 5389d858..3c681588 100644 --- a/asyncband/src/once/once/mod.rs +++ b/asyncband/src/once/once/mod.rs @@ -131,17 +131,16 @@ impl Once { self.done.try_wait().is_ok() } - /// Calls the given async closure if this is the first time `call_once` has been called - /// on this `Once` instance. + /// Runs `init` if this `Once` has not yet completed an initialization. /// - /// If another task is currently running the closure, this call will wait for that task - /// to complete. + /// Concurrent callers arriving while `init` is in progress wait until it finishes. Only one + /// caller can succeed; once an execution returns normally, every current and future call + /// completes immediately without running `init` again. /// - /// If the provided operation is cancelled, the initialization attempt is cancelled. If there - /// are other tasks waiting, one of them will start another attempt. - /// - /// Calling `call_once` recursively on the same `Once` from its initializer deadlocks because - /// the inner call waits for the outer call to finish. + /// If the active attempt panics or its future is dropped before completing, the `Once` + /// stays uncompleted and a queued caller starts a fresh attempt. Calling `call_once` + /// recursively on the same `Once` from inside `init` deadlocks while waiting for the outer + /// call to finish. /// /// # Examples /// diff --git a/asyncband/src/once/once_cell/mod.rs b/asyncband/src/once/once_cell/mod.rs index e6cc9403..1f28ac06 100644 --- a/asyncband/src/once/once_cell/mod.rs +++ b/asyncband/src/once/once_cell/mod.rs @@ -95,35 +95,30 @@ impl OnceCell { self.value.is_published() } - /// Returns the stored value. + /// Returns a shared reference to the stored value without waiting. /// - /// Returns `None` if the cell is uninitialized, or being initialized. - /// - /// This method never blocks. + /// Yields [`None`] while the cell is empty, including when another task is in the middle of + /// initializing it. pub fn get(&self) -> Option<&T> { self.value.get() } - /// Returns mutable access to the stored value. - /// - /// Returns `None` if the cell is uninitialized. + /// Returns an exclusive reference to the stored value without blocking or initializing. /// - /// This method never blocks. Since it borrows the `OnceCell` mutably, it is statically - /// guaranteed that no active borrows to the `OnceCell` exist, including from other threads. + /// Yields [`None`] when the cell is empty. Because `&mut self` forbids concurrent access, no + /// asynchronous initialization can be in flight during this call. pub fn get_mut(&mut self) -> Option<&mut T> { self.value.get_mut() } - /// Returns the value, initializing an empty cell with the provided asynchronous function. + /// Returns a reference to the stored value, running `init` if the cell is currently empty. /// - /// If some other task is currently working on initializing the `OnceCell`, this call will wait - /// for that other task to finish, then return the value that the other task produced. + /// Concurrent callers share one in-flight initialization: later callers wait for the active + /// attempt to complete and receive a reference to its published value. If the active + /// initializer panics or its future is dropped before completing, the cell remains empty and a + /// waiting caller takes over with its own `init`. /// - /// If the provided operation is cancelled, the initialization attempt is cancelled. If there - /// are other tasks waiting for the value to be initialized, one of them will start another - /// attempt at initializing the value. - /// - /// This will deadlock if `init` tries to initialize the cell recursively. + /// Calling this method recursively from inside `init` on the same cell will deadlock. pub async fn get_or_init(&self, init: F) -> &T where F: AsyncFnOnce() -> T, @@ -136,17 +131,13 @@ impl OnceCell { } } - /// Returns the value, initializing an empty cell with the provided fallible asynchronous - /// function. - /// - /// If some other task is currently working on initializing the `OnceCell`, this call will wait - /// for that other task to finish, then return the value that the other task produced. + /// Returns a reference to the stored value, running a fallible `init` if the cell is empty. /// - /// If the provided operation returns an error, is cancelled or panics, the initialization - /// attempt is cancelled. If there are other tasks waiting for the value to be initialized - /// one of them will start another attempt at initializing the value. + /// Concurrent callers wait while an initialization attempt is running. When that attempt + /// publishes a value, waiting callers receive a reference to it; when it returns `Err`, + /// panics, or is cancelled, the cell stays empty and the next queued caller runs its `init`. /// - /// This will deadlock if `init` tries to initialize the cell recursively. + /// Calling this method recursively from inside `init` on the same cell will deadlock. pub async fn get_or_try_init(&self, init: F) -> Result<&T, E> where F: AsyncFnOnce() -> Result, @@ -158,8 +149,7 @@ impl OnceCell { let permit = self.semaphore.acquire(2).await; if let Some(v) = self.get() { - // double-checked: another task initialized the value - // while we were waiting for the permit + // Another task initialized the cell while we waited for permits. return Ok(v); } @@ -167,15 +157,10 @@ impl OnceCell { Ok(self.set_value(value, permit)) } - /// Returns mutable access to the value, initializing an empty cell asynchronously. + /// Returns exclusive access to the stored value, running `init` if the cell is empty. /// - /// This method never blocks other tasks because it takes `&mut self`, which guarantees - /// exclusive access to the `OnceCell` and thus no concurrent initialization can be in - /// progress. - /// - /// If the cell is already initialized, it returns a mutable reference to the existing value. - /// Otherwise, it runs `init`, stores the result, and returns a mutable reference to the newly - /// initialized value. + /// Exclusive ownership (`&mut self`) rules out concurrent initialization, so this method runs + /// `init` directly when needed without acquiring the semaphore. /// /// # Examples /// @@ -184,10 +169,10 @@ impl OnceCell { /// # async fn main() { /// use asyncband::once::OnceCell; /// - /// let mut cell: OnceCell = OnceCell::new(); - /// let v = cell.get_mut_or_init(|| async { 41 }).await; - /// *v += 1; - /// assert_eq!(*cell.get().unwrap(), 42); + /// let mut slot = OnceCell::new(); + /// let text = slot.get_mut_or_init(|| async { String::from("abc") }).await; + /// text.push('d'); + /// assert_eq!(slot.get().map(String::as_str), Some("abcd")); /// # } /// ``` pub async fn get_mut_or_init(&mut self, init: F) -> &mut T @@ -202,17 +187,11 @@ impl OnceCell { } } - /// Returns mutable access to the value, initializing an empty cell with a fallible asynchronous - /// function. - /// - /// This method never blocks other tasks because it takes `&mut self`, which guarantees - /// exclusive access to the `OnceCell` and thus no concurrent initialization can be in - /// progress. + /// Returns exclusive access to the stored value, running a fallible `init` if the cell is + /// empty. /// - /// If the cell is already initialized, it returns a mutable reference to the existing value. - /// Otherwise, it runs `init`. On success, it stores the result and returns a mutable - /// reference to the newly initialized value. On error, it returns the error and leaves the - /// cell uninitialized. + /// Exclusive ownership (`&mut self`) rules out concurrent initialization, so this method runs + /// `init` directly when needed. Returning an error leaves the cell empty for a later call. /// /// # Examples /// @@ -221,26 +200,26 @@ impl OnceCell { /// # async fn main() { /// use asyncband::once::OnceCell; /// - /// let mut cell: OnceCell = OnceCell::new(); + /// let mut slot: OnceCell = OnceCell::new(); /// assert!( - /// cell.get_mut_or_try_init(|| async { Err(()) }) + /// slot.get_mut_or_try_init(|| async { Err("unavailable") }) /// .await /// .is_err() /// ); - /// let v = cell - /// .get_mut_or_try_init(|| async { Ok::<_, ()>(10) }) + /// + /// let count = slot + /// .get_mut_or_try_init(|| async { Ok::<_, &str>(7) }) /// .await /// .unwrap(); - /// *v += 5; - /// assert_eq!(*cell.get().unwrap(), 15); + /// *count *= 3; + /// assert_eq!(slot.get(), Some(&21)); /// # } /// ``` pub async fn get_mut_or_try_init(&mut self, init: F) -> Result<&mut T, E> where F: AsyncFnOnce() -> Result, { - // Workaround if let Some(v) = self.get_mut() { return Ok(v); } - // @see https://github.com/rust-lang/rust/issues/51545 + // Check the flag before borrowing so the fallback branch can call `set_value_mut`. if self.value.has_value_mut() { return Ok(self .value @@ -252,35 +231,22 @@ impl OnceCell { Ok(self.set_value_mut(value)) } - /// Initializes the contents of the cell to `value` if the cell was uninitialized, - /// then returns a reference to it. - /// - /// May wait if another task is currently attempting to initialize the cell. The cell is - /// guaranteed to contain a value when `try_insert` returns, though not necessarily the - /// one provided. + /// Stores `value` if the cell is empty and returns a reference to the resulting contents. /// - /// Returns `Ok(&value)` if the cell was uninitialized and `Err((¤t_value, value))` - /// if it was already initialized. + /// If another initialization is currently in flight, this call waits until that attempt + /// finishes. Returns `Ok(&stored)` when `value` was installed, or `Err((&existing, value))` if + /// the cell already held a value. /// /// # Examples /// /// ``` - /// use asyncband::once::OnceCell; - /// - /// static CELL: OnceCell = OnceCell::new(); - /// /// # #[tokio::main] /// # async fn main() { - /// assert!(CELL.get().is_none()); - /// - /// tokio::spawn(async { - /// assert_eq!(CELL.try_insert(92).await, Ok(&92)); - /// }) - /// .await - /// .unwrap(); + /// use asyncband::once::OnceCell; /// - /// assert_eq!(CELL.try_insert(62).await, Err((&92, 62))); - /// assert_eq!(CELL.get(), Some(&92)); + /// let slot = OnceCell::new(); + /// assert_eq!(slot.try_insert("alpha").await, Ok(&"alpha")); + /// assert_eq!(slot.try_insert("beta").await, Err((&"alpha", "beta"))); /// # } /// ``` pub async fn try_insert(&self, value: T) -> Result<&T, (&T, T)> { @@ -292,33 +258,31 @@ impl OnceCell { } } - /// Initializes the contents of the cell to `value`. + /// Stores `value` if the cell is empty, waiting behind any active initialization. /// - /// May wait if another task is currently attempting to initialize the cell. The cell is - /// guaranteed to contain a value when `set` returns, though not necessarily the one provided. - /// - /// Returns `Ok(())` if the cell was uninitialized and `Err(value)` if the cell was already - /// initialized. + /// Returns `Ok(())` when `value` was installed, or `Err(value)` if another caller had already + /// populated the cell. /// /// # Examples /// /// ``` - /// use asyncband::once::OnceCell; - /// - /// static CELL: OnceCell = OnceCell::new(); - /// /// # #[tokio::main] /// # async fn main() { - /// assert!(CELL.get().is_none()); + /// use std::sync::Arc; /// - /// tokio::spawn(async { - /// assert_eq!(CELL.set(92).await, Ok(())); + /// use asyncband::once::OnceCell; + /// + /// let shared = Arc::new(OnceCell::new()); + /// let first = shared.clone(); + /// + /// tokio::spawn(async move { + /// assert_eq!(first.set("winner").await, Ok(())); /// }) /// .await /// .unwrap(); /// - /// assert_eq!(CELL.set(62).await, Err(62)); - /// assert_eq!(CELL.get(), Some(&92)); + /// assert_eq!(shared.set("runner-up").await, Err("runner-up")); + /// assert_eq!(shared.get(), Some(&"winner")); /// # } /// ``` pub async fn set(&self, value: T) -> Result<(), T> { @@ -328,49 +292,43 @@ impl OnceCell { } } - /// Consumes the `OnceCell`, returning the wrapped value. Returns `None` if the cell was - /// uninitialized. + /// Consumes the cell and extracts its stored value, or [`None`] if empty. /// /// # Examples /// /// ``` - /// use asyncband::once::OnceCell; - /// /// # #[tokio::main] /// # async fn main() { - /// let cell: OnceCell = OnceCell::new(); - /// assert_eq!(cell.into_inner(), None); + /// use asyncband::once::OnceCell; /// - /// let cell = OnceCell::new(); - /// cell.set("hello".to_string()).await.unwrap(); - /// assert_eq!(cell.into_inner(), Some("hello".to_string())); + /// let empty: OnceCell = OnceCell::new(); + /// assert_eq!(empty.into_inner(), None); + /// + /// let populated = OnceCell::from_value(2048); + /// assert_eq!(populated.into_inner(), Some(2048)); /// # } /// ``` pub fn into_inner(self) -> Option { self.value.into_inner() } - /// Takes the value out of this `OnceCell`, moving it back to an uninitialized state. - /// - /// Has no effect and returns `None` if the `OnceCell` was uninitialized. + /// Removes the stored value, leaving the cell empty so a future call can populate it again. /// - /// Since this method borrows the `OnceCell` mutably, it is statically guaranteed that - /// no active borrows to the `OnceCell` exist, including from other threads. + /// Returns [`None`] if the cell held no value. /// /// # Examples /// /// ``` - /// use asyncband::once::OnceCell; - /// /// # #[tokio::main] /// # async fn main() { - /// let mut cell: OnceCell = OnceCell::new(); - /// assert_eq!(cell.take(), None); + /// use asyncband::once::OnceCell; + /// + /// let mut slot = OnceCell::from_value(String::from("cached")); + /// assert_eq!(slot.take().as_deref(), Some("cached")); + /// assert_eq!(slot.take(), None); /// - /// let mut cell = OnceCell::new(); - /// cell.set("hello".to_string()).await.unwrap(); - /// assert_eq!(cell.take(), Some("hello".to_string())); - /// assert_eq!(cell.get(), None); + /// slot.set(String::from("refreshed")).await.unwrap(); + /// assert_eq!(slot.get().map(String::as_str), Some("refreshed")); /// # } /// ``` pub fn take(&mut self) -> Option { diff --git a/asyncband/src/oneshot/mod.rs b/asyncband/src/oneshot/mod.rs index bdafd4f2..fbca62b7 100644 --- a/asyncband/src/oneshot/mod.rs +++ b/asyncband/src/oneshot/mod.rs @@ -21,16 +21,12 @@ // Upstream source (oneshot v0.1.11, before its channel code was split into modules): // https://github.com/faern/oneshot/blob/25274e995ee0a702b3e9e1ac81e577f8c3ce0892/src/lib.rs -//! A one-shot channel is used for sending a single message between asynchronous tasks. The -//! [`channel`] function is used to create a [`Sender`] and [`Receiver`] pair that form the channel. +//! Single-value channel linking one producer to one consumer across async or blocking boundaries. //! -//! The sender is used by the producer to send the value. The receiver is used by the consumer -//! to receive the value. -//! -//! The sender and receiver can be used by separate tasks. -//! -//! Since [`Sender::send`] is not async, it can be used anywhere. This includes sending between -//! two runtimes, and using it from non-async code. +//! Call [`channel`] to create a matched [`Sender`] and [`Receiver`] pair. Sending completes +//! synchronously without awaiting, so values can be handed off from ordinary threads, drop +//! destructors, or tasks running on a different executor while the receiving side awaits +//! completion. //! //! # Examples //! @@ -41,41 +37,40 @@ //! //! let (tx, rx) = oneshot::channel(); //! -//! let sender = tokio::spawn(async move { -//! tx.send(3).unwrap(); +//! let producer = tokio::spawn(async move { +//! tx.send("pong").unwrap(); //! }); //! -//! assert_eq!(rx.await, Ok(3)); -//! sender.await.unwrap(); +//! assert_eq!(rx.await, Ok("pong")); +//! producer.await.unwrap(); //! # } //! ``` //! -//! If the sender is dropped without sending, the receiver will fail with [`RecvError`]: +//! Dropping the [`Sender`] without sending causes awaiting the [`Receiver`] to fail with +//! [`RecvError::Disconnected`]: //! //! ``` //! # #[tokio::main] //! # async fn main() { //! use asyncband::oneshot; //! -//! let (tx, rx) = oneshot::channel::(); -//! +//! let (tx, rx) = oneshot::channel::(); //! drop(tx); //! //! assert_eq!(rx.await, Err(oneshot::RecvError::Disconnected)); //! # } //! ``` //! -//! If the receiver is dropped before receiving, the sender will fail with [`SendError`]: +//! Dropping the [`Receiver`] before a send lets the caller recover its payload from [`SendError`]: //! //! ``` //! use asyncband::oneshot; //! -//! let (tx, rx) = oneshot::channel::(); -//! +//! let (tx, rx) = oneshot::channel::(); //! drop(rx); //! -//! let error = tx.send(42).unwrap_err(); -//! assert_eq!(error.into_inner(), 42); +//! let error = tx.send(99).unwrap_err(); +//! assert_eq!(error.into_inner(), 99); //! ``` //! //! # Cancellation diff --git a/asyncband/src/oneshot/receiver.rs b/asyncband/src/oneshot/receiver.rs index 7df36bbf..643ea4c8 100644 --- a/asyncband/src/oneshot/receiver.rs +++ b/asyncband/src/oneshot/receiver.rs @@ -43,10 +43,10 @@ use crate::oneshot::Sender; use crate::oneshot::deallocate_empty_channel; use crate::oneshot::drop_message_and_deallocate_channel; -/// Receives a value from the associated [`Sender`]. +/// The receiving half of a [`oneshot`](crate::oneshot) channel. /// -/// Awaiting converts this receiver into a [`Recv`] future. Dropping either the receiver or its -/// future disconnects the channel and discards any unread message. +/// Awaiting converts this receiver into a [`Recv`] future linked to the paired [`Sender`]. Dropping +/// either the receiver or its future disconnects the channel and discards any unread message. /// /// To keep a pending operation alive when another branch wins, call [`Receiver::into_future`] /// before selecting and borrow the resulting future as `&mut Recv`. The receiver itself does not @@ -82,12 +82,10 @@ impl IntoFuture for Receiver { } impl Receiver { - /// Returns `true` if the channel is disconnected. + /// Reports whether the channel can never yield a message because the paired [`Sender`] was + /// dropped without sending, or because the message was already extracted. /// - /// This occurs when the associated [`Sender`] is dropped without sending a message, or after - /// the message is received. - /// - /// If `true` is returned, any subsequent attempt to receive the message will return an error. + /// Once this returns `true`, subsequent attempts to receive fail. pub fn is_disconnected(&self) -> bool { // SAFETY: The existence of `self` guarantees that the receiver is still alive. If the // sender was dropped, it observed the live receiver and left allocation cleanup to it, so @@ -198,8 +196,8 @@ impl Drop for Receiver { } } -/// A future that receives a value from the associated [`Sender`], or returns [`RecvError`] if the -/// sender is dropped without sending. +/// Future produced by awaiting a [`Receiver`], resolving to the message from [`Sender`] or +/// [`RecvError`] if the sender dropped without sending. /// /// Created by [`Receiver::into_future`], this future owns the receiving endpoint. Dropping it /// disconnects the channel and discards any unread message. diff --git a/asyncband/src/oneshot/sender.rs b/asyncband/src/oneshot/sender.rs index 74d004bf..f4c7b3aa 100644 --- a/asyncband/src/oneshot/sender.rs +++ b/asyncband/src/oneshot/sender.rs @@ -40,7 +40,7 @@ use crate::oneshot::Receiver; use crate::oneshot::deallocate_empty_channel; use crate::oneshot::drop_message_and_deallocate_channel; -/// Sends a value to the associated [`Receiver`]. +/// The sending half of a [`oneshot`](crate::oneshot) channel. pub struct Sender { channel_ptr: NonNull>, } @@ -55,8 +55,8 @@ unsafe impl Send for Sender {} unsafe impl Sync for Sender {} impl Sender { - /// Attempts to send a value on this channel, returning an error containing the message if it - /// could not be sent. + /// Delivers `message` to the paired [`Receiver`], or returns it in [`SendError`] if the + /// receiver has already been dropped. pub fn send(self, message: T) -> Result<(), SendError> { // `send` takes over endpoint cleanup, so `Sender::drop` must not run afterward. let sender = ManuallyDrop::new(self); @@ -125,12 +125,10 @@ impl Sender { } } - /// Returns `true` if the channel is disconnected. + /// Reports whether the paired [`Receiver`] has already been dropped. /// - /// This occurs when the receiver is dropped. - /// - /// If `true` is returned, a future call to [`send`](Sender::send) is guaranteed to return an - /// error. + /// When this returns `true`, a later call to [`send`](Sender::send) is guaranteed to fail with + /// [`SendError`]. A `false` result does not prevent the receiver from dropping concurrently. pub fn is_disconnected(&self) -> bool { // SAFETY: The channel exists on the heap for the entire duration of this method, and we // only ever acquire shared references to it. Dropping the receiver does not immediately diff --git a/asyncband/src/semaphore/mod.rs b/asyncband/src/semaphore/mod.rs index 75e8c561..bd39f6bd 100644 --- a/asyncband/src/semaphore/mod.rs +++ b/asyncband/src/semaphore/mod.rs @@ -37,17 +37,14 @@ //! # async fn main() { //! use asyncband::semaphore::Semaphore; //! -//! let semaphore = Semaphore::new(3); -//! let a_permit = semaphore.acquire(1).await; -//! let two_permits = semaphore.acquire(2).await; +//! let limits = Semaphore::new(4); +//! let batch = limits.acquire(3).await; +//! assert_eq!(limits.available_permits(), 1); //! -//! assert_eq!(semaphore.available_permits(), 0); +//! assert!(limits.try_acquire(2).is_none()); //! -//! let permit_attempt = semaphore.try_acquire(1); -//! assert!(permit_attempt.is_none()); -//! -//! drop(a_permit); -//! assert_eq!(semaphore.available_permits(), 1); +//! drop(batch); +//! assert_eq!(limits.available_permits(), 4); //! # } //! ``` @@ -168,32 +165,26 @@ impl Semaphore { self.s.release(permits); } - /// Attempts to acquire `permits` permits from the semaphore without blocking. + /// Attempts to claim `permits` immediately without waiting. /// - /// If the permits are successfully acquired, a [`SemaphorePermit`] is returned. - /// The permits will be automatically returned to the semaphore when the permit - /// is dropped, unless [`forget`] is called. + /// Returns [`None`] if fewer than `permits` permits are available. Otherwise returns a + /// [`SemaphorePermit`] whose destructor releases those permits back to the semaphore unless + /// [`SemaphorePermit::forget`] consumes it first. /// /// # Examples /// /// ``` /// use asyncband::semaphore::Semaphore; /// - /// let sem = Semaphore::new(2); + /// let quota = Semaphore::new(3); /// - /// // First acquisition succeeds - /// let permit1 = sem.try_acquire(1).unwrap(); - /// assert_eq!(sem.available_permits(), 1); - /// - /// // Second acquisition succeeds - /// let permit2 = sem.try_acquire(1).unwrap(); - /// assert_eq!(sem.available_permits(), 0); + /// let first = quota.try_acquire(2).unwrap(); + /// assert_eq!(quota.available_permits(), 1); + /// assert!(quota.try_acquire(2).is_none()); /// - /// // Third acquisition fails - /// assert!(sem.try_acquire(1).is_none()); + /// drop(first); + /// assert_eq!(quota.available_permits(), 3); /// ``` - /// - /// [`forget`]: SemaphorePermit::forget pub fn try_acquire(&self, permits: usize) -> Option> { if self.s.try_acquire(permits) { Some(SemaphorePermit { sem: self, permits }) @@ -202,10 +193,10 @@ impl Semaphore { } } - /// Acquires `permits` permits from the semaphore. + /// Waits until `permits` permits are available and returns a guard holding them. /// - /// If the permits are not immediately available, this method will wait until they become - /// available. Returns a [`SemaphorePermit`] that will release the permits when dropped. + /// Dropping the returned [`SemaphorePermit`] returns its permits to this semaphore unless + /// [`SemaphorePermit::forget`] consumes it first. /// /// # Cancel safety /// @@ -220,20 +211,19 @@ impl Semaphore { /// /// use asyncband::semaphore::Semaphore; /// - /// let sem = Arc::new(Semaphore::new(2)); - /// let sem2 = sem.clone(); + /// let quota = Arc::new(Semaphore::new(2)); + /// let worker_quota = quota.clone(); /// - /// let handle = tokio::spawn(async move { - /// let permit = sem2.acquire(1).await; - /// // Do some work with the permit. - /// // Permit is automatically released when dropped. + /// let task = tokio::spawn(async move { + /// let _lease = worker_quota.acquire(1).await; + /// 42 /// }); /// - /// let permit = sem.acquire(1).await; - /// // Do some work with the permit - /// drop(permit); // Explicitly release the permit + /// let lease = quota.acquire(1).await; + /// assert_eq!(lease.permits(), 1); + /// drop(lease); /// - /// handle.await.unwrap(); + /// assert_eq!(task.await.unwrap(), 42); /// # } /// ``` pub async fn acquire(&self, permits: usize) -> SemaphorePermit<'_> { @@ -241,13 +231,11 @@ impl Semaphore { SemaphorePermit { sem: self, permits } } - /// Attempts to acquire `permits` permits from the semaphore without blocking. - /// - /// The semaphore must be wrapped in an [`Arc`] to call this method. + /// Attempts to claim `permits` immediately on an [`Arc`]-shared semaphore without waiting. /// - /// If the permits are successfully acquired, a [`OwnedSemaphorePermit`] is returned. - /// The permits will be automatically returned to the semaphore when the permit - /// is dropped, unless [`forget`] is called. + /// Returns [`None`] if fewer than `permits` permits are available. Otherwise returns an + /// [`OwnedSemaphorePermit`] that keeps the `Arc` alive and releases its permits on drop unless + /// [`OwnedSemaphorePermit::forget`] consumes it first. /// /// # Examples /// @@ -256,19 +244,15 @@ impl Semaphore { /// /// use asyncband::semaphore::Semaphore; /// - /// let sem = Arc::new(Semaphore::new(2)); + /// let quota = Arc::new(Semaphore::new(4)); /// - /// let p1 = sem.clone().try_acquire_owned(1).unwrap(); - /// assert_eq!(sem.available_permits(), 1); + /// let lease = quota.clone().try_acquire_owned(3).unwrap(); + /// assert_eq!(quota.available_permits(), 1); + /// assert!(quota.clone().try_acquire_owned(2).is_none()); /// - /// let p2 = sem.clone().try_acquire_owned(1).unwrap(); - /// assert_eq!(sem.available_permits(), 0); - /// - /// let p3 = sem.try_acquire_owned(1); - /// assert!(p3.is_none()); + /// drop(lease); + /// assert_eq!(quota.available_permits(), 4); /// ``` - /// - /// [`forget`]: OwnedSemaphorePermit::forget pub fn try_acquire_owned(self: Arc, permits: usize) -> Option { if self.s.try_acquire(permits) { Some(OwnedSemaphorePermit { sem: self, permits }) @@ -277,12 +261,11 @@ impl Semaphore { } } - /// Acquires `permits` permits from the semaphore. + /// Waits on an [`Arc`]-shared semaphore until `permits` permits are available and returns an + /// owned guard. /// - /// The semaphore must be wrapped in an [`Arc`] to call this method. - /// - /// If the permits are not immediately available, this method will wait until they become - /// available. Returns a [`OwnedSemaphorePermit`] that will release the permits when dropped. + /// Dropping the returned [`OwnedSemaphorePermit`] releases both the permits and its reference + /// to the semaphore unless [`OwnedSemaphorePermit::forget`] consumes it first. /// /// # Cancel safety /// @@ -297,21 +280,22 @@ impl Semaphore { /// /// use asyncband::semaphore::Semaphore; /// - /// let sem = Arc::new(Semaphore::new(3)); - /// let mut join_handles = vec![]; + /// let quota = Arc::new(Semaphore::new(2)); + /// let mut workers = Vec::new(); /// - /// for _ in 0..5 { - /// let permit = sem.clone().acquire_owned(1).await; - /// join_handles.push(tokio::spawn(async move { - /// // perform task... - /// // explicitly own `permit` in the task - /// drop(permit); + /// for job_id in 0..4usize { + /// let lease = quota.clone().acquire_owned(1).await; + /// workers.push(tokio::spawn(async move { + /// let _lease = lease; + /// job_id * 2 /// })); /// } /// - /// for handle in join_handles { - /// handle.await.unwrap(); + /// let mut results = Vec::new(); + /// for worker in workers { + /// results.push(worker.await.unwrap()); /// } + /// assert_eq!(results, [0, 2, 4, 6]); /// # } /// ``` pub async fn acquire_owned(self: Arc, permits: usize) -> OwnedSemaphorePermit { @@ -320,15 +304,10 @@ impl Semaphore { } } -/// A permit from the semaphore. -/// -/// This type is created by the [`acquire`] and [`try_acquire`] methods on [`Semaphore`]. -/// When the permit is dropped, the permits will be returned to the semaphore unless -/// [`forget`] is called. +/// Borrowed RAII guard representing permits acquired from a [`Semaphore`]. /// -/// [`acquire`]: Semaphore::acquire -/// [`try_acquire`]: Semaphore::try_acquire -/// [`forget`]: SemaphorePermit::forget +/// Created by [`Semaphore::acquire`] and [`Semaphore::try_acquire`]. Dropping this guard returns +/// its permits to the semaphore unless [`SemaphorePermit::forget`] is called first. #[must_use = "permits are released immediately when dropped"] #[derive(Debug)] pub struct SemaphorePermit<'a> { @@ -337,71 +316,59 @@ pub struct SemaphorePermit<'a> { } impl SemaphorePermit<'_> { - /// Forgets the permit **without** releasing it back to the semaphore. + /// Consumes this guard without returning its permits to the semaphore. /// - /// This can be used to permanently reduce the number of permits available - /// from a semaphore. + /// The acquired capacity remains permanently deducted unless explicitly restored later via + /// [`Semaphore::release`]. To deduct permits without first holding a guard, use + /// [`Semaphore::reduce_permits`]. /// /// # Examples /// /// ``` - /// use std::sync::Arc; - /// /// use asyncband::semaphore::Semaphore; /// - /// let sem = Arc::new(Semaphore::new(10)); - /// { - /// let permit = sem.try_acquire(5).unwrap(); - /// assert_eq!(sem.available_permits(), 5); - /// permit.forget(); - /// } + /// let quota = Semaphore::new(6); + /// let held = quota.try_acquire(4).unwrap(); + /// held.forget(); /// - /// // Since we forgot the permit, available permits won't go back to - /// // its initial value even after the permit is dropped - /// assert_eq!(sem.available_permits(), 5); + /// assert_eq!(quota.available_permits(), 2); + /// quota.release(4); + /// assert_eq!(quota.available_permits(), 6); /// ``` pub fn forget(mut self) { self.permits = 0; } - /// Merge two [`SemaphorePermit`] instances together, consuming `other` - /// without releasing the permits it holds. + /// Combines the permits held by `other` into `self`. /// - /// Permits held by both `self` and `other` are released when `self` drops. + /// `other` is consumed without releasing its permits; all combined permits are later released + /// together when `self` drops (or retained permanently if forgotten). /// /// # Panics /// - /// This function panics if permits from different [`Semaphore`] instances are merged or if - /// their combined permit count exceeds `usize::MAX`. + /// Panics if `self` and `other` belong to different [`Semaphore`] instances or if their + /// combined permit count overflows `usize::MAX`. /// /// # Examples /// /// ``` - /// use std::sync::Arc; - /// /// use asyncband::semaphore::Semaphore; /// - /// let sem = Arc::new(Semaphore::new(10)); - /// let mut permit = sem.try_acquire(1).unwrap(); - /// - /// for _ in 0..9 { - /// let new_permit = sem.try_acquire(1).unwrap(); - /// // Merge individual permits into a single one. - /// permit.merge(new_permit) - /// } - /// - /// assert_eq!(sem.available_permits(), 0); + /// let quota = Semaphore::new(8); + /// let mut combined = quota.try_acquire(2).unwrap(); + /// combined.merge(quota.try_acquire(3).unwrap()); /// - /// // Release all permits in a single batch. - /// drop(permit); + /// assert_eq!(combined.permits(), 5); + /// assert_eq!(quota.available_permits(), 3); /// - /// assert_eq!(sem.available_permits(), 10); + /// drop(combined); + /// assert_eq!(quota.available_permits(), 8); /// ``` #[track_caller] pub fn merge(&mut self, mut other: Self) { assert!( ptr::eq(self.sem, other.sem), - "merging permits from different semaphore instances" + "cannot merge permits from different semaphores" ); self.permits = self .permits @@ -410,25 +377,22 @@ impl SemaphorePermit<'_> { other.permits = 0; } - /// Splits `n` permits from `self` and returns a new [`SemaphorePermit`] instance that holds `n` - /// permits. + /// Detaches `n` permits from `self` into a new borrowed guard. /// - /// If there are insufficient permits, and it is impossible to reduce by `n`, returns `None`. + /// Returns [`None`] without modifying `self` when `n` exceeds [`Self::permits`]. /// /// # Examples /// /// ``` - /// use std::sync::Arc; - /// /// use asyncband::semaphore::Semaphore; /// - /// let sem = Arc::new(Semaphore::new(3)); + /// let quota = Semaphore::new(5); + /// let mut whole = quota.try_acquire(5).unwrap(); + /// let part = whole.split(2).unwrap(); /// - /// let mut p1 = sem.try_acquire(3).unwrap(); - /// let p2 = p1.split(1).unwrap(); - /// - /// assert_eq!(p1.permits(), 2); - /// assert_eq!(p2.permits(), 1); + /// assert_eq!(whole.permits(), 3); + /// assert_eq!(part.permits(), 2); + /// assert!(whole.split(4).is_none()); /// ``` pub fn split(&mut self, n: usize) -> Option { if n > self.permits { @@ -443,16 +407,16 @@ impl SemaphorePermit<'_> { }) } - /// Returns the number of permits this permit holds. + /// Returns how many permits this guard currently holds. /// /// # Examples /// /// ``` /// use asyncband::semaphore::Semaphore; /// - /// let sem = Semaphore::new(5); - /// let permit = sem.try_acquire(3).unwrap(); - /// assert_eq!(permit.permits(), 3); + /// let quota = Semaphore::new(7); + /// let guard = quota.try_acquire(4).unwrap(); + /// assert_eq!(guard.permits(), 4); /// ``` pub fn permits(&self) -> usize { self.permits @@ -465,11 +429,11 @@ impl Drop for SemaphorePermit<'_> { } } -/// An owned permit from the semaphore. -/// -/// This type is created by the [`acquire_owned`] method. +/// Owned RAII guard representing permits acquired from an [`Arc`]-shared [`Semaphore`]. /// -/// [`acquire_owned`]: Semaphore::acquire_owned +/// Created by [`Semaphore::acquire_owned`] and [`Semaphore::try_acquire_owned`]. Dropping this +/// guard returns its permits to the semaphore unless [`OwnedSemaphorePermit::forget`] is called +/// first. #[must_use = "permits are released immediately when dropped"] #[derive(Debug)] pub struct OwnedSemaphorePermit { @@ -478,10 +442,11 @@ pub struct OwnedSemaphorePermit { } impl OwnedSemaphorePermit { - /// Forgets the permit **without** releasing it back to the semaphore. + /// Consumes this guard without returning its permits to the semaphore. /// - /// This can be used to permanently reduce the number of permits available - /// from a semaphore. + /// The acquired capacity remains permanently deducted unless explicitly restored later via + /// [`Semaphore::release`]. To deduct permits without first holding a guard, use + /// [`Semaphore::reduce_permits`]. /// /// # Examples /// @@ -490,30 +455,27 @@ impl OwnedSemaphorePermit { /// /// use asyncband::semaphore::Semaphore; /// - /// let sem = Arc::new(Semaphore::new(10)); - /// { - /// let permit = sem.clone().try_acquire_owned(5).unwrap(); - /// assert_eq!(sem.available_permits(), 5); - /// permit.forget(); - /// } + /// let quota = Arc::new(Semaphore::new(6)); + /// let held = quota.clone().try_acquire_owned(4).unwrap(); + /// held.forget(); /// - /// // Since we forgot the permit, available permits won't go back to - /// // its initial value even after the permit is dropped - /// assert_eq!(sem.available_permits(), 5); + /// assert_eq!(quota.available_permits(), 2); + /// quota.release(4); + /// assert_eq!(quota.available_permits(), 6); /// ``` pub fn forget(mut self) { self.permits = 0; } - /// Merge two [`OwnedSemaphorePermit`] instances together, consuming `other` - /// without releasing the permits it holds. + /// Combines the permits held by `other` into `self`. /// - /// Permits held by both `self` and `other` are released when `self` drops. + /// `other` is consumed without releasing its permits; all combined permits are later released + /// together when `self` drops (or retained permanently if forgotten). /// /// # Panics /// - /// This function panics if permits from different [`Semaphore`] instances are merged or if - /// their combined permit count exceeds `usize::MAX`. + /// Panics if `self` and `other` belong to different [`Semaphore`] instances or if their + /// combined permit count overflows `usize::MAX`. /// /// # Examples /// @@ -522,27 +484,21 @@ impl OwnedSemaphorePermit { /// /// use asyncband::semaphore::Semaphore; /// - /// let sem = Arc::new(Semaphore::new(10)); - /// let mut permit = sem.clone().try_acquire_owned(1).unwrap(); - /// - /// for _ in 0..9 { - /// let new_permit = sem.clone().try_acquire_owned(1).unwrap(); - /// // Merge individual permits into a single one. - /// permit.merge(new_permit) - /// } - /// - /// assert_eq!(sem.available_permits(), 0); + /// let quota = Arc::new(Semaphore::new(8)); + /// let mut combined = quota.clone().try_acquire_owned(2).unwrap(); + /// combined.merge(quota.clone().try_acquire_owned(3).unwrap()); /// - /// // Release all permits in a single batch. - /// drop(permit); + /// assert_eq!(combined.permits(), 5); + /// assert_eq!(quota.available_permits(), 3); /// - /// assert_eq!(sem.available_permits(), 10); + /// drop(combined); + /// assert_eq!(quota.available_permits(), 8); /// ``` #[track_caller] pub fn merge(&mut self, mut other: Self) { assert!( Arc::ptr_eq(&self.sem, &other.sem), - "merging permits from different semaphore instances" + "cannot merge permits from different semaphores" ); self.permits = self .permits @@ -551,14 +507,9 @@ impl OwnedSemaphorePermit { other.permits = 0; } - /// Splits `n` permits from `self` and returns a new [`OwnedSemaphorePermit`] instance that - /// holds `n` permits. - /// - /// If there are insufficient permits, and it is impossible to reduce by `n`, returns `None`. + /// Detaches `n` permits from `self` into a new owned guard, cloning the underlying [`Arc`]. /// - /// # Note - /// - /// It will clone the owned `Arc` to construct the new instance. + /// Returns [`None`] without modifying `self` when `n` exceeds [`Self::permits`]. /// /// # Examples /// @@ -567,13 +518,13 @@ impl OwnedSemaphorePermit { /// /// use asyncband::semaphore::Semaphore; /// - /// let sem = Arc::new(Semaphore::new(3)); - /// - /// let mut p1 = sem.try_acquire_owned(3).unwrap(); - /// let p2 = p1.split(1).unwrap(); + /// let quota = Arc::new(Semaphore::new(5)); + /// let mut whole = quota.try_acquire_owned(5).unwrap(); + /// let part = whole.split(2).unwrap(); /// - /// assert_eq!(p1.permits(), 2); - /// assert_eq!(p2.permits(), 1); + /// assert_eq!(whole.permits(), 3); + /// assert_eq!(part.permits(), 2); + /// assert!(whole.split(4).is_none()); /// ``` pub fn split(&mut self, n: usize) -> Option { if n > self.permits { @@ -588,7 +539,7 @@ impl OwnedSemaphorePermit { }) } - /// Returns the number of permits this permit holds. + /// Returns how many permits this guard currently holds. /// /// # Examples /// @@ -597,9 +548,9 @@ impl OwnedSemaphorePermit { /// /// use asyncband::semaphore::Semaphore; /// - /// let sem = Arc::new(Semaphore::new(5)); - /// let permit = sem.try_acquire_owned(3).unwrap(); - /// assert_eq!(permit.permits(), 3); + /// let quota = Arc::new(Semaphore::new(7)); + /// let guard = quota.try_acquire_owned(4).unwrap(); + /// assert_eq!(guard.permits(), 4); /// ``` pub fn permits(&self) -> usize { self.permits diff --git a/tests-integration/tests/semaphore_test.rs b/tests-integration/tests/semaphore_test.rs index 8f6937dc..2af3bd48 100644 --- a/tests-integration/tests/semaphore_test.rs +++ b/tests-integration/tests/semaphore_test.rs @@ -35,16 +35,20 @@ use tests_integration::expect_ready; use tests_integration::poll_with; #[test] -fn try_acquire() { - let sem = Semaphore::new(1); - { - let p1 = sem.try_acquire(1); - assert!(p1.is_some()); - let p2 = sem.try_acquire(1); - assert!(p2.is_none()); - } - let p3 = sem.try_acquire(1); - assert!(p3.is_some()); +fn try_acquire_tracks_held_and_dropped_permits() { + let semaphore = Semaphore::new(2); + let first = semaphore.try_acquire(1).unwrap(); + assert_eq!(semaphore.available_permits(), 1); + assert!(semaphore.try_acquire(2).is_none()); + + let second = semaphore.try_acquire(1).unwrap(); + assert_eq!(semaphore.available_permits(), 0); + assert!(semaphore.try_acquire(1).is_none()); + + drop(first); + assert_eq!(semaphore.available_permits(), 1); + drop(second); + assert_eq!(semaphore.available_permits(), 2); } #[test] @@ -64,16 +68,17 @@ fn released_permit_wakes_a_pending_acquire() { } #[test] -fn forget() { - let sem = Arc::new(Semaphore::new(1)); - { - let p = sem.try_acquire(1).unwrap(); - assert_eq!(sem.available_permits(), 0); - p.forget(); - assert_eq!(sem.available_permits(), 0); - } - assert_eq!(sem.available_permits(), 0); - assert!(sem.try_acquire(1).is_none()); +fn forgetting_a_permit_permanently_consumes_capacity() { + let semaphore = Semaphore::new(2); + let held = semaphore.try_acquire(2).unwrap(); + assert_eq!(semaphore.available_permits(), 0); + + held.forget(); + assert_eq!(semaphore.available_permits(), 0); + assert!(semaphore.try_acquire(1).is_none()); + + semaphore.release(1); + assert_eq!(semaphore.available_permits(), 1); } #[test] @@ -135,40 +140,40 @@ fn merge_overflow_panics_without_losing_owned_permits() { } #[test] -fn acquire_then_drop() { - let waker = Waker::noop(); - let mut context = Context::from_waker(waker); +fn dropping_pending_acquire_preserves_subsequent_releases() { + let semaphore = Semaphore::new(1); + let held = semaphore.try_acquire(1).unwrap(); + let (waker, wakes) = WakeCounter::new(); - let s = Semaphore::new(1); - let p1 = s.try_acquire(1).unwrap(); { - let p2 = s.acquire(1); - let poll = pin!(p2).poll(&mut context); - assert!(poll.is_pending()); + let mut waiter = pin!(semaphore.acquire(1)); + assert!(poll_with(waiter.as_mut(), &waker).is_pending()); } - drop(p1); - assert_eq!(s.available_permits(), 1); + + drop(held); + assert_eq!(wakes.count(), 0); + assert_eq!(semaphore.available_permits(), 1); } #[test] -fn wake_then_drop() { - let waker = Waker::noop(); - let mut context = Context::from_waker(waker); +fn dropping_notified_waiters_returns_assigned_permits() { + let semaphore = Semaphore::new(2); + let held = semaphore.try_acquire(2).unwrap(); + let (waker1, wakes1) = WakeCounter::new(); + let (waker2, wakes2) = WakeCounter::new(); - let s = Semaphore::new(2); - let p1 = s.try_acquire(2).unwrap(); - { - let p2 = s.acquire(1); - let p2 = pin!(p2); - assert!(p2.poll(&mut context).is_pending()); - { - let p3 = s.acquire(1); - let p3 = pin!(p3); - assert!(p3.poll(&mut context).is_pending()); - drop(p1); - } - } - assert_eq!(s.available_permits(), 2); + let mut first = Box::pin(semaphore.acquire(1)); + let mut second = Box::pin(semaphore.acquire(1)); + assert!(poll_with(first.as_mut(), &waker1).is_pending()); + assert!(poll_with(second.as_mut(), &waker2).is_pending()); + + drop(held); + assert_eq!(wakes1.count(), 1); + assert_eq!(wakes2.count(), 1); + + drop(second); + drop(first); + assert_eq!(semaphore.available_permits(), 2); } #[test] From 118d2e0fc6e57af7f125430560783b400f76178d Mon Sep 17 00:00:00 2001 From: tison Date: Fri, 13 Nov 2026 13:33:27 +0800 Subject: [PATCH 3/4] docs: standardize primitive API contracts --- AGENTS.md | 8 ++ asyncband/src/barrier/mod.rs | 7 +- asyncband/src/blocking/mod.rs | 16 ++- asyncband/src/broadcast/mpmc/bounded/mod.rs | 65 +++++----- asyncband/src/broadcast/mpmc/error.rs | 2 +- asyncband/src/broadcast/mpmc/unbounded/mod.rs | 43 ++++-- asyncband/src/completion/mod.rs | 17 ++- asyncband/src/condvar/mod.rs | 53 +++++--- asyncband/src/event/auto_reset.rs | 10 +- asyncband/src/event/manual_reset.rs | 10 +- asyncband/src/latch/mod.rs | 25 ++-- asyncband/src/mpmc/bounded.rs | 62 ++++++--- asyncband/src/mpmc/error.rs | 4 +- asyncband/src/mpmc/queue.rs | 8 +- asyncband/src/mpmc/unbounded.rs | 31 +++-- asyncband/src/mpsc/bounded/receiver.rs | 26 ++-- asyncband/src/mpsc/bounded/sender.rs | 48 ++++--- asyncband/src/mpsc/error.rs | 4 +- asyncband/src/mpsc/unbounded/receiver.rs | 26 ++-- asyncband/src/mpsc/unbounded/sender.rs | 10 +- asyncband/src/mutex/mod.rs | 75 ++++++++++- asyncband/src/once/lazy_cell/mod.rs | 101 ++++++++++----- asyncband/src/once/once/mod.rs | 35 +++-- asyncband/src/once/once_cell/mod.rs | 122 ++++++++++++++---- asyncband/src/once/once_map/mod.rs | 59 ++++++--- asyncband/src/oneshot/receiver.rs | 53 +++++--- asyncband/src/oneshot/sender.rs | 9 +- asyncband/src/phaser/mod.rs | 42 ++++-- asyncband/src/pool/bounded.rs | 39 ++++-- asyncband/src/pool/common.rs | 27 +++- asyncband/src/pool/unbounded.rs | 33 ++++- asyncband/src/rwlock/mapped_read_guard.rs | 21 ++- asyncband/src/rwlock/mapped_write_guard.rs | 23 +++- .../src/rwlock/owned_mapped_read_guard.rs | 21 ++- .../src/rwlock/owned_mapped_write_guard.rs | 23 +++- asyncband/src/rwlock/owned_read_guard.rs | 25 +++- asyncband/src/rwlock/owned_write_guard.rs | 27 +++- asyncband/src/rwlock/read_guard.rs | 25 +++- asyncband/src/rwlock/upgradable.rs | 9 +- asyncband/src/rwlock/write_guard.rs | 27 +++- asyncband/src/semaphore/mod.rs | 12 +- asyncband/src/shutdown/mod.rs | 43 ++++-- asyncband/src/singleflight/mod.rs | 57 ++++---- asyncband/src/spmc/bounded.rs | 37 ++++-- asyncband/src/spmc/error.rs | 4 +- asyncband/src/spmc/unbounded.rs | 30 +++-- asyncband/src/waitgroup/mod.rs | 22 +++- asyncband/src/watch/mod.rs | 73 ++++++++--- 48 files changed, 1119 insertions(+), 430 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 0d9cccba..86fc6020 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -50,6 +50,14 @@ Decision: [#257](https://github.com/apache/asyncband/pull/257). Keep each Markdown prose paragraph and list item on one source line. Format Markdown tables so their columns and separators align in the source. Keep skill resource links within the skill directory; reference other skills or agents by name and resolve repository paths from the supplied repository root. +Write public API documentation from Asyncband's observable contracts. Start with a short action or query summary, then describe waiting conditions, ordering, ownership, and state changes relevant to callers. Keep implementation details out unless callers need them to use the API correctly. + +Use applicable sections in this order: `Returns`, `Errors`, `Cancel safety`, `Panics`, `Deadlocks`, and `Examples`. Omit empty or redundant sections. Explain simple return values in the summary or body; reserve `Returns` for nontrivial outcomes, snapshots, or resource ownership. Document `Option::None` and boolean outcomes as return semantics, not errors. For `Result`, use `Errors` to explain failure conditions, returned ownership, and any state changes. Keep examples last and focused on useful behavior rather than mechanically demonstrating syntax. + +Document cancellation for public async operations, methods returning futures, and public `Future` or `IntoFuture` types. Explain when effects occur, what dropping a pending future retains or releases, and whether retrying restarts, resumes, or repeats an effect. Distinguish cancellation of active work from cancellation of an observer, and state when caller-owned inputs are dropped or external side effects are not rolled back. Do not use an unqualified cancellation-safety claim in place of these observable guarantees. Owned variants should include a `Cancel safety` section and may link to shared semantics while explaining ownership differences. + +Put concrete panic conditions and important unwind effects in `Panics`, and recursive acquisition or callback reentrancy hazards in `Deadlocks`. Do not enumerate arbitrary panics in caller-provided code unless their effect on the primitive matters. Apply the same conventions to public trait methods. Preserve existing provenance and license notices independently of documentation rewrites. + ## Changelog - Update `CHANGELOG.md` for significant user-visible changes by comparing the final behavior with the latest release tag, not by recording the sequence of commits in the current development cycle. diff --git a/asyncband/src/barrier/mod.rs b/asyncband/src/barrier/mod.rs index c82e856b..889ec4d5 100644 --- a/asyncband/src/barrier/mod.rs +++ b/asyncband/src/barrier/mod.rs @@ -155,6 +155,11 @@ impl Barrier { /// The barrier holds the current task until all `n` participants have arrived. The final /// participant is designated as the leader for this generation. /// + /// # Returns + /// + /// Returns a [`BarrierWaitResult`] identifying whether this call completed the generation + /// and was selected as its leader. + /// /// # Cancel safety /// /// This method is not cancellation safe. @@ -169,8 +174,6 @@ impl Barrier { /// wait futures. Callers that need to keep waiting after another operation completes first /// should retain and continue polling the same `wait` future instead of creating a new one. /// - /// Returns a [`BarrierWaitResult`] that identifies whether this participant is the leader. - /// /// # Examples /// /// ``` diff --git a/asyncband/src/blocking/mod.rs b/asyncband/src/blocking/mod.rs index 95cd671d..50363ec9 100644 --- a/asyncband/src/blocking/mod.rs +++ b/asyncband/src/blocking/mod.rs @@ -62,7 +62,7 @@ use std::time::Duration; /// Extension methods for waiting on a future from synchronous code. /// -/// # Example +/// # Examples /// /// ``` /// use std::time::Duration; @@ -87,12 +87,22 @@ pub trait FutureExt: IntoFuture { /// Blocks the current thread until this future is ready or `timeout` elapses. /// /// The future is polled before checking the timeout, so an immediately ready future succeeds - /// even when `timeout` is zero. Returns [`None`] on timeout and drops the future, cancelling it - /// according to that future's cancellation semantics. + /// even when `timeout` is zero. /// /// The timeout cannot interrupt a call to [`Future::poll`]. See the /// [`blocking`](crate::blocking) module documentation for other runtime limitations and /// executor-thread caveats. + /// + /// # Returns + /// + /// Returns `Some(output)` if the future completes, or `None` when the timeout is observed + /// while the future is pending. + /// + /// # Cancel safety + /// + /// On timeout, the supplied future is dropped. Its own cancellation contract determines which + /// effects remain. Passing a mutable reference to a retained future allows that future to be + /// polled again after the timeout. fn wait_timeout(self, timeout: Duration) -> Option where Self: Sized, diff --git a/asyncband/src/broadcast/mpmc/bounded/mod.rs b/asyncband/src/broadcast/mpmc/bounded/mod.rs index a9c7d54a..d0d33701 100644 --- a/asyncband/src/broadcast/mpmc/bounded/mod.rs +++ b/asyncband/src/broadcast/mpmc/bounded/mod.rs @@ -268,11 +268,8 @@ impl BoundedSender { /// /// # Cancel safety /// - /// This method is cancel safe in the sense that matters for a lossless log: the value is - /// either published to every active receiver or not published at all. Publication happens in - /// one indivisible step, so cancelling a `send` future cannot leave a reserved but unfilled - /// position in the committed order. Dropping the future before publication drops the unsent - /// value. + /// Dropping a pending future does not publish the value and drops the unsent input. Publication + /// is indivisible: it cannot leave a reserved but unfilled position in the message order. /// /// # Panics /// @@ -363,17 +360,18 @@ impl BoundedSender { poll_fn(|cx| send.poll_send(cx)).await } - /// Attempts to broadcast a value to all active receivers without waiting. + /// Broadcasts a value to all active receivers without waiting for capacity. /// - /// # Returns + /// If no receivers are active, the value is discarded and this returns `Ok(())`. /// - /// * `Ok(())`: The value was published, or discarded because no receivers are active. - /// * `Err(TrySendError::Full(value))`: The channel already retains `capacity` messages. The - /// value was not published and is returned unchanged. + /// # Errors + /// + /// Returns [`TrySendError::Full`] containing the unsent value if the channel has no capacity. + /// The value has not been published. /// /// # Panics /// - /// Panics if the internal message version counter overflows. + /// Panics if the channel's message version counter would overflow. /// /// # Examples /// @@ -549,11 +547,10 @@ impl BoundedReceiver { /// caller that keeps it keeps the payload alive but holds no capacity. Use /// [`recv_cloned`](BoundedReceiver::recv_cloned) to take an owned value instead. /// - /// # Returns + /// # Errors /// - /// * `Ok(Arc)`: The next message. - /// * `Err(RecvError::Disconnected)`: All senders have been dropped and this receiver has no - /// remaining messages. + /// Returns [`RecvError::Disconnected`] once all senders have been dropped and this + /// receiver has consumed its remaining messages. /// /// # Cancel safety /// @@ -581,14 +578,12 @@ impl BoundedReceiver { /// /// The payload is moved out of the channel without cloning when this read reclaims the message /// and nothing else still refers to it, such as an `Arc` kept from a shared receive or another - /// receiver's clone still in progress. Otherwise it is cloned with the channel unlocked. If - /// `T::clone` panics, this receiver has still advanced past the value. + /// receiver's clone still in progress. Otherwise it is cloned with the channel unlocked. /// - /// # Returns + /// # Errors /// - /// * `Ok(T)`: The next message. - /// * `Err(RecvError::Disconnected)`: All senders have been dropped and this receiver has no - /// remaining messages. + /// Returns [`RecvError::Disconnected`] once all senders have been dropped and this + /// receiver has consumed its remaining messages. /// /// # Cancel safety /// @@ -596,6 +591,11 @@ impl BoundedReceiver { /// subsequent receive can still return the same next value, so these futures may safely be /// raced with other futures in a selection construct. /// + /// # Panics + /// + /// If `T::clone` panics, this receiver has already advanced past the message. Retrying + /// receives a later message rather than the one whose clone panicked. + /// /// # Examples /// /// ``` @@ -622,12 +622,11 @@ impl BoundedReceiver { /// See [`recv`](BoundedReceiver::recv) for what keeping the `Arc` means, and /// [`try_recv_cloned`](BoundedReceiver::try_recv_cloned) to take an owned value instead. /// - /// # Returns + /// # Errors /// - /// * `Ok(Arc)`: The next message. - /// * `Err(TryRecvError::Empty)`: No message is currently available. - /// * `Err(TryRecvError::Disconnected)`: All senders have been dropped and this receiver has no - /// remaining messages. + /// Returns [`TryRecvError::Empty`] when no message is available but a sender remains. + /// Returns [`TryRecvError::Disconnected`] once all senders have been dropped and this + /// receiver has consumed its remaining messages. /// /// # Examples /// @@ -647,12 +646,16 @@ impl BoundedReceiver { /// See [`recv_cloned`](BoundedReceiver::recv_cloned) for when the payload is moved out rather /// than cloned. /// - /// # Returns + /// # Errors + /// + /// Returns [`TryRecvError::Empty`] when no message is available but a sender remains. + /// Returns [`TryRecvError::Disconnected`] once all senders have been dropped and this + /// receiver has consumed its remaining messages. + /// + /// # Panics /// - /// * `Ok(T)`: The next message. - /// * `Err(TryRecvError::Empty)`: No message is currently available. - /// * `Err(TryRecvError::Disconnected)`: All senders have been dropped and this receiver has no - /// remaining messages. + /// If `T::clone` panics, this receiver has already advanced past the message. Retrying + /// receives a later message rather than the one whose clone panicked. /// /// # Examples /// diff --git a/asyncband/src/broadcast/mpmc/error.rs b/asyncband/src/broadcast/mpmc/error.rs index 28f21726..73221b95 100644 --- a/asyncband/src/broadcast/mpmc/error.rs +++ b/asyncband/src/broadcast/mpmc/error.rs @@ -34,7 +34,7 @@ pub enum TrySendError { } impl TrySendError { - /// Gets a reference to the message that failed to be sent. + /// Returns a reference to the message that failed to be sent. pub fn as_inner(&self) -> &T { match self { TrySendError::Full(msg) => msg, diff --git a/asyncband/src/broadcast/mpmc/unbounded/mod.rs b/asyncband/src/broadcast/mpmc/unbounded/mod.rs index 6019e406..3593cb60 100644 --- a/asyncband/src/broadcast/mpmc/unbounded/mod.rs +++ b/asyncband/src/broadcast/mpmc/unbounded/mod.rs @@ -267,9 +267,10 @@ impl UnboundedReceiver { /// caller that keeps it keeps the payload alive. Use /// [`recv_cloned`](UnboundedReceiver::recv_cloned) to take an owned value instead. /// - /// Values already published for this receiver are returned before disconnection. - /// [`RecvError::Disconnected`] is returned only when no sender remains and this receiver's - /// backlog is empty. + /// # Errors + /// + /// Returns [`RecvError::Disconnected`] once all senders have been dropped and this + /// receiver has consumed its remaining messages. /// /// # Cancel safety /// @@ -301,12 +302,12 @@ impl UnboundedReceiver { /// /// The payload is moved out of the channel without cloning when this read reclaims the message /// and nothing else still refers to it, such as an `Arc` kept from a shared receive or another - /// receiver's clone still in progress. Otherwise it is cloned with the channel unlocked. If - /// `T::clone` panics, this receiver has still advanced past the value. + /// receiver's clone still in progress. Otherwise it is cloned with the channel unlocked. + /// + /// # Errors /// - /// Values already published for this receiver are returned before disconnection. - /// [`RecvError::Disconnected`] is returned only when no sender remains and this receiver's - /// backlog is empty. + /// Returns [`RecvError::Disconnected`] once all senders have been dropped and this + /// receiver has consumed its remaining messages. /// /// # Cancel safety /// @@ -314,6 +315,11 @@ impl UnboundedReceiver { /// subsequent receive can still return the same next value, so these futures may safely be /// raced with other futures in a selection construct. /// + /// # Panics + /// + /// If `T::clone` panics, this receiver has already advanced past the message. Retrying + /// receives a later message rather than the one whose clone panicked. + /// /// # Examples /// /// ``` @@ -340,9 +346,11 @@ impl UnboundedReceiver { /// See [`recv`](UnboundedReceiver::recv) for what keeping the `Arc` means, and /// [`try_recv_cloned`](UnboundedReceiver::try_recv_cloned) to take an owned value instead. /// - /// [`TryRecvError::Empty`] means this receiver is currently caught up while a sender remains. - /// [`TryRecvError::Disconnected`] means no sender remains and this receiver has drained its - /// backlog. + /// # Errors + /// + /// Returns [`TryRecvError::Empty`] when no message is available but a sender remains. + /// Returns [`TryRecvError::Disconnected`] once all senders have been dropped and this + /// receiver has consumed its remaining messages. /// /// # Examples /// @@ -369,9 +377,16 @@ impl UnboundedReceiver { /// See [`recv_cloned`](UnboundedReceiver::recv_cloned) for when the payload is moved out /// rather than cloned. /// - /// [`TryRecvError::Empty`] means this receiver is currently caught up while a sender remains. - /// [`TryRecvError::Disconnected`] means no sender remains and this receiver has drained its - /// backlog. + /// # Errors + /// + /// Returns [`TryRecvError::Empty`] when no message is available but a sender remains. + /// Returns [`TryRecvError::Disconnected`] once all senders have been dropped and this + /// receiver has consumed its remaining messages. + /// + /// # Panics + /// + /// If `T::clone` panics, this receiver has already advanced past the message. Retrying + /// receives a later message rather than the one whose clone panicked. /// /// # Examples /// diff --git a/asyncband/src/completion/mod.rs b/asyncband/src/completion/mod.rs index 923e80be..560da756 100644 --- a/asyncband/src/completion/mod.rs +++ b/asyncband/src/completion/mod.rs @@ -118,10 +118,13 @@ impl fmt::Debug for Completer { } impl Completer { - /// Completes the primitive with `value` and wakes all pending observers. + /// Publishes `value` and wakes all pending observers. /// - /// Returns `value` if all observers were already dropped. A successful completion does not - /// guarantee that an observer will remain alive long enough to read the value. + /// Success does not guarantee that an observer remains alive long enough to read the value. + /// + /// # Errors + /// + /// Returns `Err(value)` if all observers have already been dropped. pub fn complete(mut self, value: T) -> Result<(), T> { let Some(shared) = self.shared.upgrade() else { return Err(value); @@ -178,11 +181,15 @@ impl fmt::Debug for Completion { impl Completion { /// Waits for the shared value and returns a reference to it. /// + /// # Errors + /// /// Returns [`Abandoned`] if the completer is dropped before providing a value. Abandonment /// remains distinct from any error stored inside `T`. /// - /// This method is cancel safe. Dropping one pending wait unregisters only that call and does - /// not affect this observer, another wait, or the eventual result. + /// # Cancel safety + /// + /// Dropping a pending future unregisters only this wait. It does not affect this observer, + /// other waiters, or the eventual result. pub async fn wait(&self) -> Result<&T, Abandoned> { Wait { shared: &self.shared, diff --git a/asyncband/src/condvar/mod.rs b/asyncband/src/condvar/mod.rs index 796c794e..cfe6e13c 100644 --- a/asyncband/src/condvar/mod.rs +++ b/asyncband/src/condvar/mod.rs @@ -172,8 +172,9 @@ impl Condvar { /// /// # Cancel safety /// - /// Cancelling this wait removes it from the current waiters. If the task was selected by - /// [`notify_one`](Self::notify_one) but has not yet reacquired the mutex, the notification is + /// Dropping the future releases any guard it owns and removes this wait; the original guard + /// is not returned to the caller. If the task was selected by [`notify_one`](Self::notify_one) + /// but has not yet reacquired the mutex, the notification is /// passed to another task that is waiting at that point, if one exists. It is never buffered /// for a future waiter. pub async fn wait<'a, T: ?Sized>(&self, guard: MutexGuard<'a, T>) -> MutexGuard<'a, T> { @@ -191,10 +192,16 @@ impl Condvar { guard } - /// Waits for a notification, atomically releasing and then reacquiring the owned mutex. + /// Waits for a notification, releasing and then reacquiring the owned mutex. /// - /// This has the same notification and cancellation semantics as [`wait`](Self::wait), but - /// accepts and returns an owned guard. + /// The wait is registered before the mutex is released. The returned owned guard holds the + /// mutex again; the caller must recheck its predicate as with [`wait`](Self::wait). + /// + /// # Cancel safety + /// + /// Dropping the future releases any guard it owns without returning it to the caller. + /// Notifications are transferred to another current waiter under the same conditions as + /// [`wait`](Self::wait); they are never saved for a future waiter. pub async fn wait_owned(&self, guard: OwnedMutexGuard) -> OwnedMutexGuard { let mutex = mutex::owned_guard_lock(&guard); let notify_one_baton = Wait { @@ -216,6 +223,18 @@ impl Condvar { /// `condition` is checked under the lock before waiting and after every wakeup; if it is /// already `false` on entry, this method returns immediately without waiting. /// + /// # Cancel safety + /// + /// Each wait iteration has the same cancellation semantics as [`wait`](Self::wait). Cancelling + /// drops the mutex guard; mutations already made by `condition` are not rolled back. + /// + /// # Panics + /// + /// If `condition` panics, unwinding drops the guard and releases the mutex. Changes already + /// made by the predicate are not rolled back. + /// + /// # Examples + /// /// ``` /// # #[tokio::main] /// # async fn main() { @@ -243,11 +262,6 @@ impl Condvar { /// notifier.await.unwrap(); /// # } /// ``` - /// - /// # Cancel safety - /// - /// Each wait iteration has the same cancellation semantics as [`wait`](Self::wait). Cancelling - /// drops the mutex guard; mutations already made by `condition` are not rolled back. pub async fn wait_while<'a, T: ?Sized, F>( &self, mut guard: MutexGuard<'a, T>, @@ -268,6 +282,19 @@ impl Condvar { /// `condition` is checked under the lock before waiting and after every wakeup; if it is /// already `false` on entry, this method returns immediately without waiting. /// + /// # Cancel safety + /// + /// Each wait iteration has the same cancellation semantics as + /// [`wait_owned`](Self::wait_owned). Cancelling drops the owned mutex guard; mutations already + /// made by `condition` are not rolled back. + /// + /// # Panics + /// + /// If `condition` panics, unwinding drops the guard and releases the mutex. Changes already + /// made by the predicate are not rolled back. + /// + /// # Examples + /// /// ``` /// # #[tokio::main] /// # async fn main() { @@ -295,12 +322,6 @@ impl Condvar { /// notifier.await.unwrap(); /// # } /// ``` - /// - /// # Cancel safety - /// - /// Each wait iteration has the same cancellation semantics as - /// [`wait_owned`](Self::wait_owned). Cancelling drops the owned mutex guard; mutations already - /// made by `condition` are not rolled back. pub async fn wait_while_owned( &self, mut guard: OwnedMutexGuard, diff --git a/asyncband/src/event/auto_reset.rs b/asyncband/src/event/auto_reset.rs index 9e6107f5..2cb98ac0 100644 --- a/asyncband/src/event/auto_reset.rs +++ b/asyncband/src/event/auto_reset.rs @@ -177,10 +177,14 @@ impl AutoResetEvent { .await } - /// Waits without borrowing the event. + /// Waits for and consumes one signal while owning this [`Arc`]. /// - /// The future owns the [`Arc`], making it suitable for spawned tasks. Its waiting and - /// cancellation semantics match [`wait`](Self::wait). + /// The future keeps the event alive without borrowing the caller's handle. + /// + /// # Cancel safety + /// + /// Cancellation has the same signal semantics as [`wait`](Self::wait) and releases the + /// future's `Arc`. /// /// # Examples /// diff --git a/asyncband/src/event/manual_reset.rs b/asyncband/src/event/manual_reset.rs index f272fc44..44ab7db6 100644 --- a/asyncband/src/event/manual_reset.rs +++ b/asyncband/src/event/manual_reset.rs @@ -186,10 +186,14 @@ impl ManualResetEvent { .await } - /// Waits without borrowing the event. + /// Waits until the event is set while owning this [`Arc`]. /// - /// The future owns the [`Arc`], making it suitable for spawned tasks. Its waiting and - /// cancellation semantics match [`wait`](Self::wait). + /// The future keeps the event alive without borrowing the caller's handle. + /// + /// # Cancel safety + /// + /// Cancellation has the same signal semantics as [`wait`](Self::wait) and releases the + /// future's `Arc`. /// /// # Examples /// diff --git a/asyncband/src/latch/mod.rs b/asyncband/src/latch/mod.rs index feae9c16..ac2eae14 100644 --- a/asyncband/src/latch/mod.rs +++ b/asyncband/src/latch/mod.rs @@ -136,9 +136,12 @@ impl Latch { } } - /// Attempts to wait for the latch count to reach zero without blocking. + /// Checks whether the countdown has reached zero without waiting. /// - /// Returns `Ok(())` if the latch is complete, or `Err(count)` with the current nonzero count. + /// # Errors + /// + /// Returns `Err(count)` with the observed nonzero count if the latch is not complete. The + /// count may decrease immediately after this observation. /// /// # Examples /// @@ -156,10 +159,12 @@ impl Latch { self.state.try_wait() } - /// Returns a future that will complete when the latch count reaches zero. + /// Waits until the countdown reaches zero. + /// + /// # Cancel safety /// - /// This method is cancel safe. Dropping a pending wait does not change the countdown or affect - /// other waiters. + /// Dropping a pending future unregisters only this wait. It does not change the countdown or + /// affect other waiters. /// /// # Examples /// @@ -181,10 +186,14 @@ impl Latch { fut.await } - /// Returns a future that will complete when the latch count reaches zero. + /// Waits until the countdown reaches zero while owning this [`Arc`]. + /// + /// The future keeps the latch alive without borrowing the caller's handle. + /// + /// # Cancel safety /// - /// The latch must be wrapped in an [`Arc`] to call this method. The future owns that `Arc`, so - /// it can be moved into a spawned task. Like [`wait`](Self::wait), this method is cancel safe. + /// Dropping a pending future unregisters only this wait and releases its `Arc`. It does not + /// change the countdown or affect other waiters. /// /// # Examples /// diff --git a/asyncband/src/mpmc/bounded.rs b/asyncband/src/mpmc/bounded.rs index 5964e22e..b9557359 100644 --- a/asyncband/src/mpmc/bounded.rs +++ b/asyncband/src/mpmc/bounded.rs @@ -78,9 +78,11 @@ impl Drop for BoundedSender { } impl BoundedSender { - /// Sends a value, waiting until capacity is available if the queue is full. + /// Sends a value, waiting for capacity when the queue is full. /// - /// If all receivers have been dropped, the value is returned in [`SendError`]. + /// # Errors + /// + /// Returns [`SendError`] containing the unsent value if all receivers have been dropped. /// /// # Cancel safety /// @@ -91,12 +93,16 @@ impl BoundedSender { self.shared.send(value).await } - /// Reserves capacity for one value before constructing it. + /// Reserves capacity for one message before the caller supplies its value. + /// + /// The returned [`Permit`] holds one capacity slot. Dropping it releases that slot; + /// [`Permit::send`] publishes a value without waiting for space. Holding a permit does not + /// establish message order or keep a receiver alive. /// - /// A successful reservation returns a [`Permit`]. Dropping it releases capacity. A permit - /// reserves space, not message order, and does not keep receivers alive. + /// # Errors /// - /// Returns `SendError(())` if all receivers have been dropped. + /// Returns `SendError(())` if all receivers have been dropped. A successful reservation + /// can still fail to send if disconnection happens before [`Permit::send`]. /// /// # Cancel safety /// @@ -124,18 +130,27 @@ impl BoundedSender { self.shared.reserve().await } - /// Reserves capacity for one value without waiting. + /// Reserves capacity for one message without waiting. + /// + /// Dropping the returned [`Permit`] releases its capacity. See [`reserve`](Self::reserve) + /// for reservation and message ordering. + /// + /// # Errors /// - /// Returns [`TrySendError::Full`] when all capacity belongs to queued values, held permits, - /// or granted waiters, and [`TrySendError::Disconnected`] when all receivers are gone. + /// Returns [`TrySendError::Full`] if capacity is unavailable, or + /// [`TrySendError::Disconnected`] if all receivers have been dropped. Errors carry `()` + /// because no message has been supplied. pub fn try_reserve(&self) -> Result, TrySendError<()>> { self.shared.try_reserve() } - /// Attempts to send a value without waiting for capacity. + /// Sends a value without waiting for capacity. /// - /// Returns [`TrySendError::Full`] when no unassigned capacity remains and - /// [`TrySendError::Disconnected`] when all receivers have been dropped. + /// # Errors + /// + /// Returns [`TrySendError::Full`] if capacity is unavailable, or + /// [`TrySendError::Disconnected`] if all receivers have been dropped. Both errors contain the + /// unsent value; nothing is enqueued on error. pub fn try_send(&self, value: T) -> Result<(), TrySendError> { self.shared.try_send(value) } @@ -194,23 +209,30 @@ impl Drop for BoundedReceiver { } impl BoundedReceiver { - /// Receives the next available value. + /// Waits for and receives the next queued value, freeing one capacity slot. + /// + /// Queued values remain receivable after disconnection. /// - /// Buffered values remain available after the final sender is dropped. Once they are drained, - /// this method returns [`RecvError::Disconnected`]. + /// # Errors + /// + /// Returns [`RecvError::Disconnected`] once all senders have been dropped and the queue is + /// empty. /// /// # Cancel safety /// - /// Dropping a pending `recv` future does not consume a value or prevent other receivers from - /// receiving it. + /// Dropping a pending future does not remove a value from the queue or prevent another receiver + /// from receiving it. pub async fn recv(&self) -> Result { self.shared.recv().await } - /// Attempts to receive the next available value without waiting for a message. + /// Receives the next queued value without waiting for a message, freeing one capacity slot. + /// + /// # Errors /// - /// Returns [`TryRecvError::Empty`] while the queue is empty and a sender remains, or - /// [`TryRecvError::Disconnected`] once the queue is empty and all senders have been dropped. + /// Returns [`TryRecvError::Empty`] when the queue is empty but a sender remains. + /// Returns [`TryRecvError::Disconnected`] once all senders have been dropped and the queue is + /// empty. pub fn try_recv(&self) -> Result { self.shared.try_recv() } diff --git a/asyncband/src/mpmc/error.rs b/asyncband/src/mpmc/error.rs index 06bfcf89..cac26ddc 100644 --- a/asyncband/src/mpmc/error.rs +++ b/asyncband/src/mpmc/error.rs @@ -26,7 +26,7 @@ use std::fmt; pub struct SendError(T); impl SendError { - /// Gets a reference to the value that failed to be sent. + /// Returns a reference to the value that failed to be sent. pub fn as_inner(&self) -> &T { &self.0 } @@ -66,7 +66,7 @@ pub enum TrySendError { } impl TrySendError { - /// Gets a reference to the value that failed to be sent. + /// Returns a reference to the value that failed to be sent. pub fn as_inner(&self) -> &T { match self { TrySendError::Full(value) | TrySendError::Disconnected(value) => value, diff --git a/asyncband/src/mpmc/queue.rs b/asyncband/src/mpmc/queue.rs index e355fcf1..2196eab5 100644 --- a/asyncband/src/mpmc/queue.rs +++ b/asyncband/src/mpmc/queue.rs @@ -340,9 +340,13 @@ impl fmt::Debug for Permit<'_, T> { } impl Permit<'_, T> { - /// Enqueues a value using the reserved capacity without waiting for space. + /// Publishes a value using this reserved capacity without waiting for space. /// - /// If the last receiver has been dropped, the error returns ownership of the value. + /// This consumes the permit, including when sending fails. + /// + /// # Errors + /// + /// Returns [`SendError`] containing the unsent value if all receivers have been dropped. pub fn send(self, value: T) -> Result<(), SendError> { let waker = { let mut state = self.shared.state.lock(); diff --git a/asyncband/src/mpmc/unbounded.rs b/asyncband/src/mpmc/unbounded.rs index cd03f15d..0360c0ad 100644 --- a/asyncband/src/mpmc/unbounded.rs +++ b/asyncband/src/mpmc/unbounded.rs @@ -69,9 +69,13 @@ impl Drop for UnboundedSender { } impl UnboundedSender { - /// Sends a value without waiting for capacity. + /// Enqueues a value without waiting for capacity. /// - /// If all receivers have been dropped, the value is returned in [`SendError`]. + /// Success does not guarantee that a receiver will consume the value before being dropped. + /// + /// # Errors + /// + /// Returns [`SendError`] containing the unsent value if all receivers have been dropped. pub fn send(&self, value: T) -> Result<(), SendError> { match self.shared.try_send(value) { Ok(()) => Ok(()), @@ -133,23 +137,30 @@ impl Drop for UnboundedReceiver { } impl UnboundedReceiver { - /// Receives the next available value. + /// Waits for and receives the next queued value. + /// + /// Queued values remain receivable after disconnection. /// - /// Buffered values remain available after the final sender is dropped. Once they are drained, - /// this method returns [`RecvError::Disconnected`]. + /// # Errors + /// + /// Returns [`RecvError::Disconnected`] once all senders have been dropped and the queue is + /// empty. /// /// # Cancel safety /// - /// Dropping a pending `recv` future does not consume a value or prevent other receivers from - /// receiving it. + /// Dropping a pending future does not remove a value from the queue or prevent another receiver + /// from receiving it. pub async fn recv(&self) -> Result { self.shared.recv().await } - /// Attempts to receive the next available value without waiting for a message. + /// Receives the next queued value without waiting for a message. + /// + /// # Errors /// - /// Returns [`TryRecvError::Empty`] while the queue is empty and a sender remains, or - /// [`TryRecvError::Disconnected`] once the queue is empty and all senders have been dropped. + /// Returns [`TryRecvError::Empty`] when the queue is empty but a sender remains. + /// Returns [`TryRecvError::Disconnected`] once all senders have been dropped and the queue is + /// empty. pub fn try_recv(&self) -> Result { self.shared.try_recv() } diff --git a/asyncband/src/mpsc/bounded/receiver.rs b/asyncband/src/mpsc/bounded/receiver.rs index 47d6bd16..2907bf40 100644 --- a/asyncband/src/mpsc/bounded/receiver.rs +++ b/asyncband/src/mpsc/bounded/receiver.rs @@ -70,11 +70,13 @@ impl BoundedReceiver { Self { shared } } - /// Attempts to receive the next queued value without waiting for a new message. + /// Receives the next queued value without waiting for a message, freeing one capacity slot. /// - /// Receiving a value frees one buffer slot. An empty channel returns [`TryRecvError::Empty`] - /// while at least one sender remains, or [`TryRecvError::Disconnected`] after every sender has - /// been dropped and all queued values have been consumed. + /// # Errors + /// + /// Returns [`TryRecvError::Empty`] when the queue is empty but a sender remains. + /// Returns [`TryRecvError::Disconnected`] once all senders have been dropped and the queue is + /// empty. /// /// # Examples /// @@ -107,17 +109,19 @@ impl BoundedReceiver { Ok(value) } - /// Waits for and receives the next value, freeing one buffer slot. + /// Waits for and receives the next queued value, freeing one capacity slot. + /// + /// Queued values remain receivable after disconnection. + /// + /// # Errors /// - /// If no value is queued, this method waits until a sender adds one or the last sender is - /// dropped. It returns [`RecvError::Disconnected`] only after all senders are gone and the - /// buffer has been drained. + /// Returns [`RecvError::Disconnected`] once all senders have been dropped and the queue is + /// empty. /// /// # Cancel safety /// - /// Dropping a pending `recv` future does not remove a message from the channel. A subsequent - /// `recv` future can still observe the next queued value, so these futures may safely be raced - /// with other futures in a selection construct. + /// Dropping a pending future does not remove a value from the queue. A later call on this + /// receiver can still receive the next value. /// /// # Examples /// diff --git a/asyncband/src/mpsc/bounded/sender.rs b/asyncband/src/mpsc/bounded/sender.rs index 03927f7d..d72b73b6 100644 --- a/asyncband/src/mpsc/bounded/sender.rs +++ b/asyncband/src/mpsc/bounded/sender.rs @@ -74,9 +74,11 @@ impl BoundedSender { Self { shared } } - /// Sends a message, waiting until the channel has capacity when necessary. + /// Sends a value, waiting for capacity when the queue is full. /// - /// If the receiver has been dropped, the returned error contains `value`. + /// # Errors + /// + /// Returns [`SendError`] containing the unsent value if the receiver has been dropped. /// /// # Cancel safety /// @@ -97,15 +99,16 @@ impl BoundedSender { } } - /// Reserves capacity for one message before constructing it. + /// Reserves capacity for one message before the caller supplies its value. + /// + /// The returned [`Permit`] holds one capacity slot. Dropping it releases that slot; + /// [`Permit::send`] publishes a value without waiting for space. Holding a permit does not + /// establish message order or keep a receiver alive. /// - /// A successful reservation returns a [`Permit`]. Dropping the permit releases capacity; - /// [`Permit::send`] publishes a value without waiting for space. Reservations do not establish - /// message order: other producers may send while a permit is held. + /// # Errors /// - /// Returns `SendError(())` if the receiver has been dropped. A permit obtained earlier does - /// not keep the receiver alive; sending with it can still return the unsent value on - /// disconnect. + /// Returns `SendError(())` if the receiver has been dropped. A successful reservation + /// can still fail to send if disconnection happens before [`Permit::send`]. /// /// # Cancel safety /// @@ -139,8 +142,14 @@ impl BoundedSender { /// Reserves capacity for one message without waiting. /// - /// Returns [`TrySendError::Full`] if queued messages and outstanding permits occupy the - /// buffer, or [`TrySendError::Disconnected`] if the receiver has been dropped. + /// Dropping the returned [`Permit`] releases its capacity. See [`reserve`](Self::reserve) + /// for reservation and message ordering. + /// + /// # Errors + /// + /// Returns [`TrySendError::Full`] if capacity is unavailable, or + /// [`TrySendError::Disconnected`] if the receiver has been dropped. Errors carry `()` + /// because no message has been supplied. pub fn try_reserve(&self) -> Result, TrySendError<()>> { self.shared.lock().acquire()?; Ok(Permit { @@ -148,10 +157,13 @@ impl BoundedSender { }) } - /// Attempts to send a message without waiting for capacity. + /// Sends a value without waiting for capacity. /// - /// A full buffer returns [`TrySendError::Full`], while a dropped receiver returns - /// [`TrySendError::Disconnected`]. Both errors return ownership of the unsent value. + /// # Errors + /// + /// Returns [`TrySendError::Full`] if capacity is unavailable, or + /// [`TrySendError::Disconnected`] if the receiver has been dropped. Both errors contain the + /// unsent value; nothing is enqueued on error. /// /// # Examples /// @@ -224,9 +236,13 @@ impl fmt::Debug for Permit<'_, T> { } impl Permit<'_, T> { - /// Publishes a message using this permit, without waiting for capacity. + /// Publishes a value using this reserved capacity without waiting for space. + /// + /// This consumes the permit, including when sending fails. + /// + /// # Errors /// - /// If the receiver has been dropped, the returned error contains the unsent value. + /// Returns [`SendError`] containing the unsent value if the receiver has been dropped. pub fn send(self, value: T) -> Result<(), SendError> { let mut state = self.shared.lock(); if !state.receiver { diff --git a/asyncband/src/mpsc/error.rs b/asyncband/src/mpsc/error.rs index e55d801a..f67f7931 100644 --- a/asyncband/src/mpsc/error.rs +++ b/asyncband/src/mpsc/error.rs @@ -36,7 +36,7 @@ use std::fmt; pub struct SendError(T); impl SendError { - /// Gets a reference to the unsent message, or `()` for a failed reservation. + /// Returns a reference to the unsent message, or `()` for a failed reservation. pub fn as_inner(&self) -> &T { &self.0 } @@ -81,7 +81,7 @@ pub enum TrySendError { } impl TrySendError { - /// Gets a reference to the unsent message, or `()` for a failed reservation. + /// Returns a reference to the unsent message, or `()` for a failed reservation. pub fn as_inner(&self) -> &T { match self { TrySendError::Full(msg) | TrySendError::Disconnected(msg) => msg, diff --git a/asyncband/src/mpsc/unbounded/receiver.rs b/asyncband/src/mpsc/unbounded/receiver.rs index d81d064e..33e6ba88 100644 --- a/asyncband/src/mpsc/unbounded/receiver.rs +++ b/asyncband/src/mpsc/unbounded/receiver.rs @@ -72,11 +72,13 @@ impl UnboundedReceiver { } } - /// Attempts to receive the next queued value without waiting for a new message. + /// Receives the next queued value without waiting for a message. /// - /// An empty channel returns [`TryRecvError::Empty`] while at least one sender remains, or - /// [`TryRecvError::Disconnected`] after every sender has been dropped and all queued values - /// have been consumed. + /// # Errors + /// + /// Returns [`TryRecvError::Empty`] when the queue is empty but a sender remains. + /// Returns [`TryRecvError::Disconnected`] once all senders have been dropped and the queue is + /// empty. /// /// # Examples /// @@ -113,17 +115,19 @@ impl UnboundedReceiver { Ok(pop_batch(batch)) } - /// Waits for and receives the next value. + /// Waits for and receives the next queued value. + /// + /// Queued values remain receivable after disconnection. + /// + /// # Errors /// - /// If no value is queued, this method waits until a sender adds one or the last sender is - /// dropped. It returns [`RecvError::Disconnected`] only after all senders are gone and the - /// queue has been drained. + /// Returns [`RecvError::Disconnected`] once all senders have been dropped and the queue is + /// empty. /// /// # Cancel safety /// - /// Dropping a pending `recv` future does not remove a message from the channel. A subsequent - /// `recv` future can still observe the next queued value, so these futures may safely be raced - /// with other futures in a selection construct. + /// Dropping a pending future does not remove a value from the queue. A later call on this + /// receiver can still receive the next value. /// /// # Examples /// diff --git a/asyncband/src/mpsc/unbounded/sender.rs b/asyncband/src/mpsc/unbounded/sender.rs index 6e111b41..8e58571a 100644 --- a/asyncband/src/mpsc/unbounded/sender.rs +++ b/asyncband/src/mpsc/unbounded/sender.rs @@ -66,11 +66,13 @@ impl UnboundedSender { Self { shared } } - /// Enqueues a message without waiting for capacity. + /// Enqueues a value without waiting for capacity. /// - /// This operation is synchronous because the channel has no capacity limit. If the receiver has - /// been dropped, the returned error contains `value`. Success means the message was queued; - /// it does not guarantee that the receiver will consume it before being dropped. + /// Success does not guarantee that a receiver will consume the value before being dropped. + /// + /// # Errors + /// + /// Returns [`SendError`] containing the unsent `value` if the receiver has been dropped. pub fn send(&self, value: T) -> Result<(), SendError> { let waker = { let mut state = self.shared.lock(); diff --git a/asyncband/src/mutex/mod.rs b/asyncband/src/mutex/mod.rs index 74b3bd0d..6f895ecf 100644 --- a/asyncband/src/mutex/mod.rs +++ b/asyncband/src/mutex/mod.rs @@ -146,7 +146,8 @@ impl Mutex { /// /// # Cancel safety /// - /// Pending lock requests complete in order. Cancelling this call loses its place among them. + /// Dropping a pending future removes this request and releases any assigned permit. A later + /// attempt joins the back of the queue. No guard is returned on cancellation. /// /// # Examples /// @@ -198,7 +199,9 @@ impl Mutex { /// /// # Cancel safety /// - /// Pending lock requests complete in order. Cancelling this call loses its place among them. + /// Dropping a pending future removes this request and releases any assigned permit. A later + /// attempt joins the back of the queue. No guard is returned, and the future releases its owned + /// `Arc`. /// /// # Examples /// @@ -326,6 +329,11 @@ impl<'a, T: ?Sized> MutexGuard<'a, T> { /// The returned guard keeps the same mutex locked. Call this as /// `MutexGuard::map(...)` so a method named `map` on `T` remains accessible through deref. /// + /// # Panics + /// + /// If `f` panics, unwinding drops the consumed guard and releases its lock access. Changes + /// already made through the guard are not rolled back. + /// /// # Examples /// /// ``` @@ -378,9 +386,19 @@ impl<'a, T: ?Sized> MutexGuard<'a, T> { /// Attempts to project this guard to a mutable component of the protected value. /// - /// The original guard is returned when `f` returns `None`. Call this as + /// Call this as /// `MutexGuard::filter_map(...)` so a method with the same name on `T` remains accessible. /// + /// # Errors + /// + /// Returns `Err(orig)` if `f` returns `None`. The original guard still holds its lock access; + /// no unlock occurs on this path. + /// + /// # Panics + /// + /// If `f` panics, unwinding drops the consumed guard and releases its lock access. Changes + /// already made through the guard are not rolled back. + /// /// # Examples /// /// ``` @@ -493,6 +511,11 @@ impl OwnedMutexGuard { /// The returned guard retains the same `Arc` and keeps the mutex locked. Call this as /// `OwnedMutexGuard::map(...)` so a method named `map` on `T` remains accessible through deref. /// + /// # Panics + /// + /// If `f` panics, unwinding drops the consumed guard and releases its lock access. Changes + /// already made through the guard are not rolled back. + /// /// # Examples /// /// ``` @@ -543,9 +566,19 @@ impl OwnedMutexGuard { /// Attempts to project this guard to a mutable component of the protected value. /// - /// The original guard is returned when `f` returns `None`. Call this as + /// Call this as /// `OwnedMutexGuard::filter_map(...)` so a method with the same name on `T` remains accessible. /// + /// # Errors + /// + /// Returns `Err(orig)` if `f` returns `None`. The original guard still holds its lock access; + /// no unlock occurs on this path. + /// + /// # Panics + /// + /// If `f` panics, unwinding drops the consumed guard and releases its lock access. Changes + /// already made through the guard are not rolled back. + /// /// # Examples /// /// ``` @@ -692,6 +725,11 @@ impl<'a, T: ?Sized> MappedMutexGuard<'a, T> { /// `MappedMutexGuard::map(...)` so a method named `map` on `T` remains accessible through /// deref. /// + /// # Panics + /// + /// If `f` panics, unwinding drops the consumed guard and releases its lock access. Changes + /// already made through the guard are not rolled back. + /// /// # Examples /// /// ``` @@ -748,10 +786,20 @@ impl<'a, T: ?Sized> MappedMutexGuard<'a, T> { /// Attempts to project an already mapped guard to a deeper mutable component. /// - /// The original mapped guard is returned when `f` returns `None`. Call this as + /// Call this as /// `MappedMutexGuard::filter_map(...)` so a method with the same name on `T` remains /// accessible. /// + /// # Errors + /// + /// Returns `Err(orig)` if `f` returns `None`. The original guard still holds its lock access; + /// no unlock occurs on this path. + /// + /// # Panics + /// + /// If `f` panics, unwinding drops the consumed guard and releases its lock access. Changes + /// already made through the guard are not rolled back. + /// /// # Examples /// /// ``` @@ -893,6 +941,11 @@ impl OwnedMappedMutexGuard { /// `OwnedMappedMutexGuard::map(...)` so a method named `map` on `U` remains accessible through /// deref. /// + /// # Panics + /// + /// If `f` panics, unwinding drops the consumed guard and releases its lock access. Changes + /// already made through the guard are not rolled back. + /// /// # Examples /// /// ``` @@ -947,10 +1000,20 @@ impl OwnedMappedMutexGuard { /// Attempts to project an owned mapped guard to a deeper mutable component. /// - /// The original mapped guard is returned when `f` returns `None`. Call this as + /// Call this as /// `OwnedMappedMutexGuard::filter_map(...)` so a method with the same name on `U` remains /// accessible through deref. /// + /// # Errors + /// + /// Returns `Err(orig)` if `f` returns `None`. The original guard still holds its lock access; + /// no unlock occurs on this path. + /// + /// # Panics + /// + /// If `f` panics, unwinding drops the consumed guard and releases its lock access. Changes + /// already made through the guard are not rolled back. + /// /// # Examples /// /// ``` diff --git a/asyncband/src/once/lazy_cell/mod.rs b/asyncband/src/once/lazy_cell/mod.rs index 8e6fc19d..d3b42a86 100644 --- a/asyncband/src/once/lazy_cell/mod.rs +++ b/asyncband/src/once/lazy_cell/mod.rs @@ -196,13 +196,27 @@ where F: FnOnce() -> Fut, Fut: Future + Unpin, { - /// Initializes the value if needed and returns a reference to it. + /// Initializes the cell if needed and returns a reference to its value. /// - /// This method is available when moving the stored future is safe. A future behind - /// `Pin>` is `Unpin` because moving the box does not move its pointee. + /// The stored initialization future must be [`Unpin`]. Use a pinned cell and + /// [`force_pin`](Self::force_pin) for an inline future that is not `Unpin`. + /// Concurrent callers wait while one caller drives the initializer. /// - /// If another task is initializing the cell, this call waits for that attempt. If the task - /// driving initialization is cancelled, a later caller resumes the same future. + /// # Cancel safety + /// + /// Dropping a pending call preserves the initialization future in the cell. A later caller + /// resumes that same future instead of invoking the initializer again. Already performed + /// effects remain in place; initialization does not continue without a caller polling it. + /// + /// # Panics + /// + /// Panics if the cell is poisoned. A panic while invoking or polling the initializer propagates + /// to the caller and poisons the cell; subsequent forcing calls panic as well. + /// + /// # Deadlocks + /// + /// The initializer must not await forced access to the same cell, because it would wait for + /// its own completion. /// /// # Examples /// @@ -217,16 +231,26 @@ where /// assert_eq!(*LazyCell::force(&lazy).await, 92); /// # } /// ``` - /// - /// # Panics - /// - /// Panics if the initializer panics or the cell was previously poisoned. Recursive - /// initialization of the same cell deadlocks. pub async fn force(this: &Self) -> &T { Self::force_pin(Pin::new(this)).await } - /// Initializes the value if needed and returns mutable access to it. + /// Initializes the cell if needed and returns mutable access to its value. + /// + /// The stored initialization future must be [`Unpin`]. Use a pinned cell and + /// [`force_pin_mut`](Self::force_pin_mut) for an inline future that is not `Unpin`. + /// The exclusive borrow prevents another caller from driving initialization. + /// + /// # Cancel safety + /// + /// Dropping a pending call preserves the initialization future in the cell. A later caller + /// resumes that same future instead of invoking the initializer again. Already performed + /// effects remain in place; initialization does not continue without a caller polling it. + /// + /// # Panics + /// + /// Panics if the cell is poisoned. A panic while invoking or polling the initializer propagates + /// to the caller and poisons the cell; subsequent forcing calls panic as well. /// /// # Examples /// @@ -240,10 +264,6 @@ where /// assert_eq!(LazyCell::get(&lazy).map(String::as_str), Some("ready!")); /// # } /// ``` - /// - /// # Panics - /// - /// Panics under the same conditions as [`force`](Self::force). pub async fn force_mut(this: &mut Self) -> &mut T { Self::force_pin_mut(Pin::new(this)).await } @@ -254,13 +274,27 @@ where F: FnOnce() -> Fut, Fut: Future, { - /// Initializes a pinned cell and returns a reference to its value. + /// Initializes the pinned cell if needed and returns a reference to its value. /// - /// Pinning the cell keeps an inline initialization future at a stable address. Cancellation - /// leaves that future in the cell so a later caller can resume it. + /// Pinning keeps the stored initialization future at a stable address, including when it is + /// not [`Unpin`]. + /// Concurrent callers wait while one caller drives the initializer. /// - /// Use this method when the stored future is not [`Unpin`]. [`Box::pin`] is one way to pin the - /// whole cell; [`Pin::as_ref`] then supplies the `Pin<&Self>` required here. + /// # Cancel safety + /// + /// Dropping a pending call preserves the initialization future in the cell. A later caller + /// resumes that same future instead of invoking the initializer again. Already performed + /// effects remain in place; initialization does not continue without a caller polling it. + /// + /// # Panics + /// + /// Panics if the cell is poisoned. A panic while invoking or polling the initializer propagates + /// to the caller and poisons the cell; subsequent forcing calls panic as well. + /// + /// # Deadlocks + /// + /// The initializer must not await forced access to the same cell, because it would wait for + /// its own completion. /// /// # Examples /// @@ -274,11 +308,6 @@ where /// assert_eq!(*LazyCell::force_pin(lazy.as_ref()).await, 92); /// # } /// ``` - /// - /// # Panics - /// - /// Panics if the initializer panics or the cell was previously poisoned. Recursive - /// initialization of the same cell deadlocks. pub async fn force_pin<'a>(this: Pin<&'a Self>) -> &'a T { let this_ref: &'a Self = Pin::get_ref(this); if let Some(value) = Self::get(this_ref) { @@ -302,10 +331,22 @@ where unsafe { this_ref.value.publish(value) } } - /// Initializes a pinned cell and returns mutable access to its value. + /// Initializes the pinned cell if needed and returns mutable access to its value. /// - /// Exclusive access lets this method initialize the cell directly. Calling [`Pin::as_mut`] on - /// a pinned box supplies the required `Pin<&mut Self>` without moving the cell. + /// Pinning keeps the stored initialization future at a stable address, including when it is + /// not [`Unpin`]. + /// The exclusive borrow prevents another caller from driving initialization. + /// + /// # Cancel safety + /// + /// Dropping a pending call preserves the initialization future in the cell. A later caller + /// resumes that same future instead of invoking the initializer again. Already performed + /// effects remain in place; initialization does not continue without a caller polling it. + /// + /// # Panics + /// + /// Panics if the cell is poisoned. A panic while invoking or polling the initializer propagates + /// to the caller and poisons the cell; subsequent forcing calls panic as well. /// /// # Examples /// @@ -320,10 +361,6 @@ where /// assert_eq!(value, "ready!"); /// # } /// ``` - /// - /// # Panics - /// - /// Panics under the same conditions as [`force_pin`](Self::force_pin). pub async fn force_pin_mut<'a>(this: Pin<&'a mut Self>) -> &'a mut T { // SAFETY: We do not move the structurally pinned future through this reference. The stored // value is not structurally pinned and may be accessed normally. diff --git a/asyncband/src/once/once/mod.rs b/asyncband/src/once/once/mod.rs index 3c681588..5fecb846 100644 --- a/asyncband/src/once/once/mod.rs +++ b/asyncband/src/once/once/mod.rs @@ -131,16 +131,26 @@ impl Once { self.done.try_wait().is_ok() } - /// Runs `init` if this `Once` has not yet completed an initialization. + /// Runs `f` if initialization has not completed. /// - /// Concurrent callers arriving while `init` is in progress wait until it finishes. Only one - /// caller can succeed; once an execution returns normally, every current and future call - /// completes immediately without running `init` again. + /// One caller runs its initializer while others wait. After an initializer completes normally, + /// all current and future calls return without running another initializer. /// - /// If the active attempt panics or its future is dropped before completing, the `Once` - /// stays uncompleted and a queued caller starts a fresh attempt. Calling `call_once` - /// recursively on the same `Once` from inside `init` deadlocks while waiting for the outer - /// call to finish. + /// # Cancel safety + /// + /// Dropping a pending call that is waiting does not interrupt the active initializer. Dropping + /// the call running `f` cancels that attempt and allows a waiting or later caller to run its + /// own initializer. Effects already performed by `f` are not rolled back. + /// + /// # Panics + /// + /// A panic in `f` propagates to its caller. Initialization remains incomplete and may be + /// retried; this primitive is not poisoned. + /// + /// # Deadlocks + /// + /// An initializer must not await `call_once` or `wait` on the same `Once`, because completion + /// depends on that initializer returning. /// /// # Examples /// @@ -182,7 +192,14 @@ impl Once { self.done.wake_all(); } - /// Waits asynchronously until some `call_once` has completed successfully. + /// Waits until an initializer passed to [`call_once`](Self::call_once) completes successfully. + /// + /// This call does not start initialization. + /// + /// # Cancel safety + /// + /// Dropping a pending future unregisters only this wait. It does not cancel initialization, + /// change the completed state, or affect other waiters. /// /// # Examples /// diff --git a/asyncband/src/once/once_cell/mod.rs b/asyncband/src/once/once_cell/mod.rs index 1f28ac06..8ee243d5 100644 --- a/asyncband/src/once/once_cell/mod.rs +++ b/asyncband/src/once/once_cell/mod.rs @@ -111,14 +111,27 @@ impl OnceCell { self.value.get_mut() } - /// Returns a reference to the stored value, running `init` if the cell is currently empty. + /// Returns a reference to the stored value, initializing the cell with `init` when needed. /// - /// Concurrent callers share one in-flight initialization: later callers wait for the active - /// attempt to complete and receive a reference to its published value. If the active - /// initializer panics or its future is dropped before completing, the cell remains empty and a - /// waiting caller takes over with its own `init`. + /// Concurrent callers wait for the active initializer. Once it publishes a value, all callers + /// receive a reference to that value without running their own initializer. /// - /// Calling this method recursively from inside `init` on the same cell will deadlock. + /// # Cancel safety + /// + /// Dropping a pending call while it waits leaves the active initializer running. If this call + /// is running the initializer, cancellation drops that initializer and leaves the cell empty; + /// a later caller can start a new attempt. Effects already performed by the initializer are + /// not rolled back. + /// + /// # Panics + /// + /// An initializer panic propagates to its caller and leaves the cell empty for a later + /// attempt. The cell is not poisoned. + /// + /// # Deadlocks + /// + /// An initializer must not wait for initialization of the same cell: that would wait for + /// its own completion. pub async fn get_or_init(&self, init: F) -> &T where F: AsyncFnOnce() -> T, @@ -131,13 +144,33 @@ impl OnceCell { } } - /// Returns a reference to the stored value, running a fallible `init` if the cell is empty. + /// Returns a reference to the stored value, initializing the cell with a fallible `init` when + /// needed. + /// + /// Concurrent callers wait for the active initializer. A published value is shared by all + /// callers; an unsuccessful attempt allows another caller to run its own initializer. + /// + /// # Errors + /// + /// Returns the error from this call's initializer without storing a value. The error is not + /// shared with waiting callers, which may make their own attempts. /// - /// Concurrent callers wait while an initialization attempt is running. When that attempt - /// publishes a value, waiting callers receive a reference to it; when it returns `Err`, - /// panics, or is cancelled, the cell stays empty and the next queued caller runs its `init`. + /// # Cancel safety /// - /// Calling this method recursively from inside `init` on the same cell will deadlock. + /// Dropping a pending call while it waits leaves the active initializer running. If this call + /// is running the initializer, cancellation drops that initializer and leaves the cell empty; + /// a later caller can start a new attempt. Effects already performed by the initializer are + /// not rolled back. + /// + /// # Panics + /// + /// An initializer panic propagates to its caller and leaves the cell empty for a later + /// attempt. The cell is not poisoned. + /// + /// # Deadlocks + /// + /// An initializer must not wait for initialization of the same cell: that would wait for + /// its own completion. pub async fn get_or_try_init(&self, init: F) -> Result<&T, E> where F: AsyncFnOnce() -> Result, @@ -157,10 +190,19 @@ impl OnceCell { Ok(self.set_value(value, permit)) } - /// Returns exclusive access to the stored value, running `init` if the cell is empty. + /// Returns mutable access to the stored value, running `init` if the cell is empty. + /// + /// The exclusive borrow prevents concurrent initialization. + /// + /// # Cancel safety + /// + /// Dropping a pending call drops its initializer and leaves the cell empty. A later call can + /// start a new attempt; effects already performed by the initializer are not rolled back. /// - /// Exclusive ownership (`&mut self`) rules out concurrent initialization, so this method runs - /// `init` directly when needed without acquiring the semaphore. + /// # Panics + /// + /// An initializer panic propagates to its caller and leaves the cell empty for a later + /// attempt. The cell is not poisoned. /// /// # Examples /// @@ -187,11 +229,23 @@ impl OnceCell { } } - /// Returns exclusive access to the stored value, running a fallible `init` if the cell is - /// empty. + /// Returns mutable access to the stored value, running a fallible `init` if the cell is empty. + /// + /// The exclusive borrow prevents concurrent initialization. + /// + /// # Errors + /// + /// Returns the initializer's error and leaves the cell empty. /// - /// Exclusive ownership (`&mut self`) rules out concurrent initialization, so this method runs - /// `init` directly when needed. Returning an error leaves the cell empty for a later call. + /// # Cancel safety + /// + /// Dropping a pending call drops its initializer and leaves the cell empty. A later call can + /// start a new attempt; effects already performed by the initializer are not rolled back. + /// + /// # Panics + /// + /// An initializer panic propagates to its caller and leaves the cell empty for a later + /// attempt. The cell is not poisoned. /// /// # Examples /// @@ -231,11 +285,20 @@ impl OnceCell { Ok(self.set_value_mut(value)) } - /// Stores `value` if the cell is empty and returns a reference to the resulting contents. + /// Stores `value` if the cell is empty and returns a reference to it. + /// + /// Waits for an active initializer before checking whether a value has already been published. /// - /// If another initialization is currently in flight, this call waits until that attempt - /// finishes. Returns `Ok(&stored)` when `value` was installed, or `Err((&existing, value))` if - /// the cell already held a value. + /// # Errors + /// + /// Returns `Err((existing, value))` if the cell is already initialized, including when another + /// initializer publishes a value while this call waits. The error contains a reference to the + /// stored value and the unused input. The stored value is unchanged. + /// + /// # Cancel safety + /// + /// Dropping a pending future drops `value` without storing it. It does not cancel another + /// caller's initialization. /// /// # Examples /// @@ -258,10 +321,19 @@ impl OnceCell { } } - /// Stores `value` if the cell is empty, waiting behind any active initialization. + /// Stores `value` if the cell is empty. + /// + /// Waits for an active initializer before checking whether a value has already been published. + /// + /// # Errors + /// + /// Returns `Err(value)` if the cell is already initialized, including when another initializer + /// publishes a value while this call waits. The stored value is unchanged. + /// + /// # Cancel safety /// - /// Returns `Ok(())` when `value` was installed, or `Err(value)` if another caller had already - /// populated the cell. + /// Dropping a pending future drops `value` without storing it. It does not cancel another + /// caller's initialization. /// /// # Examples /// diff --git a/asyncband/src/once/once_map/mod.rs b/asyncband/src/once/once_map/mod.rs index 8e660279..74664771 100644 --- a/asyncband/src/once/once_map/mod.rs +++ b/asyncband/src/once/once_map/mod.rs @@ -296,18 +296,29 @@ where } } - /// Compute the value for the given key if absent. + /// Returns a clone of the stored or newly computed value for `key`. /// - /// If the value for the key is already being computed by another task, this task will wait for - /// the computation to finish and return the result. + /// A stored value is returned without invoking `func`. Otherwise, callers for an equivalent + /// key share an active computation and receive clones of its successful result. Values are + /// cached until removed. Removing an in-flight entry detaches it: existing callers still + /// receive its result, but that result is not cached in the map. /// - /// If the computation is cancelled or panics, another caller waiting for the same key may retry - /// it. + /// # Cancel safety + /// + /// Dropping a caller that is waiting for a result does not cancel the active computation. + /// Dropping the caller executing `func` drops that attempt; another caller on the entry may + /// run its own `func`. Effects already performed by the cancelled attempt are not rolled back. + /// + /// # Panics + /// + /// A panic in `func` propagates to its caller without publishing a value. Another caller may + /// retry the computation. /// /// # Deadlocks /// - /// The computation must not call `compute` or `try_compute` for an equivalent key on this map - /// because it would wait for its own result. Operations on other keys remain independent. + /// The computation must not await `compute` or `try_compute` for an equivalent key on + /// the same current entry, because it would wait for its own result. Other keys are + /// independent. pub async fn compute(&self, key: K, func: F) -> V where F: AsyncFnOnce() -> V, @@ -322,18 +333,34 @@ where result } - /// Compute the value for the given key if absent. + /// Returns a clone of the stored or newly computed value for `key`. + /// + /// A stored value is returned without invoking `func`. Otherwise, callers for an equivalent + /// key share an active computation and receive clones of its successful result. Values are + /// cached until removed. Removing an in-flight entry detaches it: existing callers still + /// receive its result, but that result is not cached in the map. + /// + /// # Errors + /// + /// Returns an error from this call's `func` only to this caller. Errors are not stored or + /// shared; another caller waiting on the entry may run its own `func`. + /// + /// # Cancel safety + /// + /// Dropping a caller that is waiting for a result does not cancel the active computation. + /// Dropping the caller executing `func` drops that attempt; another caller on the entry may + /// run its own `func`. Effects already performed by the cancelled attempt are not rolled back. /// - /// If the value for the key is already being computed by another task, this task will wait for - /// the computation to finish and return the result. + /// # Panics /// - /// If the computation returns an error, it is returned to that caller and the value is not - /// stored. After an error, cancellation, or panic, another caller may retry the computation. + /// A panic in `func` propagates to its caller without publishing a value. Another caller may + /// retry the computation. /// /// # Deadlocks /// - /// The computation must not call `compute` or `try_compute` for an equivalent key on this map - /// because it would wait for its own result. Operations on other keys remain independent. + /// The computation must not await `compute` or `try_compute` for an equivalent key on + /// the same current entry, because it would wait for its own result. Other keys are + /// independent. pub async fn try_compute(&self, key: K, func: F) -> Result where F: AsyncFnOnce() -> Result, @@ -348,7 +375,7 @@ where Ok(result) } - /// Gets a clone of the value for the given key without waiting. + /// Returns a clone of the value for the given key without waiting. /// /// Returns `None` when the key is absent or its computation is still in flight. pub fn get(&self, key: &Q) -> Option @@ -359,7 +386,7 @@ where self.get_value(key) } - /// Remove the given key from the map. + /// Removes the given key from the map. /// /// If you need to get the value that has been removed, use the [`remove`] method instead. /// diff --git a/asyncband/src/oneshot/receiver.rs b/asyncband/src/oneshot/receiver.rs index 643ea4c8..9f1f862e 100644 --- a/asyncband/src/oneshot/receiver.rs +++ b/asyncband/src/oneshot/receiver.rs @@ -45,8 +45,12 @@ use crate::oneshot::drop_message_and_deallocate_channel; /// The receiving half of a [`oneshot`](crate::oneshot) channel. /// -/// Awaiting converts this receiver into a [`Recv`] future linked to the paired [`Sender`]. Dropping -/// either the receiver or its future disconnects the channel and discards any unread message. +/// Awaiting converts this receiver into a [`Recv`] future linked to the paired [`Sender`]. +/// +/// # Cancel safety +/// +/// Dropping either the receiver or its future disconnects the channel and discards any unread +/// message. /// /// To keep a pending operation alive when another branch wins, call [`Receiver::into_future`] /// before selecting and borrow the resulting future as `&mut Recv`. The receiver itself does not @@ -73,6 +77,17 @@ impl IntoFuture for Receiver { type IntoFuture = Recv; + /// Converts this receiver into a future that owns the receiving endpoint. + /// + /// # Errors + /// + /// The future reports [`RecvError::Disconnected`] if the sender drops without sending or + /// the message was already taken. See [`Recv`]. + /// + /// # Cancel safety + /// + /// Dropping the returned future disconnects the channel and discards any unread message, + /// even before its first poll. Retain and borrow the future to keep waiting after selection. fn into_future(self) -> Self::IntoFuture { // `Recv` takes over receiver-side cleanup, so `Receiver::drop` must not run afterward. let receiver = ManuallyDrop::new(self); @@ -115,16 +130,15 @@ impl Receiver { matches!(channel.state.load(Ordering::Relaxed), MESSAGE) } - /// Checks if there is a message in the channel without blocking. Returns: + /// Takes the message if it is available, without waiting. + /// + /// A successful receive consumes the only message. Later receives report disconnection. /// - /// * `Ok(message)` if there was a message in the channel. - /// * `Err(TryRecvError::Empty)` if the [`Sender`] is alive, but has not yet sent a message. - /// * `Err(TryRecvError::Disconnected)` if the [`Sender`] was dropped before sending anything or - /// if the message has already been extracted by a previous `try_recv` call. + /// # Errors /// - /// If a message is returned, the channel is disconnected and any subsequent attempt to receive - /// a message will return an error: [`TryRecvError::Disconnected`] for `try_recv`, or - /// [`RecvError::Disconnected`] when awaiting the receiver. + /// Returns [`TryRecvError::Empty`] if the sender is alive but has not sent a message. + /// Returns [`TryRecvError::Disconnected`] if the sender dropped without sending or the + /// message was already taken. pub fn try_recv(&self) -> Result { // SAFETY: The channel will not be freed while this method is still running. let channel = unsafe { self.channel_ptr.as_ref() }; @@ -196,14 +210,21 @@ impl Drop for Receiver { } } -/// Future produced by awaiting a [`Receiver`], resolving to the message from [`Sender`] or -/// [`RecvError`] if the sender dropped without sending. +/// A future that receives the channel's only message. +/// +/// Created by [`Receiver::into_future`], this future owns the receiving endpoint. A completed +/// future must not be polled again. +/// +/// # Errors +/// +/// Resolves to [`RecvError::Disconnected`] if the sender drops without sending or the +/// message was already taken before conversion. /// -/// Created by [`Receiver::into_future`], this future owns the receiving endpoint. Dropping it -/// disconnects the channel and discards any unread message. +/// # Cancel safety /// -/// Select on `&mut Recv` to keep the pending future alive when another branch wins. A completed -/// `Recv` future must not be polled again. +/// Dropping this future disconnects the channel and discards any unread message, even if the +/// future was never polled. To keep the operation alive when another branch wins a selection, +/// retain the same future and select on `&mut Recv`. pub struct Recv { channel_ptr: NonNull>, } diff --git a/asyncband/src/oneshot/sender.rs b/asyncband/src/oneshot/sender.rs index f4c7b3aa..ea639e16 100644 --- a/asyncband/src/oneshot/sender.rs +++ b/asyncband/src/oneshot/sender.rs @@ -55,8 +55,11 @@ unsafe impl Send for Sender {} unsafe impl Sync for Sender {} impl Sender { - /// Delivers `message` to the paired [`Receiver`], or returns it in [`SendError`] if the - /// receiver has already been dropped. + /// Sends `message` to the paired [`Receiver`], consuming this sender. + /// + /// # Errors + /// + /// Returns [`SendError`] containing the unsent message if the receiver has been dropped. pub fn send(self, message: T) -> Result<(), SendError> { // `send` takes over endpoint cleanup, so `Sender::drop` must not run afterward. let sender = ManuallyDrop::new(self); @@ -226,7 +229,7 @@ unsafe impl Send for SendError {} unsafe impl Sync for SendError {} impl SendError { - /// Get a reference to the message that failed to be sent. + /// Returns a reference to the message that failed to be sent. pub fn as_inner(&self) -> &T { // SAFETY: SendError exclusively owns the allocation and its initialized message. unsafe { self.channel_ptr.as_ref().message() } diff --git a/asyncband/src/phaser/mod.rs b/asyncband/src/phaser/mod.rs index 60c0f40e..a7f6f706 100644 --- a/asyncband/src/phaser/mod.rs +++ b/asyncband/src/phaser/mod.rs @@ -271,11 +271,15 @@ impl Phaser { self.state.lock().unarrived } - /// Registers one participant in the current phase, or returns [`Closed`]. + /// Registers one participant in the current phase. /// /// Registration racing with advancement joins the phase before or after that advancement. /// The returned participant owns a shared handle and does not borrow this one. /// + /// # Errors + /// + /// Returns [`Closed`] without registering a participant if the phaser is closed. + /// /// # Panics /// /// Panics if the registered count would exceed `usize::MAX`. @@ -292,8 +296,12 @@ impl Phaser { /// participants; yielded handles keep their registrations. The iterator owns a shared handle /// and does not borrow this phaser. /// - /// Returns [`Closed`] without registering anyone if the phaser is closed. On an open phaser, - /// a zero-sized batch does nothing. Collect this iterator into a collection of your choice. + /// On an open phaser, a zero-sized batch does nothing. Collect this iterator into a collection + /// of your choice. + /// + /// # Errors + /// + /// Returns [`Closed`] without registering any participants if the phaser is closed. /// /// # Panics /// @@ -328,13 +336,18 @@ impl Phaser { /// a future target number: a number different from the current phase returns immediately. /// It neither registers a participant nor records an arrival. /// + /// Phase numbers wrap; do not retain an observation across a full `u64` cycle or use a number + /// obtained from another phaser. + /// + /// # Errors + /// /// Returns [`Closed`] if the observed phase is still current when the phaser closes. A phase - /// completed before the phaser was closed remains successful. Phase numbers wrap; do not retain - /// an observation across a full `u64` cycle or use a number obtained from another phaser. + /// completed before closure remains successful. /// /// # Cancel safety /// - /// Cancelling only unregisters this wait's waker. The same observation can be retried. + /// Dropping a pending future unregisters only this wait. It does not record an arrival or + /// change the phase; the same observation can be retried. pub async fn wait(&self, observed: u64) -> Result { PhaserWait { phaser: self, @@ -428,9 +441,13 @@ impl PhaserParticipant { /// Records this participant's arrival and remembers that phase for [`wait`](Self::wait). /// - /// Returns the phase in which arrival was recorded, or [`Closed`] without recording one. + /// Returns the phase in which arrival was recorded. /// Repeated calls within one phase count only once. After advancement, an explicit new call /// arrives in the new phase and replaces any previous pending observation. + /// + /// # Errors + /// + /// Returns [`Closed`] without recording an arrival if the phaser is closed. pub fn arrive(&mut self) -> Result { let mut state = self.phaser.state.lock(); if state.closed { @@ -453,6 +470,11 @@ impl PhaserParticipant { /// already completed. With no pending observation, the first poll arrives in the current /// phase. Success consumes the pending observation and returns the new current phase. /// + /// # Errors + /// + /// Returns [`Closed`] if the phaser closes before this call can record an arrival or before + /// its pending phase completes. A phase completed before closure remains successful. + /// /// # Cancel safety /// /// An unpolled call has no effect. Once polled, arrival is committed. Cancelling preserves @@ -468,11 +490,15 @@ impl PhaserParticipant { Ok(next) } - /// Withdraws this participant and returns the phase from which it withdrew, or [`Closed`]. + /// Withdraws this participant and returns the phase from which it withdrew. /// /// Any outstanding arrival is discharged without counting an already-arrived participant /// twice. This always removes the registration, even if the phaser is closed. The pending /// observation is abandoned. Dropping a participant has the same membership effect. + /// + /// # Errors + /// + /// Returns [`Closed`] if the phaser is closed. The registration is still removed. pub fn deregister(mut self) -> Result { self.do_deregister() } diff --git a/asyncband/src/pool/bounded.rs b/asyncband/src/pool/bounded.rs index 1a5f9de6..db574e22 100644 --- a/asyncband/src/pool/bounded.rs +++ b/asyncband/src/pool/bounded.rs @@ -210,8 +210,15 @@ impl Pool { /// Concurrent calls and checkouts can change the observed idle count while this method is /// running, so the target is the best effort rather than a postcondition. /// - /// Returns the number of objects created. If [`ManageObject::create`] fails, objects created by - /// this call before the failure remain in the pool and the error is returned. + /// # Returns + /// + /// Returns the number of objects created by this call, which may be less than the requested + /// target because of the capacity limit or concurrent checkouts. + /// + /// # Errors + /// + /// Returns an error from [`ManageObject::create`]. Objects created before the failure remain + /// in the pool. /// /// # Cancel safety /// @@ -266,13 +273,19 @@ impl Pool { /// object is returned to or detached from the pool. /// /// Idle objects are checked with [`ManageObject::is_recyclable`]. A failed check detaches the - /// object and retries checkout; its error is discarded. Only errors from - /// [`ManageObject::create`] are returned to the caller. + /// object and retries checkout; its error is discarded. + /// + /// # Errors + /// + /// Returns errors from [`ManageObject::create`]. A failed creation does not add an object to + /// the pool. /// /// # Cancel safety /// /// Cancelling while waiting for capacity or creating a new object restores the reserved pool - /// capacity. Cancelling while [`ManageObject::is_recyclable`] is checking an idle object + /// capacity and drops any in-progress creation future. Retrying does not retain a place in + /// the capacity queue. Objects already detached by failed recycling checks stay detached. + /// Cancelling while [`ManageObject::is_recyclable`] is checking an idle object /// follows [`PoolConfig::recycle_cancelled_strategy`]. pub async fn get(self: &Arc) -> Result, M::Error> { let permit = self.permits.clone().acquire_owned(1).await; @@ -321,9 +334,19 @@ impl Pool { /// Retains idle objects for which `f` returns `true`. /// - /// Checked-out objects are skipped and may return to the pool later. The predicate runs while - /// the pool is locked and must not call back into it; detachment hooks run after the lock is - /// released. + /// Checked-out objects are skipped and may return to the pool later. Detachment hooks run + /// after the lock is released. + /// + /// # Returns + /// + /// Returns the number of idle objects retained and the removed objects after their detachment + /// hooks have run. Checked-out objects are not included in the retained count. + /// + /// # Deadlocks + /// + /// The predicate runs while the pool is locked and must not call back into it. + /// + /// # Examples /// /// The following example starts a background task that runs every 30 seconds and removes /// objects from the pool that have not been used for more than one minute. The task will diff --git a/asyncband/src/pool/common.rs b/asyncband/src/pool/common.rs index 49fbb1d8..f5b02ee3 100644 --- a/asyncband/src/pool/common.rs +++ b/asyncband/src/pool/common.rs @@ -85,13 +85,32 @@ pub trait ManageObject: Send + Sync { type Error: Send; /// Creates a new object. + /// + /// # Errors + /// + /// Returns an implementation-defined error when an object cannot be created. Pool checkout + /// and replenishment propagate this error to their caller. + /// + /// # Cancel safety + /// + /// The pool may drop this future when checkout or replenishment is cancelled. Implementations + /// must arrange cleanup of resources owned by an unfinished creation attempt. fn create(&self) -> impl Future> + Send; - /// Whether the object `o` is recyclable. + /// Checks whether `o` can be reused for another checkout, refreshing it if needed. + /// + /// # Errors + /// + /// Returns an implementation-defined error when the object should not be reused. The pool + /// detaches the object and retries checkout; it does not return this error to the caller. + /// Record any required diagnostics in this implementation. + /// + /// # Cancel safety /// - /// Returns `Ok(())` if the object is recyclable. On error, the pool detaches the object and - /// retries checkout with another idle object or creates a replacement. The error is discarded - /// rather than returned by `Pool::get`; record any needed diagnostics in this implementation. + /// The pool may drop this future when checkout is cancelled. Mutations already made to `o` + /// are not rolled back. [`RecycleCancelledStrategy`] determines whether the object is detached + /// or returned to the idle queue; returning it requires the object to remain valid after an + /// interrupted check. fn is_recyclable( &self, o: &mut Self::Object, diff --git a/asyncband/src/pool/unbounded.rs b/asyncband/src/pool/unbounded.rs index 793e7860..c12c2114 100644 --- a/asyncband/src/pool/unbounded.rs +++ b/asyncband/src/pool/unbounded.rs @@ -282,6 +282,10 @@ impl Pool { /// [`ManageObject`] implementation, you should use [`Pool::get`] instead, and it will call /// [`ManageObject::create`] to create a new object if the pool is empty. /// + /// # Errors + /// + /// Returns the error from `f` if creation fails. No object is added to the pool on error. + /// /// # Cancel safety /// /// Cancelling while the provided future is pending leaves the pool unchanged. @@ -320,12 +324,17 @@ impl> Pool { /// If no idle object is available, this method calls [`ManageObject::create`]. /// /// Idle objects are checked with [`ManageObject::is_recyclable`]. A failed check detaches the - /// object and retries checkout; its error is discarded. Only errors from - /// [`ManageObject::create`] are returned to the caller. + /// object and retries checkout; its error is discarded. + /// + /// # Errors + /// + /// Returns errors from [`ManageObject::create`]. A failed creation does not add an object to + /// the pool. /// /// # Cancel safety /// - /// Cancelling while creating a new object leaves the pool unchanged. Cancelling while + /// Cancelling during creation drops the creation future without adding an object. Objects + /// already detached by failed recycling checks stay detached. Cancelling while /// [`ManageObject::is_recyclable`] is checking an idle object follows /// [`PoolConfig::recycle_cancelled_strategy`]. pub async fn get(self: &Arc) -> Result, M::Error> { @@ -389,6 +398,8 @@ impl> Pool { /// Extends the pool with the objects of an iterator. /// + /// # Deadlocks + /// /// The iterator is consumed while the pool is locked and must not call back into it. /// /// # Examples @@ -415,9 +426,19 @@ impl> Pool { /// Retains idle objects for which `f` returns `true`. /// - /// Checked-out objects are skipped and may return to the pool later. The predicate runs while - /// the pool is locked and must not call back into it; detachment hooks run after the lock is - /// released. + /// Checked-out objects are skipped and may return to the pool later. Detachment hooks run + /// after the lock is released. + /// + /// # Returns + /// + /// Returns the number of idle objects retained and the removed objects after their detachment + /// hooks have run. Checked-out objects are not included in the retained count. + /// + /// # Deadlocks + /// + /// The predicate runs while the pool is locked and must not call back into it. + /// + /// # Examples /// /// The following example starts a background task that runs every 30 seconds and removes /// objects from the pool that have not been used for more than one minute. The task will diff --git a/asyncband/src/rwlock/mapped_read_guard.rs b/asyncband/src/rwlock/mapped_read_guard.rs index 6d88c1b9..c84b4aa5 100644 --- a/asyncband/src/rwlock/mapped_read_guard.rs +++ b/asyncband/src/rwlock/mapped_read_guard.rs @@ -91,9 +91,13 @@ impl Deref for MappedRwLockReadGuard<'_, T> { impl<'a, T: ?Sized> MappedRwLockReadGuard<'a, T> { /// Selects a component while retaining the same lock access. /// - /// The closure runs while the original guard is held. If it panics, that guard is released. + /// The closure runs while the original guard is held. /// Call this as `MappedRwLockReadGuard::map(guard, f)` to avoid shadowing methods of the /// value. + /// + /// # Panics + /// + /// If `f` panics, unwinding drops the consumed guard and releases its lock access. pub fn map(orig: Self, f: F) -> MappedRwLockReadGuard<'a, U> where F: FnOnce(&T) -> &U, @@ -103,11 +107,18 @@ impl<'a, T: ?Sized> MappedRwLockReadGuard<'a, T> { MappedRwLockReadGuard::new(d, orig.access) } - /// Selects a component, or returns the still-held original guard when the closure returns - /// `None`. + /// Attempts to select a component while retaining the same lock access. + /// + /// Call this as `MappedRwLockReadGuard::filter_map(guard, f)`. + /// + /// # Errors + /// + /// Returns `Err(orig)` if `f` returns `None`. The original guard still holds its lock access; + /// no unlock occurs on this path. + /// + /// # Panics /// - /// A panic in the closure releases the guard. Call this as - /// `MappedRwLockReadGuard::filter_map(guard, f)`. + /// If `f` panics, unwinding drops the consumed guard and releases its lock access. pub fn filter_map(orig: Self, f: F) -> Result, Self> where F: FnOnce(&T) -> Option<&U>, diff --git a/asyncband/src/rwlock/mapped_write_guard.rs b/asyncband/src/rwlock/mapped_write_guard.rs index 20868de6..cd0dca3f 100644 --- a/asyncband/src/rwlock/mapped_write_guard.rs +++ b/asyncband/src/rwlock/mapped_write_guard.rs @@ -97,9 +97,14 @@ impl DerefMut for MappedRwLockWriteGuard<'_, T> { impl<'a, T: ?Sized> MappedRwLockWriteGuard<'a, T> { /// Selects a component while retaining the same lock access. /// - /// The closure runs while the original guard is held. If it panics, that guard is released. + /// The closure runs while the original guard is held. /// Call this as `MappedRwLockWriteGuard::map(guard, f)` to avoid shadowing methods of the /// value. + /// + /// # Panics + /// + /// If `f` panics, unwinding drops the consumed guard and releases its lock access. Changes + /// already made through the guard are not rolled back. pub fn map(mut orig: Self, f: F) -> MappedRwLockWriteGuard<'a, U> where F: FnOnce(&mut T) -> &mut U, @@ -109,11 +114,19 @@ impl<'a, T: ?Sized> MappedRwLockWriteGuard<'a, T> { MappedRwLockWriteGuard::new(d, orig.access) } - /// Selects a component, or returns the still-held original guard when the closure returns - /// `None`. + /// Attempts to select a component while retaining the same lock access. + /// + /// Call this as `MappedRwLockWriteGuard::filter_map(guard, f)`. + /// + /// # Errors + /// + /// Returns `Err(orig)` if `f` returns `None`. The original guard still holds its lock access; + /// no unlock occurs on this path. + /// + /// # Panics /// - /// A panic in the closure releases the guard. Call this as - /// `MappedRwLockWriteGuard::filter_map(guard, f)`. + /// If `f` panics, unwinding drops the consumed guard and releases its lock access. Changes + /// already made through the guard are not rolled back. pub fn filter_map(mut orig: Self, f: F) -> Result, Self> where F: FnOnce(&mut T) -> Option<&mut U>, diff --git a/asyncband/src/rwlock/owned_mapped_read_guard.rs b/asyncband/src/rwlock/owned_mapped_read_guard.rs index 11bd0964..a5ba9afd 100644 --- a/asyncband/src/rwlock/owned_mapped_read_guard.rs +++ b/asyncband/src/rwlock/owned_mapped_read_guard.rs @@ -90,9 +90,13 @@ impl Deref for OwnedMappedRwLockReadGuard { impl OwnedMappedRwLockReadGuard { /// Selects a component while retaining the same lock access. /// - /// The closure runs while the original guard is held. If it panics, that guard is released. + /// The closure runs while the original guard is held. /// Call this as `OwnedMappedRwLockReadGuard::map(guard, f)` to avoid shadowing methods of /// the value. + /// + /// # Panics + /// + /// If `f` panics, unwinding drops the consumed guard and releases its lock access. pub fn map(orig: Self, f: F) -> OwnedMappedRwLockReadGuard where F: FnOnce(&U) -> &V, @@ -102,11 +106,18 @@ impl OwnedMappedRwLockReadGuard { OwnedMappedRwLockReadGuard::new(d, orig.access) } - /// Selects a component, or returns the still-held original guard when the closure returns - /// `None`. + /// Attempts to select a component while retaining the same lock access. + /// + /// Call this as `OwnedMappedRwLockReadGuard::filter_map(guard, f)`. + /// + /// # Errors + /// + /// Returns `Err(orig)` if `f` returns `None`. The original guard still holds its lock access; + /// no unlock occurs on this path. + /// + /// # Panics /// - /// A panic in the closure releases the guard. Call this as - /// `OwnedMappedRwLockReadGuard::filter_map(guard, f)`. + /// If `f` panics, unwinding drops the consumed guard and releases its lock access. pub fn filter_map(orig: Self, f: F) -> Result, Self> where F: FnOnce(&U) -> Option<&V>, diff --git a/asyncband/src/rwlock/owned_mapped_write_guard.rs b/asyncband/src/rwlock/owned_mapped_write_guard.rs index 359c57cf..5ff196a0 100644 --- a/asyncband/src/rwlock/owned_mapped_write_guard.rs +++ b/asyncband/src/rwlock/owned_mapped_write_guard.rs @@ -99,9 +99,14 @@ impl DerefMut for OwnedMappedRwLockWriteGuard { impl OwnedMappedRwLockWriteGuard { /// Selects a component while retaining the same lock access. /// - /// The closure runs while the original guard is held. If it panics, that guard is released. + /// The closure runs while the original guard is held. /// Call this as `OwnedMappedRwLockWriteGuard::map(guard, f)` to avoid shadowing methods /// of the value. + /// + /// # Panics + /// + /// If `f` panics, unwinding drops the consumed guard and releases its lock access. Changes + /// already made through the guard are not rolled back. pub fn map(mut orig: Self, f: F) -> OwnedMappedRwLockWriteGuard where F: FnOnce(&mut U) -> &mut V, @@ -111,11 +116,19 @@ impl OwnedMappedRwLockWriteGuard { OwnedMappedRwLockWriteGuard::new(d, orig.access) } - /// Selects a component, or returns the still-held original guard when the closure returns - /// `None`. + /// Attempts to select a component while retaining the same lock access. + /// + /// Call this as `OwnedMappedRwLockWriteGuard::filter_map(guard, f)`. + /// + /// # Errors + /// + /// Returns `Err(orig)` if `f` returns `None`. The original guard still holds its lock access; + /// no unlock occurs on this path. + /// + /// # Panics /// - /// A panic in the closure releases the guard. Call this as - /// `OwnedMappedRwLockWriteGuard::filter_map(guard, f)`. + /// If `f` panics, unwinding drops the consumed guard and releases its lock access. Changes + /// already made through the guard are not rolled back. pub fn filter_map(mut orig: Self, f: F) -> Result, Self> where F: FnOnce(&mut U) -> Option<&mut V>, diff --git a/asyncband/src/rwlock/owned_read_guard.rs b/asyncband/src/rwlock/owned_read_guard.rs index 8eb4a62e..cad2ba4c 100644 --- a/asyncband/src/rwlock/owned_read_guard.rs +++ b/asyncband/src/rwlock/owned_read_guard.rs @@ -39,8 +39,8 @@ impl RwLock { /// /// # Cancel safety /// - /// Pending lock requests complete in order. Cancelling this future loses its place among them - /// and releases any reserved permits. + /// Dropping a pending future removes the request and returns any permits already assigned to + /// it. Retrying joins the back of the queue. The future also releases its owned `Arc`. pub async fn read_owned(self: Arc) -> OwnedRwLockReadGuard { self.s.acquire(1).await; OwnedRwLockReadGuard { @@ -94,9 +94,13 @@ impl Deref for OwnedRwLockReadGuard { impl OwnedRwLockReadGuard { /// Selects a component while retaining the same lock access. /// - /// The closure runs while the original guard is held. If it panics, that guard is released. + /// The closure runs while the original guard is held. /// Call this as `OwnedRwLockReadGuard::map(guard, f)` to avoid shadowing methods of the /// value. + /// + /// # Panics + /// + /// If `f` panics, unwinding drops the consumed guard and releases its lock access. pub fn map(orig: Self, f: F) -> OwnedMappedRwLockReadGuard where F: FnOnce(&T) -> &U, @@ -106,11 +110,18 @@ impl OwnedRwLockReadGuard { OwnedMappedRwLockReadGuard::new(d, orig.access) } - /// Selects a component, or returns the still-held original guard when the closure returns - /// `None`. + /// Attempts to select a component while retaining the same lock access. + /// + /// Call this as `OwnedRwLockReadGuard::filter_map(guard, f)`. + /// + /// # Errors + /// + /// Returns `Err(orig)` if `f` returns `None`. The original guard still holds its lock access; + /// no unlock occurs on this path. + /// + /// # Panics /// - /// A panic in the closure releases the guard. Call this as - /// `OwnedRwLockReadGuard::filter_map(guard, f)`. + /// If `f` panics, unwinding drops the consumed guard and releases its lock access. pub fn filter_map(orig: Self, f: F) -> Result, Self> where F: FnOnce(&T) -> Option<&U>, diff --git a/asyncband/src/rwlock/owned_write_guard.rs b/asyncband/src/rwlock/owned_write_guard.rs index 0838589e..eaaca0fc 100644 --- a/asyncband/src/rwlock/owned_write_guard.rs +++ b/asyncband/src/rwlock/owned_write_guard.rs @@ -40,8 +40,8 @@ impl RwLock { /// /// # Cancel safety /// - /// Pending lock requests complete in order. Cancelling this future loses its place among them - /// and releases any reserved permits. + /// Dropping a pending future removes the request and returns any permits already assigned to + /// it. Retrying joins the back of the queue. The future also releases its owned `Arc`. pub async fn write_owned(self: Arc) -> OwnedRwLockWriteGuard { self.s.acquire(self.max_readers).await; let permits_acquired = self.max_readers; @@ -105,9 +105,14 @@ impl DerefMut for OwnedRwLockWriteGuard { impl OwnedRwLockWriteGuard { /// Selects a component while retaining the same lock access. /// - /// The closure runs while the original guard is held. If it panics, that guard is released. + /// The closure runs while the original guard is held. /// Call this as `OwnedRwLockWriteGuard::map(guard, f)` to avoid shadowing methods of the /// value. + /// + /// # Panics + /// + /// If `f` panics, unwinding drops the consumed guard and releases its lock access. Changes + /// already made through the guard are not rolled back. pub fn map(mut orig: Self, f: F) -> OwnedMappedRwLockWriteGuard where F: FnOnce(&mut T) -> &mut U, @@ -117,11 +122,19 @@ impl OwnedRwLockWriteGuard { OwnedMappedRwLockWriteGuard::new(d, orig.access) } - /// Selects a component, or returns the still-held original guard when the closure returns - /// `None`. + /// Attempts to select a component while retaining the same lock access. + /// + /// Call this as `OwnedRwLockWriteGuard::filter_map(guard, f)`. + /// + /// # Errors + /// + /// Returns `Err(orig)` if `f` returns `None`. The original guard still holds its lock access; + /// no unlock occurs on this path. + /// + /// # Panics /// - /// A panic in the closure releases the guard. Call this as - /// `OwnedRwLockWriteGuard::filter_map(guard, f)`. + /// If `f` panics, unwinding drops the consumed guard and releases its lock access. Changes + /// already made through the guard are not rolled back. pub fn filter_map(mut orig: Self, f: F) -> Result, Self> where F: FnOnce(&mut T) -> Option<&mut U>, diff --git a/asyncband/src/rwlock/read_guard.rs b/asyncband/src/rwlock/read_guard.rs index 0afd2b0c..88d70c81 100644 --- a/asyncband/src/rwlock/read_guard.rs +++ b/asyncband/src/rwlock/read_guard.rs @@ -38,8 +38,8 @@ impl RwLock { /// /// # Cancel safety /// - /// Pending lock requests complete in order. Cancelling this future loses its place among them - /// and releases any reserved permits. + /// Dropping a pending future removes the request and returns any permits already assigned to + /// it. Retrying joins the back of the queue. pub async fn read(&self) -> RwLockReadGuard<'_, T> { self.s.acquire(1).await; RwLockReadGuard { @@ -95,8 +95,12 @@ impl Deref for RwLockReadGuard<'_, T> { impl<'a, T: ?Sized> RwLockReadGuard<'a, T> { /// Selects a component while retaining the same lock access. /// - /// The closure runs while the original guard is held. If it panics, that guard is released. + /// The closure runs while the original guard is held. /// Call this as `RwLockReadGuard::map(guard, f)` to avoid shadowing methods of the value. + /// + /// # Panics + /// + /// If `f` panics, unwinding drops the consumed guard and releases its lock access. pub fn map(orig: Self, f: F) -> MappedRwLockReadGuard<'a, U> where F: FnOnce(&T) -> &U, @@ -106,11 +110,18 @@ impl<'a, T: ?Sized> RwLockReadGuard<'a, T> { MappedRwLockReadGuard::new(d, orig.access.into_semaphore()) } - /// Selects a component, or returns the still-held original guard when the closure returns - /// `None`. + /// Attempts to select a component while retaining the same lock access. + /// + /// Call this as `RwLockReadGuard::filter_map(guard, f)`. + /// + /// # Errors + /// + /// Returns `Err(orig)` if `f` returns `None`. The original guard still holds its lock access; + /// no unlock occurs on this path. + /// + /// # Panics /// - /// A panic in the closure releases the guard. Call this as - /// `RwLockReadGuard::filter_map(guard, f)`. + /// If `f` panics, unwinding drops the consumed guard and releases its lock access. pub fn filter_map(orig: Self, f: F) -> Result, Self> where F: FnOnce(&T) -> Option<&U>, diff --git a/asyncband/src/rwlock/upgradable.rs b/asyncband/src/rwlock/upgradable.rs index d84ffb69..722fcd1e 100644 --- a/asyncband/src/rwlock/upgradable.rs +++ b/asyncband/src/rwlock/upgradable.rs @@ -68,7 +68,7 @@ use crate::rwlock::write_guard; /// queue position. [`upgrade`](RwLockUpgradableReadGuard::upgrade) consumes its guard; cancelling /// that future releases the guard as well, including before its first poll. /// -/// # Example +/// # Examples /// /// ``` /// use asyncband::rwlock::RwLockUpgradableReadGuard; @@ -285,6 +285,13 @@ impl<'a, T: ?Sized> RwLockUpgradableReadGuard<'a, T> { /// Failure neither consumes additional permits nor reserves a queue position. With a reader /// limit of one, this always succeeds. /// + /// # Errors + /// + /// Returns the original upgradable guard if exclusive access is unavailable. It retains its + /// reader slot and reservation; no additional permits are consumed. + /// + /// # Examples + /// /// ``` /// use asyncband::rwlock::RwLockUpgradableReadGuard; /// use asyncband::rwlock::UpgradableRwLock; diff --git a/asyncband/src/rwlock/write_guard.rs b/asyncband/src/rwlock/write_guard.rs index 27696654..a3c11a64 100644 --- a/asyncband/src/rwlock/write_guard.rs +++ b/asyncband/src/rwlock/write_guard.rs @@ -42,8 +42,8 @@ impl RwLock { /// /// # Cancel safety /// - /// Pending lock requests complete in order. Cancelling this future loses its place among them - /// and releases any reserved permits. + /// Dropping a pending future removes the request and returns any permits already assigned to + /// it. Retrying joins the back of the queue. pub async fn write(&self) -> RwLockWriteGuard<'_, T> { self.s.acquire(self.max_readers).await; RwLockWriteGuard { @@ -103,9 +103,14 @@ impl DerefMut for RwLockWriteGuard<'_, T> { impl<'a, T: ?Sized> RwLockWriteGuard<'a, T> { /// Selects a component while retaining the same lock access. /// - /// The closure runs while the original guard is held. If it panics, that guard is released. + /// The closure runs while the original guard is held. /// Call this as `RwLockWriteGuard::map(guard, f)` to avoid shadowing methods of the /// value. + /// + /// # Panics + /// + /// If `f` panics, unwinding drops the consumed guard and releases its lock access. Changes + /// already made through the guard are not rolled back. pub fn map(mut orig: Self, f: F) -> MappedRwLockWriteGuard<'a, U> where F: FnOnce(&mut T) -> &mut U, @@ -115,11 +120,19 @@ impl<'a, T: ?Sized> RwLockWriteGuard<'a, T> { MappedRwLockWriteGuard::new(d, orig.access.into_semaphore()) } - /// Selects a component, or returns the still-held original guard when the closure returns - /// `None`. + /// Attempts to select a component while retaining the same lock access. + /// + /// Call this as `RwLockWriteGuard::filter_map(guard, f)`. + /// + /// # Errors + /// + /// Returns `Err(orig)` if `f` returns `None`. The original guard still holds its lock access; + /// no unlock occurs on this path. + /// + /// # Panics /// - /// A panic in the closure releases the guard. Call this as - /// `RwLockWriteGuard::filter_map(guard, f)`. + /// If `f` panics, unwinding drops the consumed guard and releases its lock access. Changes + /// already made through the guard are not rolled back. pub fn filter_map(mut orig: Self, f: F) -> Result, Self> where F: FnOnce(&mut T) -> Option<&mut U>, diff --git a/asyncband/src/semaphore/mod.rs b/asyncband/src/semaphore/mod.rs index bd39f6bd..01bf503c 100644 --- a/asyncband/src/semaphore/mod.rs +++ b/asyncband/src/semaphore/mod.rs @@ -198,9 +198,13 @@ impl Semaphore { /// Dropping the returned [`SemaphorePermit`] returns its permits to this semaphore unless /// [`SemaphorePermit::forget`] consumes it first. /// + /// Pending acquisitions are served in queue order. A larger request can hold up smaller + /// requests behind it until enough permits become available. + /// /// # Cancel safety /// - /// Pending acquisitions complete in order. Cancelling this call loses its place among them. + /// Dropping a pending future removes the request and returns any permits already assigned to + /// it. Retrying joins the back of the queue. /// /// # Examples /// @@ -267,9 +271,13 @@ impl Semaphore { /// Dropping the returned [`OwnedSemaphorePermit`] releases both the permits and its reference /// to the semaphore unless [`OwnedSemaphorePermit::forget`] consumes it first. /// + /// Pending acquisitions are served in queue order. A larger request can hold up smaller + /// requests behind it until enough permits become available. + /// /// # Cancel safety /// - /// Pending acquisitions complete in order. Cancelling this call loses its place among them. + /// Dropping a pending future removes the request and returns any permits already assigned to + /// it. Retrying joins the back of the queue. The future also releases its owned `Arc`. /// /// # Examples /// diff --git a/asyncband/src/shutdown/mod.rs b/asyncband/src/shutdown/mod.rs index 8dcaab22..60fa8de1 100644 --- a/asyncband/src/shutdown/mod.rs +++ b/asyncband/src/shutdown/mod.rs @@ -90,15 +90,16 @@ pub fn new() -> (Shutdown, ShutdownGuard) { (shutdown, guard) } -/// Coordinates a graceful shutdown request and completion. +/// A future that requests shutdown and waits for all completion guards to be dropped. /// -/// Awaiting this handle requests shutdown and waits for every [`ShutdownGuard`] to be dropped. The -/// request is issued on the first poll. Merely creating, moving, or dropping an unpolled handle -/// does not request shutdown. +/// # Cancel safety /// -/// Once the handle has been polled, the shutdown request is sticky even if the future is cancelled -/// or dropped. If shutdown must be requested before a `select` can choose another branch, call -/// [`request_shutdown`](Self::request_shutdown) before entering the `select`: +/// The first poll requests shutdown. Dropping an unpolled future does not request it; dropping +/// a pending future after its first poll does not withdraw the request or drop completion +/// guards. Call [`request_shutdown`](Self::request_shutdown) before selecting when the request +/// must happen even if this future is never polled. +/// +/// # Examples /// /// ``` /// # #[tokio::main] @@ -187,13 +188,23 @@ impl ShutdownGuard { } /// Waits until shutdown is requested. + /// + /// # Cancel safety + /// + /// Dropping a pending future removes only this wait. It does not request shutdown, clear an + /// existing request, or drop the handle borrowed by this call. pub async fn shutdown_requested(&self) { self.latch.wait().await; } - /// Returns a future that resolves when shutdown is requested. + /// Returns a future that observes a shutdown request without borrowing this handle. + /// + /// The future retains the request state but does not keep shutdown completion pending. + /// + /// # Cancel safety /// - /// The returned future can be moved into a spawned task. + /// Dropping the future removes only this wait. It does not request shutdown or clear an + /// existing request. pub fn shutdown_requested_owned(&self) -> impl Future + 'static { self.latch.clone().wait_owned() } @@ -214,13 +225,23 @@ impl ShutdownWatch { } /// Waits until shutdown is requested. + /// + /// # Cancel safety + /// + /// Dropping a pending future removes only this wait. It does not request shutdown, clear an + /// existing request, or drop the handle borrowed by this call. pub async fn shutdown_requested(&self) { self.latch.wait().await; } - /// Returns an owned future that resolves when shutdown is requested. + /// Returns a future that observes a shutdown request without borrowing this handle. + /// + /// The future retains the request state but does not keep shutdown completion pending. + /// + /// # Cancel safety /// - /// The returned future has no lifetime constraints. + /// Dropping the future removes only this wait. It does not request shutdown or clear an + /// existing request. pub fn shutdown_requested_owned(&self) -> impl Future + 'static { self.latch.clone().wait_owned() } diff --git a/asyncband/src/singleflight/mod.rs b/asyncband/src/singleflight/mod.rs index 91763df9..c748e0fe 100644 --- a/asyncband/src/singleflight/mod.rs +++ b/asyncband/src/singleflight/mod.rs @@ -228,24 +228,28 @@ where } } - /// Executes and returns the results of the given function, making sure that only one execution - /// is in-flight for a given key at a time. + /// Returns a clone of the shared computation's value for `key`. /// - /// If a duplicate comes in, the duplicate caller waits for the original to complete and - /// receives the same results. + /// Callers that join the same in-flight entry share its successful result. Once `func` + /// completes successfully, the entry is removed so later calls can start new work. + /// [`forget`](Self::forget) lets new calls start independently of an existing entry. /// - /// If the computation is cancelled or panics, another caller waiting for the same key may retry - /// it. + /// # Cancel safety /// - /// Once the function completes, the key, if not [`forgotten`], is removed from the group, - /// allowing future calls with the same key to execute the function again. + /// Dropping a caller that is waiting for a result does not cancel the active computation. + /// Dropping the caller executing `func` drops that attempt; another caller on the entry may + /// run its own `func`. Effects already performed by the cancelled attempt are not rolled back. /// - /// [`forgotten`]: Self::forget + /// # Panics + /// + /// A panic in `func` propagates to its caller without publishing a value. Another caller may + /// retry the computation. /// /// # Deadlocks /// - /// The function must not recursively call `work` or `try_work` for the same current key because - /// it would wait for its own result. Work for other keys remains independent. + /// The computation must not await `work` or `try_work` for an equivalent key on + /// the same current entry, because it would wait for its own result. Other keys are + /// independent. /// /// # Examples /// @@ -303,24 +307,33 @@ where result } - /// Executes and returns the results of the given function, making sure that only one execution - /// is in-flight for a given key at a time. + /// Returns a clone of the shared computation's value for `key`. + /// + /// Callers that join the same in-flight entry share its successful result. Once `func` + /// completes successfully, the entry is removed so later calls can start new work. + /// [`forget`](Self::forget) lets new calls start independently of an existing entry. + /// + /// # Errors + /// + /// Returns an error from this call's `func` only to this caller. Errors are not stored or + /// shared; another caller waiting on the entry may run its own `func`. /// - /// If a duplicate comes in, the duplicate caller waits for the original to complete and - /// receives the same results. + /// # Cancel safety /// - /// If the computation returns an error, it is returned to that caller. After an error, - /// cancellation, or panic, another caller may retry the computation. + /// Dropping a caller that is waiting for a result does not cancel the active computation. + /// Dropping the caller executing `func` drops that attempt; another caller on the entry may + /// run its own `func`. Effects already performed by the cancelled attempt are not rolled back. /// - /// Once the function completes successfully, the key, if not [`forgotten`], is removed from - /// the group, allowing future calls with the same key to execute the function again. + /// # Panics /// - /// [`forgotten`]: Self::forget + /// A panic in `func` propagates to its caller without publishing a value. Another caller may + /// retry the computation. /// /// # Deadlocks /// - /// The function must not recursively call `work` or `try_work` for the same current key because - /// it would wait for its own result. Work for other keys remains independent. + /// The computation must not await `work` or `try_work` for an equivalent key on + /// the same current entry, because it would wait for its own result. Other keys are + /// independent. /// /// # Examples /// diff --git a/asyncband/src/spmc/bounded.rs b/asyncband/src/spmc/bounded.rs index fad902a3..99766fce 100644 --- a/asyncband/src/spmc/bounded.rs +++ b/asyncband/src/spmc/bounded.rs @@ -69,9 +69,11 @@ impl Drop for BoundedSender { } impl BoundedSender { - /// Sends a value, waiting until capacity is available if the queue is full. + /// Sends a value, waiting for capacity when the queue is full. /// - /// If all receivers have been dropped, the value is returned in [`SendError`]. + /// # Errors + /// + /// Returns [`SendError`] containing the unsent value if all receivers have been dropped. /// /// # Cancel safety /// @@ -84,10 +86,13 @@ impl BoundedSender { self.shared.send(value).await } - /// Attempts to send a value without waiting for capacity. + /// Sends a value without waiting for capacity. + /// + /// # Errors /// - /// Returns [`TrySendError::Full`] when the queue has reached its exact capacity and - /// [`TrySendError::Disconnected`] when all receivers have been dropped. + /// Returns [`TrySendError::Full`] if capacity is unavailable, or + /// [`TrySendError::Disconnected`] if all receivers have been dropped. Both errors contain the + /// unsent value; nothing is enqueued on error. pub fn try_send(&mut self, value: T) -> Result<(), TrySendError> { self.shared.try_send(value) } @@ -123,23 +128,29 @@ impl Drop for BoundedReceiver { } impl BoundedReceiver { - /// Receives the next available value. + /// Waits for and receives the next queued value, freeing one capacity slot. + /// + /// Queued values remain receivable after disconnection. /// - /// Buffered values remain available after the sender is dropped. Once they are drained, - /// this method returns [`RecvError::Disconnected`]. + /// # Errors + /// + /// Returns [`RecvError::Disconnected`] once the sender has been dropped and the queue is empty. /// /// # Cancel safety /// - /// Dropping a pending `recv` future does not consume a value. Any unconsumed notification is - /// passed to another waiting task, so cancellation does not prevent it from receiving. + /// Dropping a pending future does not remove a value from the queue or prevent another receiver + /// from receiving it. pub async fn recv(&self) -> Result { self.shared.recv().await } - /// Attempts to receive the next available value without waiting for a message. + /// Receives the next queued value without waiting for a message, freeing one capacity slot. + /// + /// # Errors /// - /// Returns [`TryRecvError::Empty`] while the queue is empty and a sender remains, or - /// [`TryRecvError::Disconnected`] once the queue is empty and the sender has been dropped. + /// Returns [`TryRecvError::Empty`] when the queue is empty but the sender is alive. + /// Returns [`TryRecvError::Disconnected`] once the sender has been dropped and the queue is + /// empty. pub fn try_recv(&self) -> Result { self.shared.try_recv() } diff --git a/asyncband/src/spmc/error.rs b/asyncband/src/spmc/error.rs index 09a680dc..82951bd3 100644 --- a/asyncband/src/spmc/error.rs +++ b/asyncband/src/spmc/error.rs @@ -26,7 +26,7 @@ use std::fmt; pub struct SendError(T); impl SendError { - /// Gets a reference to the value that failed to be sent. + /// Returns a reference to the value that failed to be sent. pub fn as_inner(&self) -> &T { &self.0 } @@ -65,7 +65,7 @@ pub enum TrySendError { } impl TrySendError { - /// Gets a reference to the value that failed to be sent. + /// Returns a reference to the value that failed to be sent. pub fn as_inner(&self) -> &T { match self { TrySendError::Full(value) | TrySendError::Disconnected(value) => value, diff --git a/asyncband/src/spmc/unbounded.rs b/asyncband/src/spmc/unbounded.rs index b5bb0b4b..a08d522a 100644 --- a/asyncband/src/spmc/unbounded.rs +++ b/asyncband/src/spmc/unbounded.rs @@ -62,9 +62,13 @@ impl Drop for UnboundedSender { } impl UnboundedSender { - /// Sends a value without waiting for capacity. + /// Enqueues a value without waiting for capacity. /// - /// If all receivers have been dropped, the value is returned in [`SendError`]. + /// Success does not guarantee that a receiver will consume the value before being dropped. + /// + /// # Errors + /// + /// Returns [`SendError`] containing the unsent value if all receivers have been dropped. pub fn send(&mut self, value: T) -> Result<(), SendError> { match self.shared.try_send(value) { Ok(()) => Ok(()), @@ -104,23 +108,29 @@ impl Drop for UnboundedReceiver { } impl UnboundedReceiver { - /// Receives the next available value. + /// Waits for and receives the next queued value. + /// + /// Queued values remain receivable after disconnection. /// - /// Buffered values remain available after the sender is dropped. Once they are drained, - /// this method returns [`RecvError::Disconnected`]. + /// # Errors + /// + /// Returns [`RecvError::Disconnected`] once the sender has been dropped and the queue is empty. /// /// # Cancel safety /// - /// Dropping a pending `recv` future does not consume a value. Any unconsumed notification is - /// passed to another waiting task, so cancellation does not prevent it from receiving. + /// Dropping a pending future does not remove a value from the queue or prevent another receiver + /// from receiving it. pub async fn recv(&self) -> Result { self.shared.recv().await } - /// Attempts to receive the next available value without waiting for a message. + /// Receives the next queued value without waiting for a message. + /// + /// # Errors /// - /// Returns [`TryRecvError::Empty`] while the queue is empty and a sender remains, or - /// [`TryRecvError::Disconnected`] once the queue is empty and the sender has been dropped. + /// Returns [`TryRecvError::Empty`] when the queue is empty but the sender is alive. + /// Returns [`TryRecvError::Disconnected`] once the sender has been dropped and the queue is + /// empty. pub fn try_recv(&self) -> Result { self.shared.try_recv() } diff --git a/asyncband/src/waitgroup/mod.rs b/asyncband/src/waitgroup/mod.rs index 4fb7c403..f48a0d31 100644 --- a/asyncband/src/waitgroup/mod.rs +++ b/asyncband/src/waitgroup/mod.rs @@ -212,7 +212,12 @@ impl IntoFuture for WaitGroup { type Output = (); type IntoFuture = Wait; - /// Marks this participant complete and waits for every other participant to complete. + /// Marks this participant complete and returns a future observing group completion. + /// + /// # Cancel safety + /// + /// Conversion marks this participant complete immediately. Dropping the returned future does + /// not undo that completion; see [`Wait`] for observer cancellation. fn into_future(mut self) -> Self::IntoFuture { let state = self.state.take().expect("a live WaitGroup owns its state"); state.release_handle(); @@ -222,10 +227,13 @@ impl IntoFuture for WaitGroup { /// A future that completes when every [`WaitGroup`] participant has completed. /// -/// Converting a [`WaitGroup`] into this future marks that handle's participant complete. Cloning a -/// `Wait` creates another observer without registering a participant. Dropping a pending `Wait` -/// only unregisters that observer; the participant remains complete and other observers are not -/// affected. +/// Converting a [`WaitGroup`] into this future immediately marks that participant complete, +/// even before the future is polled. Cloning `Wait` adds an observer without a participant. +/// +/// # Cancel safety +/// +/// Dropping a pending future unregisters only that observer. The participant remains complete +/// and other observers are unaffected. #[must_use = "futures do nothing unless you `.await` or poll them"] pub struct Wait { token: Option, @@ -236,6 +244,10 @@ impl Clone for Wait { /// Creates a new future that observes the same group completion. /// /// This does not register another participant. + /// + /// # Cancel safety + /// + /// Dropping the returned future unregisters only that observer; see [`Wait`]. fn clone(&self) -> Self { Wait { token: None, diff --git a/asyncband/src/watch/mod.rs b/asyncband/src/watch/mod.rs index cc8d40e6..57020256 100644 --- a/asyncband/src/watch/mod.rs +++ b/asyncband/src/watch/mod.rs @@ -158,9 +158,12 @@ impl Drop for Sender { } impl Sender { - /// Publishes a new current value. + /// Publishes `value` as the new current value and notifies receivers. /// - /// If no receivers remain, the value is returned and the retained value is left unchanged. + /// # Errors + /// + /// Returns [`SendError`] containing `value` if no receivers remain. The retained value is + /// unchanged on error. /// /// # Panics /// @@ -263,9 +266,12 @@ impl Drop for Receiver { impl Receiver { /// Returns a clone of the current value without marking its version observed. /// - /// The clone is created while publication is locked. Keep `T::clone` inexpensive and avoid - /// reentering this channel from the clone implementation. Use `T = Arc` when the underlying - /// state is expensive to clone. + /// Use `T = Arc` when the underlying state is expensive to clone. + /// + /// # Deadlocks + /// + /// The clone is created while publication is locked. The clone implementation must not reenter + /// this channel. pub fn get(&self) -> T where T: Clone, @@ -275,8 +281,14 @@ impl Receiver { /// Returns whether a version newer than the last observed version exists. /// - /// An unseen final version is reported before disconnection, even if all senders have been - /// dropped. + /// An unseen final version returns `Ok(true)` even after all senders have been dropped. + /// `Ok(false)` is a snapshot: a sender may publish a new version immediately afterward. + /// This call does not mark a version observed. + /// + /// # Errors + /// + /// Returns [`RecvError::Disconnected`] when all senders have been dropped and no unseen + /// version remains. pub fn has_changed(&self) -> Result { let state = self.shared.state.lock(); if state.version != self.seen { @@ -288,11 +300,20 @@ impl Receiver { } } - /// Waits for a newer version and marks the latest version observed. + /// Waits for a newer version and marks it observed. + /// + /// Intermediate updates may be coalesced. An unseen final version remains available after all + /// senders have been dropped. Use [`recv`](Self::recv) when the value is needed. + /// + /// # Errors + /// + /// Returns [`RecvError::Disconnected`] when all senders have been dropped and no unseen + /// version remains. + /// + /// # Cancel safety /// - /// Intermediate updates may be coalesced. This method is cancel safe: until it returns, no - /// version is marked observed by the call. Use [`Receiver::recv`] instead when the value is - /// needed. + /// Dropping a pending future leaves the observed version unchanged. A later call can still + /// observe the latest unseen version. pub async fn changed(&mut self) -> Result<(), RecvError> { let seen = Change { shared: &self.shared, @@ -306,15 +327,29 @@ impl Receiver { /// Waits for a newer version, returns a clone of its latest value, and marks it observed. /// - /// Intermediate updates may be coalesced. The returned value and the receiver's observed - /// version are updated together, so a concurrent publication cannot cause the returned value - /// to be received twice. This method is cancel safe: until it returns, no version is marked - /// observed by the call. + /// Intermediate updates may be coalesced. An unseen final version remains available after all + /// senders have been dropped. + /// The returned value and observed version correspond to the same publication. Use `T = Arc` + /// when cloning the underlying value is expensive. + /// + /// # Errors + /// + /// Returns [`RecvError::Disconnected`] when all senders have been dropped and no unseen + /// version remains. + /// + /// # Cancel safety + /// + /// Dropping a pending future leaves the observed version unchanged. A later call can still + /// observe the latest unseen version. + /// + /// # Panics + /// + /// If `T::clone` panics, the version remains unseen and may be received by a later call. + /// + /// # Deadlocks /// - /// The clone is created while publication is locked. Keep `T::clone` inexpensive and avoid - /// reentering this channel from the clone implementation. Use `T = Arc` when the underlying - /// state is expensive to clone. If cloning panics, the version remains unseen and may be - /// received by a later call. + /// Cloning runs while publication is locked. The clone implementation must not reenter this + /// channel. pub async fn recv(&mut self) -> Result where T: Clone, From c66d3a76978af753187feacbd677b6744acf3d03 Mon Sep 17 00:00:00 2001 From: tison Date: Fri, 13 Nov 2026 16:27:06 +0800 Subject: [PATCH 4/4] docs: use Cancellation headings and rewrite remaining upstream phrasing Replace the Cargo/Tokio-style '# Cancel safety' heading with '# Cancellation' across AGENTS.md and crate docs, rewrite remaining verbatim or closely adapted upstream comments and examples in singleflight, once_map, once, barrier, broadcast, mutex, semaphore, and oneshot, fix misleading/second-person phrasing and US spelling inconsistencies, and drop unqualified cancellation-safety claims. --- AGENTS.md | 4 +- asyncband/src/barrier/mod.rs | 49 ++- asyncband/src/blocking/mod.rs | 2 +- asyncband/src/broadcast/mpmc/bounded/mod.rs | 139 +++---- asyncband/src/broadcast/mpmc/unbounded/mod.rs | 17 +- asyncband/src/completion/mod.rs | 2 +- asyncband/src/condvar/mod.rs | 8 +- asyncband/src/event/auto_reset.rs | 4 +- asyncband/src/event/manual_reset.rs | 4 +- asyncband/src/latch/mod.rs | 4 +- asyncband/src/mpmc/bounded.rs | 6 +- asyncband/src/mpmc/unbounded.rs | 2 +- asyncband/src/mpsc/bounded/receiver.rs | 2 +- asyncband/src/mpsc/bounded/sender.rs | 4 +- asyncband/src/mpsc/unbounded/receiver.rs | 2 +- asyncband/src/mutex/mod.rs | 348 +++++++----------- asyncband/src/once/lazy_cell/mod.rs | 8 +- asyncband/src/once/once/mod.rs | 56 +-- asyncband/src/once/once_cell/mod.rs | 12 +- asyncband/src/once/once_map/mod.rs | 29 +- asyncband/src/oneshot/receiver.rs | 75 ++-- asyncband/src/oneshot/sender.rs | 120 +++--- asyncband/src/phaser/mod.rs | 7 +- asyncband/src/pool/bounded.rs | 4 +- asyncband/src/pool/common.rs | 4 +- asyncband/src/pool/mod.rs | 3 +- asyncband/src/pool/unbounded.rs | 13 +- asyncband/src/rwlock/owned_read_guard.rs | 2 +- asyncband/src/rwlock/owned_write_guard.rs | 2 +- asyncband/src/rwlock/read_guard.rs | 2 +- asyncband/src/rwlock/upgradable.rs | 8 +- asyncband/src/rwlock/write_guard.rs | 2 +- asyncband/src/semaphore/mod.rs | 21 +- asyncband/src/shutdown/mod.rs | 14 +- asyncband/src/singleflight/mod.rs | 138 ++++--- asyncband/src/spmc/bounded.rs | 4 +- asyncband/src/spmc/unbounded.rs | 2 +- asyncband/src/waitgroup/mod.rs | 6 +- asyncband/src/watch/mod.rs | 4 +- 39 files changed, 525 insertions(+), 608 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 86fc6020..e25e5b95 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -52,9 +52,9 @@ Keep each Markdown prose paragraph and list item on one source line. Format Mark Write public API documentation from Asyncband's observable contracts. Start with a short action or query summary, then describe waiting conditions, ordering, ownership, and state changes relevant to callers. Keep implementation details out unless callers need them to use the API correctly. -Use applicable sections in this order: `Returns`, `Errors`, `Cancel safety`, `Panics`, `Deadlocks`, and `Examples`. Omit empty or redundant sections. Explain simple return values in the summary or body; reserve `Returns` for nontrivial outcomes, snapshots, or resource ownership. Document `Option::None` and boolean outcomes as return semantics, not errors. For `Result`, use `Errors` to explain failure conditions, returned ownership, and any state changes. Keep examples last and focused on useful behavior rather than mechanically demonstrating syntax. +Use applicable sections in this order: `Returns`, `Errors`, `Cancellation`, `Panics`, `Deadlocks`, and `Examples`. Omit empty or redundant sections. Explain simple return values in the summary or body; reserve `Returns` for nontrivial outcomes, snapshots, or resource ownership. Document `Option::None` and boolean outcomes as return semantics, not errors. For `Result`, use `Errors` to explain failure conditions, returned ownership, and any state changes. Keep examples last and focused on useful behavior rather than mechanically demonstrating syntax. -Document cancellation for public async operations, methods returning futures, and public `Future` or `IntoFuture` types. Explain when effects occur, what dropping a pending future retains or releases, and whether retrying restarts, resumes, or repeats an effect. Distinguish cancellation of active work from cancellation of an observer, and state when caller-owned inputs are dropped or external side effects are not rolled back. Do not use an unqualified cancellation-safety claim in place of these observable guarantees. Owned variants should include a `Cancel safety` section and may link to shared semantics while explaining ownership differences. +Document cancellation for public async operations, methods returning futures, and public `Future` or `IntoFuture` types. Explain when effects occur, what dropping a pending future retains or releases, and whether retrying restarts, resumes, or repeats an effect. Distinguish cancellation of active work from cancellation of an observer, and state when caller-owned inputs are dropped or external side effects are not rolled back. Do not use an unqualified cancellation-safety claim in place of these observable guarantees. Owned variants should include a `Cancellation` section and may link to shared semantics while explaining ownership differences. Put concrete panic conditions and important unwind effects in `Panics`, and recursive acquisition or callback reentrancy hazards in `Deadlocks`. Do not enumerate arbitrary panics in caller-provided code unless their effect on the primitive matters. Apply the same conventions to public trait methods. Preserve existing provenance and license notices independently of documentation rewrites. diff --git a/asyncband/src/barrier/mod.rs b/asyncband/src/barrier/mod.rs index 889ec4d5..7e33fb26 100644 --- a/asyncband/src/barrier/mod.rs +++ b/asyncband/src/barrier/mod.rs @@ -27,23 +27,34 @@ //! # #[tokio::main] //! # async fn main() { //! use std::sync::Arc; +//! use std::sync::atomic::AtomicUsize; +//! use std::sync::atomic::Ordering; //! //! use asyncband::barrier::Barrier; //! //! let barrier = Arc::new(Barrier::new(3)); -//! let mut tasks = vec![]; +//! let stage = Arc::new([const { AtomicUsize::new(0) }; 3]); +//! let mut workers = Vec::with_capacity(3); //! -//! for _ in 0..3 { +//! for slot in 0..3 { //! let barrier = barrier.clone(); -//! let task = tokio::spawn(async move { barrier.wait().await.is_leader() }); -//! tasks.push(task); +//! let stage = stage.clone(); +//! workers.push(tokio::spawn(async move { +//! stage[slot].store(slot + 1, Ordering::Release); +//! let result = barrier.wait().await; +//! let sum: usize = stage.iter().map(|v| v.load(Ordering::Acquire)).sum(); +//! assert_eq!(sum, 6); +//! result.is_leader() +//! })); //! } //! -//! let mut leaders = 0; -//! for task in tasks { -//! leaders += usize::from(task.await.unwrap()); +//! let mut leader_count = 0; +//! for worker in workers { +//! if worker.await.unwrap() { +//! leader_count += 1; +//! } //! } -//! assert_eq!(leaders, 1); +//! assert_eq!(leader_count, 1); //! # } //! ``` @@ -60,7 +71,7 @@ use crate::internal::wakerset::WakerToken; #[cfg(test)] mod tests; -/// A synchronization primitive for multiple tasks that need to wait for each other. +/// A reusable rendezvous point that releases tasks in fixed-size generations. /// /// See the [module level documentation](self) for more. #[derive(Debug)] @@ -150,29 +161,29 @@ impl Barrier { } } - /// Waits for all tasks to reach this point. + /// Arrives at the barrier and waits until all `n` participants in the current generation have + /// arrived. /// - /// The barrier holds the current task until all `n` participants have arrived. The final - /// participant is designated as the leader for this generation. + /// The final participant to arrive completes the generation, wakes the waiting participants, + /// and is designated as that generation's leader. /// /// # Returns /// /// Returns a [`BarrierWaitResult`] identifying whether this call completed the generation /// and was selected as its leader. /// - /// # Cancel safety - /// - /// This method is not cancellation safe. + /// # Cancellation /// /// An arrival is recorded when the future returned by `wait` is first polled. Once recorded, /// the arrival is not retracted if that future is dropped before the barrier completes. /// Cancellation therefore stops that caller from observing completion, but it does not make the /// caller leave the current barrier generation. /// - /// Calling `wait` again after canceling a pending call records another arrival. Repeatedly - /// canceling and retrying can therefore cause a generation to complete with fewer than `n` live - /// wait futures. Callers that need to keep waiting after another operation completes first - /// should retain and continue polling the same `wait` future instead of creating a new one. + /// Calling `wait` again after cancelling a pending call records another arrival. Repeatedly + /// cancelling and retrying can therefore cause a generation to complete with fewer than `n` + /// live wait futures. Callers that need to keep waiting after another operation completes + /// first should retain and continue polling the same `wait` future instead of creating a + /// new one. /// /// # Examples /// diff --git a/asyncband/src/blocking/mod.rs b/asyncband/src/blocking/mod.rs index 50363ec9..033c7257 100644 --- a/asyncband/src/blocking/mod.rs +++ b/asyncband/src/blocking/mod.rs @@ -98,7 +98,7 @@ pub trait FutureExt: IntoFuture { /// Returns `Some(output)` if the future completes, or `None` when the timeout is observed /// while the future is pending. /// - /// # Cancel safety + /// # Cancellation /// /// On timeout, the supplied future is dropped. Its own cancellation contract determines which /// effects remain. Passing a mutable reference to a retained future allows that future to be diff --git a/asyncband/src/broadcast/mpmc/bounded/mod.rs b/asyncband/src/broadcast/mpmc/bounded/mod.rs index d0d33701..bba2d771 100644 --- a/asyncband/src/broadcast/mpmc/bounded/mod.rs +++ b/asyncband/src/broadcast/mpmc/bounded/mod.rs @@ -15,11 +15,11 @@ // specific language governing permissions and limitations // under the License. -//! A multi-producer multi-consumer broadcast channel with a bounded buffer. +//! A capacity-limited fan-out channel with multiple senders and receivers. //! -//! This channel supports multiple senders and multiple receivers. Each message sent by any sender -//! is received by all active receivers. Nothing is ever displaced to make room, so receiving never -//! reports lag; instead the channel retains at most `capacity` messages and makes producers wait. +//! Publishing delivers every accepted value to every receiver subscribed at that moment. Older +//! values are never overwritten to make room, so receiving never reports lag; once the shared +//! backlog reaches `capacity`, producers wait for the slowest active receiver. //! //! # Capacity //! @@ -60,16 +60,16 @@ //! //! # #[tokio::main] //! # async fn main() { -//! let (tx, mut rx1) = mpmc::bounded(4); -//! let mut rx2 = tx.subscribe(); +//! let (publisher, mut audit) = mpmc::bounded(4); +//! let mut metrics = publisher.subscribe(); //! -//! tx.send(10).await; -//! tx.send(20).await; +//! publisher.send("started").await; +//! publisher.send("finished").await; //! -//! assert_eq!(*rx1.recv().await.unwrap(), 10); -//! assert_eq!(*rx1.recv().await.unwrap(), 20); -//! assert_eq!(*rx2.recv().await.unwrap(), 10); -//! assert_eq!(*rx2.recv().await.unwrap(), 20); +//! assert_eq!(*audit.recv().await.unwrap(), "started"); +//! assert_eq!(*audit.recv().await.unwrap(), "finished"); +//! assert_eq!(*metrics.recv().await.unwrap(), "started"); +//! assert_eq!(*metrics.recv().await.unwrap(), "finished"); //! # } //! ``` //! @@ -225,7 +225,7 @@ impl Shared { /// The sending side of a bounded broadcast channel. /// -/// The sender can be cloned to create multiple producers. Dropping the final sender disconnects +/// Cloning this handle adds another producer. Dropping the final sender disconnects /// the channel. Each receiver may drain its own buffered messages before observing disconnection. pub struct BoundedSender { shared: Arc>, @@ -266,7 +266,7 @@ impl BoundedSender { /// If no receivers are active, the message is dropped immediately and this returns without /// waiting. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping a pending future does not publish the value and drops the unsent input. Publication /// is indivisible: it cannot leave a reserved but unfilled position in the message order. @@ -283,9 +283,9 @@ impl BoundedSender { /// /// # #[tokio::main] /// # async fn main() { - /// let (tx, mut rx) = mpmc::bounded(1); - /// tx.send(10).await; - /// assert_eq!(*rx.recv().await.unwrap(), 10); + /// let (publisher, mut receiver) = mpmc::bounded(1); + /// publisher.send("ready").await; + /// assert_eq!(*receiver.recv().await.unwrap(), "ready"); /// # } /// ``` pub async fn send(&self, value: T) { @@ -379,12 +379,15 @@ impl BoundedSender { /// use asyncband::broadcast::mpmc; /// use asyncband::broadcast::mpmc::TrySendError; /// - /// let (tx, mut rx) = mpmc::bounded(1); - /// tx.try_send(10).unwrap(); - /// assert_eq!(tx.try_send(20), Err(TrySendError::Full(20))); + /// let (publisher, mut receiver) = mpmc::bounded(1); + /// publisher.try_send("first").unwrap(); + /// assert_eq!( + /// publisher.try_send("second"), + /// Err(TrySendError::Full("second")) + /// ); /// - /// assert_eq!(*rx.try_recv().unwrap(), 10); - /// tx.try_send(20).unwrap(); + /// assert_eq!(*receiver.try_recv().unwrap(), "first"); + /// publisher.try_send("second").unwrap(); /// ``` pub fn try_send(&self, value: T) -> Result<(), TrySendError> { // `Arc::new` runs inside the critical section, but only after the capacity check, so a @@ -444,12 +447,12 @@ impl BoundedSender { /// ``` /// use asyncband::broadcast::mpmc; /// - /// let (tx, mut rx) = mpmc::bounded(4); - /// tx.try_send(10).unwrap(); - /// assert_eq!(tx.retained_message_count(), 1); + /// let (publisher, mut receiver) = mpmc::bounded(4); + /// publisher.try_send("pending").unwrap(); + /// assert_eq!(publisher.retained_message_count(), 1); /// - /// assert_eq!(*rx.try_recv().unwrap(), 10); - /// assert_eq!(tx.retained_message_count(), 0); + /// assert_eq!(*receiver.try_recv().unwrap(), "pending"); + /// assert_eq!(publisher.retained_message_count(), 0); /// ``` pub fn retained_message_count(&self) -> usize { self.shared.inner.lock().log.retained() @@ -465,14 +468,14 @@ impl BoundedSender { /// ``` /// use asyncband::broadcast::mpmc; /// - /// let (tx, _rx) = mpmc::bounded::(8); - /// assert_eq!(tx.capacity(), 8); + /// let (publisher, _receiver) = mpmc::bounded::(8); + /// assert_eq!(publisher.capacity(), 8); /// ``` pub fn capacity(&self) -> usize { self.shared.capacity } - /// Creates a new receiver that starts receiving messages from the current tail of the channel. + /// Subscribes a new receiver for values published from this point forward. /// /// Subscribing never consumes capacity: the new cursor starts at the tail, so it retains /// nothing that was not already retained. @@ -485,13 +488,13 @@ impl BoundedSender { /// /// # #[tokio::main] /// # async fn main() { - /// let (tx, _rx) = mpmc::bounded(4); - /// tx.send(10).await; + /// let (publisher, _initial) = mpmc::bounded(4); + /// publisher.send("earlier").await; /// - /// let mut rx = tx.subscribe(); - /// assert_eq!(rx.try_recv(), Err(TryRecvError::Empty)); - /// tx.send(20).await; - /// assert_eq!(*rx.recv().await.unwrap(), 20); + /// let mut late = publisher.subscribe(); + /// assert_eq!(late.try_recv(), Err(TryRecvError::Empty)); + /// publisher.send("later").await; + /// assert_eq!(*late.recv().await.unwrap(), "later"); /// # } /// ``` #[must_use = "the receiver is dropped immediately if it is not retained"] @@ -541,7 +544,7 @@ impl Drop for BoundedReceiver { } impl BoundedReceiver { - /// Receives the next value for this receiver as the channel's shared `Arc`. + /// Waits for this receiver's next value and returns the channel's shared `Arc`. /// /// Receiving never clones `T`. The `Arc` outlives the message's stay in the backlog: a /// caller that keeps it keeps the payload alive but holds no capacity. Use @@ -552,11 +555,10 @@ impl BoundedReceiver { /// Returns [`RecvError::Disconnected`] once all senders have been dropped and this /// receiver has consumed its remaining messages. /// - /// # Cancel safety + /// # Cancellation /// - /// Dropping a pending `recv` future leaves this receiver's cursor unchanged. A subsequent - /// `recv` future can still return the same next value, so these futures may safely be raced - /// with other futures in a selection construct. + /// Dropping a pending future leaves this receiver's cursor unchanged, so a subsequent receive + /// observes the same next value. /// /// # Examples /// @@ -565,16 +567,16 @@ impl BoundedReceiver { /// /// # #[tokio::main] /// # async fn main() { - /// let (tx, mut rx) = mpmc::bounded(4); - /// tx.send(10).await; - /// assert_eq!(*rx.recv().await.unwrap(), 10); + /// let (publisher, mut receiver) = mpmc::bounded(4); + /// publisher.send("ready").await; + /// assert_eq!(*receiver.recv().await.unwrap(), "ready"); /// # } /// ``` pub async fn recv(&mut self) -> Result, RecvError> { Ok(self.wait_next().await?.msg) } - /// Receives the next value for this receiver as an owned `T`. + /// Waits for this receiver's next value and returns it as an owned `T`. /// /// The payload is moved out of the channel without cloning when this read reclaims the message /// and nothing else still refers to it, such as an `Arc` kept from a shared receive or another @@ -585,11 +587,10 @@ impl BoundedReceiver { /// Returns [`RecvError::Disconnected`] once all senders have been dropped and this /// receiver has consumed its remaining messages. /// - /// # Cancel safety + /// # Cancellation /// - /// Dropping a pending `recv_cloned` future leaves this receiver's cursor unchanged. A - /// subsequent receive can still return the same next value, so these futures may safely be - /// raced with other futures in a selection construct. + /// Dropping a pending future leaves this receiver's cursor unchanged, so a subsequent receive + /// observes the same next value. /// /// # Panics /// @@ -603,9 +604,9 @@ impl BoundedReceiver { /// /// # #[tokio::main] /// # async fn main() { - /// let (tx, mut rx) = mpmc::bounded(4); - /// tx.send(String::from("owned")).await; - /// assert_eq!(rx.recv_cloned().await, Ok(String::from("owned"))); + /// let (publisher, mut receiver) = mpmc::bounded(4); + /// publisher.send(String::from("owned")).await; + /// assert_eq!(receiver.recv_cloned().await, Ok(String::from("owned"))); /// # } /// ``` pub async fn recv_cloned(&mut self) -> Result @@ -616,8 +617,8 @@ impl BoundedReceiver { Ok(common::take_msg(received)) } - /// Attempts to receive the next value for this receiver without blocking, as the channel's - /// shared `Arc`. + /// Attempts to take this receiver's next value without waiting, as the channel's shared + /// `Arc`. /// /// See [`recv`](BoundedReceiver::recv) for what keeping the `Arc` means, and /// [`try_recv_cloned`](BoundedReceiver::try_recv_cloned) to take an owned value instead. @@ -633,15 +634,16 @@ impl BoundedReceiver { /// ``` /// use asyncband::broadcast::mpmc; /// - /// let (tx, mut rx) = mpmc::bounded(4); - /// tx.try_send(10).unwrap(); - /// assert_eq!(*rx.try_recv().unwrap(), 10); + /// let (publisher, mut receiver) = mpmc::bounded(4); + /// publisher.try_send("ready").unwrap(); + /// assert_eq!(*receiver.try_recv().unwrap(), "ready"); /// ``` pub fn try_recv(&mut self) -> Result, TryRecvError> { Ok(self.receive_released()?.msg) } - /// Attempts to receive the next value for this receiver without blocking, as an owned `T`. + /// Attempts to take this receiver's next value without waiting and returns it as an owned + /// `T`. /// /// See [`recv_cloned`](BoundedReceiver::recv_cloned) for when the payload is moved out rather /// than cloned. @@ -662,9 +664,9 @@ impl BoundedReceiver { /// ``` /// use asyncband::broadcast::mpmc; /// - /// let (tx, mut rx) = mpmc::bounded(4); - /// tx.try_send(String::from("owned")).unwrap(); - /// assert_eq!(rx.try_recv_cloned(), Ok(String::from("owned"))); + /// let (publisher, mut receiver) = mpmc::bounded(4); + /// publisher.try_send(String::from("owned")).unwrap(); + /// assert_eq!(receiver.try_recv_cloned(), Ok(String::from("owned"))); /// ``` pub fn try_recv_cloned(&mut self) -> Result where @@ -688,8 +690,7 @@ impl BoundedReceiver { Ok(received) } - /// Re-subscribes to the channel, returning a new receiver that starts receiving messages from - /// the *current* tail of the channel. + /// Creates a new subscription positioned after all values already published to the channel. /// /// The new receiver skips every value already published, including the latest retained value. /// The original receiver is unchanged and continues to retain its own backlog until it @@ -700,14 +701,14 @@ impl BoundedReceiver { /// ``` /// use asyncband::broadcast::mpmc; /// - /// let (tx, mut rx) = mpmc::bounded(4); - /// tx.try_send(1).unwrap(); - /// tx.try_send(2).unwrap(); + /// let (publisher, mut original) = mpmc::bounded(4); + /// publisher.try_send("first").unwrap(); + /// publisher.try_send("second").unwrap(); /// - /// let mut rx2 = rx.resubscribe(); - /// tx.try_send(3).unwrap(); + /// let mut fresh = original.resubscribe(); + /// publisher.try_send("third").unwrap(); /// - /// assert_eq!(*rx2.try_recv().unwrap(), 3); + /// assert_eq!(*fresh.try_recv().unwrap(), "third"); /// ``` #[must_use = "the receiver is dropped immediately if it is not retained"] pub fn resubscribe(&self) -> Self { diff --git a/asyncband/src/broadcast/mpmc/unbounded/mod.rs b/asyncband/src/broadcast/mpmc/unbounded/mod.rs index 3593cb60..bed25715 100644 --- a/asyncband/src/broadcast/mpmc/unbounded/mod.rs +++ b/asyncband/src/broadcast/mpmc/unbounded/mod.rs @@ -272,11 +272,10 @@ impl UnboundedReceiver { /// Returns [`RecvError::Disconnected`] once all senders have been dropped and this /// receiver has consumed its remaining messages. /// - /// # Cancel safety + /// # Cancellation /// - /// Dropping a pending `recv` future leaves this receiver's cursor unchanged. A subsequent - /// `recv` future can still return the same next value, so these futures may safely be raced - /// with other futures in a selection construct. + /// Dropping a pending future leaves this receiver's cursor unchanged, so a subsequent receive + /// observes the same next value. /// /// # Examples /// @@ -309,11 +308,10 @@ impl UnboundedReceiver { /// Returns [`RecvError::Disconnected`] once all senders have been dropped and this /// receiver has consumed its remaining messages. /// - /// # Cancel safety + /// # Cancellation /// - /// Dropping a pending `recv_cloned` future leaves this receiver's cursor unchanged. A - /// subsequent receive can still return the same next value, so these futures may safely be - /// raced with other futures in a selection construct. + /// Dropping a pending future leaves this receiver's cursor unchanged, so a subsequent receive + /// observes the same next value. /// /// # Panics /// @@ -405,8 +403,7 @@ impl UnboundedReceiver { Ok(common::take_msg(received)) } - /// Re-subscribes to the channel, returning a new receiver that starts receiving messages from - /// the *current* tail of the channel. + /// Creates a new subscription positioned after all values already published to the channel. /// /// The new receiver skips this receiver's unread backlog. The original receiver remains at its /// current position and continues retaining those values until it consumes them or is dropped. diff --git a/asyncband/src/completion/mod.rs b/asyncband/src/completion/mod.rs index 560da756..dc6e8b35 100644 --- a/asyncband/src/completion/mod.rs +++ b/asyncband/src/completion/mod.rs @@ -186,7 +186,7 @@ impl Completion { /// Returns [`Abandoned`] if the completer is dropped before providing a value. Abandonment /// remains distinct from any error stored inside `T`. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping a pending future unregisters only this wait. It does not affect this observer, /// other waiters, or the eventual result. diff --git a/asyncband/src/condvar/mod.rs b/asyncband/src/condvar/mod.rs index cfe6e13c..3e6d80c7 100644 --- a/asyncband/src/condvar/mod.rs +++ b/asyncband/src/condvar/mod.rs @@ -170,7 +170,7 @@ impl Condvar { /// Unlike the standard library equivalent, this function does not check at runtime that the /// same mutex is always used with this condition variable. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping the future releases any guard it owns and removes this wait; the original guard /// is not returned to the caller. If the task was selected by [`notify_one`](Self::notify_one) @@ -197,7 +197,7 @@ impl Condvar { /// The wait is registered before the mutex is released. The returned owned guard holds the /// mutex again; the caller must recheck its predicate as with [`wait`](Self::wait). /// - /// # Cancel safety + /// # Cancellation /// /// Dropping the future releases any guard it owns without returning it to the caller. /// Notifications are transferred to another current waiter under the same conditions as @@ -223,7 +223,7 @@ impl Condvar { /// `condition` is checked under the lock before waiting and after every wakeup; if it is /// already `false` on entry, this method returns immediately without waiting. /// - /// # Cancel safety + /// # Cancellation /// /// Each wait iteration has the same cancellation semantics as [`wait`](Self::wait). Cancelling /// drops the mutex guard; mutations already made by `condition` are not rolled back. @@ -282,7 +282,7 @@ impl Condvar { /// `condition` is checked under the lock before waiting and after every wakeup; if it is /// already `false` on entry, this method returns immediately without waiting. /// - /// # Cancel safety + /// # Cancellation /// /// Each wait iteration has the same cancellation semantics as /// [`wait_owned`](Self::wait_owned). Cancelling drops the owned mutex guard; mutations already diff --git a/asyncband/src/event/auto_reset.rs b/asyncband/src/event/auto_reset.rs index 2cb98ac0..b047273e 100644 --- a/asyncband/src/event/auto_reset.rs +++ b/asyncband/src/event/auto_reset.rs @@ -163,7 +163,7 @@ impl AutoResetEvent { /// Merely creating this future neither reserves a signal nor registers the wait. /// Signals assigned to other waits cannot be consumed by this wait. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping this future before it returns `Ready` removes its registration. If a signal was /// assigned to it, that signal is passed to another registered wait or stored for a future @@ -181,7 +181,7 @@ impl AutoResetEvent { /// /// The future keeps the event alive without borrowing the caller's handle. /// - /// # Cancel safety + /// # Cancellation /// /// Cancellation has the same signal semantics as [`wait`](Self::wait) and releases the /// future's `Arc`. diff --git a/asyncband/src/event/manual_reset.rs b/asyncband/src/event/manual_reset.rs index 44ab7db6..683131fa 100644 --- a/asyncband/src/event/manual_reset.rs +++ b/asyncband/src/event/manual_reset.rs @@ -174,7 +174,7 @@ impl ManualResetEvent { /// Merely creating this future does not register the wait. Once a [`set`](Self::set) releases /// a registered wait, a later [`reset`](Self::reset) cannot make that wait pending again. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping this future before it returns `Ready` removes its registration. This does not /// change the event or affect other waits. @@ -190,7 +190,7 @@ impl ManualResetEvent { /// /// The future keeps the event alive without borrowing the caller's handle. /// - /// # Cancel safety + /// # Cancellation /// /// Cancellation has the same signal semantics as [`wait`](Self::wait) and releases the /// future's `Arc`. diff --git a/asyncband/src/latch/mod.rs b/asyncband/src/latch/mod.rs index ac2eae14..75c28670 100644 --- a/asyncband/src/latch/mod.rs +++ b/asyncband/src/latch/mod.rs @@ -161,7 +161,7 @@ impl Latch { /// Waits until the countdown reaches zero. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping a pending future unregisters only this wait. It does not change the countdown or /// affect other waiters. @@ -190,7 +190,7 @@ impl Latch { /// /// The future keeps the latch alive without borrowing the caller's handle. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping a pending future unregisters only this wait and releases its `Arc`. It does not /// change the countdown or affect other waiters. diff --git a/asyncband/src/mpmc/bounded.rs b/asyncband/src/mpmc/bounded.rs index b9557359..89d81f25 100644 --- a/asyncband/src/mpmc/bounded.rs +++ b/asyncband/src/mpmc/bounded.rs @@ -84,7 +84,7 @@ impl BoundedSender { /// /// Returns [`SendError`] containing the unsent value if all receivers have been dropped. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping a pending `send` future releases its waiting resources before dropping `value`, /// without sending it or retaining capacity. Use [`reserve`](Self::reserve) to wait for @@ -104,7 +104,7 @@ impl BoundedSender { /// Returns `SendError(())` if all receivers have been dropped. A successful reservation /// can still fail to send if disconnection happens before [`Permit::send`]. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping a pending `reserve` future releases its place in the wait queue. If it was already /// granted capacity, that capacity passes to the next waiter or becomes available again. @@ -218,7 +218,7 @@ impl BoundedReceiver { /// Returns [`RecvError::Disconnected`] once all senders have been dropped and the queue is /// empty. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping a pending future does not remove a value from the queue or prevent another receiver /// from receiving it. diff --git a/asyncband/src/mpmc/unbounded.rs b/asyncband/src/mpmc/unbounded.rs index 0360c0ad..56b37e99 100644 --- a/asyncband/src/mpmc/unbounded.rs +++ b/asyncband/src/mpmc/unbounded.rs @@ -146,7 +146,7 @@ impl UnboundedReceiver { /// Returns [`RecvError::Disconnected`] once all senders have been dropped and the queue is /// empty. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping a pending future does not remove a value from the queue or prevent another receiver /// from receiving it. diff --git a/asyncband/src/mpsc/bounded/receiver.rs b/asyncband/src/mpsc/bounded/receiver.rs index 2907bf40..92402978 100644 --- a/asyncband/src/mpsc/bounded/receiver.rs +++ b/asyncband/src/mpsc/bounded/receiver.rs @@ -118,7 +118,7 @@ impl BoundedReceiver { /// Returns [`RecvError::Disconnected`] once all senders have been dropped and the queue is /// empty. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping a pending future does not remove a value from the queue. A later call on this /// receiver can still receive the next value. diff --git a/asyncband/src/mpsc/bounded/sender.rs b/asyncband/src/mpsc/bounded/sender.rs index d72b73b6..db56c5a7 100644 --- a/asyncband/src/mpsc/bounded/sender.rs +++ b/asyncband/src/mpsc/bounded/sender.rs @@ -80,7 +80,7 @@ impl BoundedSender { /// /// Returns [`SendError`] containing the unsent value if the receiver has been dropped. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping a pending `send` future loses its place waiting for capacity and drops `value`; /// a future that has returned `Pending` has not sent the message. Use @@ -110,7 +110,7 @@ impl BoundedSender { /// Returns `SendError(())` if the receiver has been dropped. A successful reservation /// can still fail to send if disconnection happens before [`Permit::send`]. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping a pending `reserve` future loses its place in the wait queue. If capacity has /// already been granted, it is released to the next waiter or made available to a new sender. diff --git a/asyncband/src/mpsc/unbounded/receiver.rs b/asyncband/src/mpsc/unbounded/receiver.rs index 33e6ba88..77f1331e 100644 --- a/asyncband/src/mpsc/unbounded/receiver.rs +++ b/asyncband/src/mpsc/unbounded/receiver.rs @@ -124,7 +124,7 @@ impl UnboundedReceiver { /// Returns [`RecvError::Disconnected`] once all senders have been dropped and the queue is /// empty. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping a pending future does not remove a value from the queue. A later call on this /// receiver can still receive the next value. diff --git a/asyncband/src/mutex/mod.rs b/asyncband/src/mutex/mod.rs index 6f895ecf..a4ba4da9 100644 --- a/asyncband/src/mutex/mod.rs +++ b/asyncband/src/mutex/mod.rs @@ -144,7 +144,7 @@ impl Mutex { impl Mutex { /// Waits for exclusive access and returns a borrowed guard. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping a pending future removes this request and releases any assigned permit. A later /// attempt joins the back of the queue. No guard is returned on cancellation. @@ -197,7 +197,7 @@ impl Mutex { /// The owned guard keeps the mutex alive instead of borrowing it, which allows the guard to be /// moved wherever a `'static` value is required. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping a pending future removes this request and releases any assigned permit. A later /// attempt joins the back of the queue. No guard is returned, and the future releases its owned @@ -342,32 +342,23 @@ impl<'a, T: ?Sized> MutexGuard<'a, T> { /// use asyncband::mutex::Mutex; /// use asyncband::mutex::MutexGuard; /// - /// #[derive(Debug)] - /// struct User { - /// id: u32, - /// profile: UserProfile, + /// struct Account { + /// display_name: String, + /// balance: u64, /// } /// - /// #[derive(Debug)] - /// struct UserProfile { - /// email: String, - /// name: String, - /// } - /// - /// let user = User { - /// id: 1, - /// profile: UserProfile { - /// email: "user@example.com".to_owned(), - /// name: "Alice".to_owned(), - /// }, - /// }; + /// let account = Mutex::new(Account { + /// display_name: "ops".to_owned(), + /// balance: 100, + /// }); /// - /// let mutex = Mutex::new(user); - /// let guard = mutex.lock().await; + /// { + /// let guard = account.lock().await; + /// let mut balance = MutexGuard::map(guard, |acc| &mut acc.balance); + /// *balance += 50; + /// } /// - /// // Map to only access the user's profile, allowing fine-grained locking - /// let profile_guard = MutexGuard::map(guard, |user| &mut user.profile); - /// assert_eq!(profile_guard.email, "user@example.com"); + /// assert_eq!(account.lock().await.balance, 150); /// # } /// ``` pub fn map(mut orig: Self, f: F) -> MappedMutexGuard<'a, U> @@ -386,8 +377,8 @@ impl<'a, T: ?Sized> MutexGuard<'a, T> { /// Attempts to project this guard to a mutable component of the protected value. /// - /// Call this as - /// `MutexGuard::filter_map(...)` so a method with the same name on `T` remains accessible. + /// Call this as `MutexGuard::filter_map(...)` so a method with the same name on `T` remains + /// accessible. /// /// # Errors /// @@ -404,37 +395,18 @@ impl<'a, T: ?Sized> MutexGuard<'a, T> { /// ``` /// # #[tokio::main] /// # async fn main() { - /// use std::collections::HashMap; + /// use std::collections::BTreeMap; /// /// use asyncband::mutex::Mutex; /// use asyncband::mutex::MutexGuard; /// - /// #[derive(Debug)] - /// struct Database { - /// users: HashMap, - /// admin_user_id: Option, - /// } - /// - /// let mut db = Database { - /// users: HashMap::new(), - /// admin_user_id: Some(1), - /// }; - /// db.users.insert(1, "admin@example.com".to_owned()); - /// - /// let mutex = Mutex::new(db); - /// let guard = mutex.lock().await; - /// - /// // Try to map to admin user's email if admin exists - /// let admin_email_guard = MutexGuard::filter_map(guard, |db| { - /// if let Some(admin_id) = db.admin_user_id { - /// db.users.get_mut(&admin_id) - /// } else { - /// None - /// } - /// }) - /// .expect("admin user should exist"); + /// let routes = Mutex::new(BTreeMap::from([("v1", 8080_u16)])); + /// let guard = routes.lock().await; + /// let mut port = MutexGuard::filter_map(guard, |map| map.get_mut("v1")).unwrap(); + /// *port = 8443; + /// drop(port); /// - /// assert_eq!(&*admin_email_guard, "admin@example.com"); + /// assert_eq!(routes.lock().await.get("v1"), Some(&8443)); /// # } /// ``` pub fn filter_map(mut orig: Self, f: F) -> Result, Self> @@ -526,22 +498,23 @@ impl OwnedMutexGuard { /// use asyncband::mutex::Mutex; /// use asyncband::mutex::OwnedMutexGuard; /// - /// struct Config { - /// name: String, - /// value: u32, + /// struct Limits { + /// label: &'static str, + /// max_in_flight: usize, /// } /// - /// let config = Config { - /// name: "front size".to_owned(), - /// value: 42, - /// }; + /// let limits = Arc::new(Mutex::new(Limits { + /// label: "ingress", + /// max_in_flight: 32, + /// })); /// - /// let mutex = Arc::new(Mutex::new(config)); - /// let guard = mutex.clone().lock_owned().await; + /// { + /// let guard = limits.clone().lock_owned().await; + /// let mut max_in_flight = OwnedMutexGuard::map(guard, |cfg| &mut cfg.max_in_flight); + /// *max_in_flight = 64; + /// } /// - /// // Map to access only the value field - /// let value_guard = OwnedMutexGuard::map(guard, |config| &mut config.value); - /// assert_eq!(*value_guard, 42); + /// assert_eq!(limits.lock().await.max_in_flight, 64); /// # } /// ``` pub fn map(mut orig: Self, f: F) -> OwnedMappedMutexGuard @@ -566,8 +539,8 @@ impl OwnedMutexGuard { /// Attempts to project this guard to a mutable component of the protected value. /// - /// Call this as - /// `OwnedMutexGuard::filter_map(...)` so a method with the same name on `T` remains accessible. + /// Call this as `OwnedMutexGuard::filter_map(...)` so a method with the same name on `T` + /// remains accessible. /// /// # Errors /// @@ -589,15 +562,13 @@ impl OwnedMutexGuard { /// use asyncband::mutex::Mutex; /// use asyncband::mutex::OwnedMutexGuard; /// - /// let data = vec![1, 2, 3, 4, 5]; - /// let mutex = Arc::new(Mutex::new(data)); - /// let guard = mutex.clone().lock_owned().await; - /// - /// // Map to the first element - /// let first_guard = - /// OwnedMutexGuard::filter_map(guard, |vec| vec.get_mut(0)).expect("vec should not be empty"); + /// let queue = Arc::new(Mutex::new(vec![10, 20, 30])); + /// let guard = queue.clone().lock_owned().await; + /// let mut head = OwnedMutexGuard::filter_map(guard, |items| items.first_mut()).unwrap(); + /// *head += 5; + /// drop(head); /// - /// assert_eq!(*first_guard, 1); + /// assert_eq!(*queue.lock().await, [15, 20, 30]); /// # } /// ``` pub fn filter_map(mut orig: Self, f: F) -> Result, Self> @@ -638,32 +609,23 @@ impl OwnedMutexGuard { /// use asyncband::mutex::Mutex; /// use asyncband::mutex::MutexGuard; /// -/// #[derive(Debug)] -/// struct User { -/// id: u32, -/// profile: UserProfile, +/// struct Envelope { +/// id: u64, +/// payload: String, /// } /// -/// #[derive(Debug)] -/// struct UserProfile { -/// email: String, -/// name: String, -/// } -/// -/// let user = User { -/// id: 1, -/// profile: UserProfile { -/// email: "user@example.com".to_owned(), -/// name: "Alice".to_owned(), -/// }, -/// }; +/// let mutex = Mutex::new(Envelope { +/// id: 7, +/// payload: "pending".to_owned(), +/// }); /// -/// let mutex = Mutex::new(user); -/// let guard = mutex.lock().await; -/// let profile_guard = MutexGuard::map(guard, |user| &mut user.profile); +/// { +/// let guard = mutex.lock().await; +/// let mut payload = MutexGuard::map(guard, |env| &mut env.payload); +/// *payload = "committed".to_owned(); +/// } /// -/// // Now we can only access the user's profile -/// assert_eq!(profile_guard.email, "user@example.com"); +/// assert_eq!(mutex.lock().await.payload, "committed"); /// # } /// ``` #[must_use = "dropping the guard releases the mutex immediately"] @@ -674,15 +636,12 @@ pub struct MappedMutexGuard<'a, T: ?Sized> { variance: PhantomData<&'a mut T>, } -// SAFETY: MappedMutexGuard can be safely sent between threads when T: Send. -// The guard holds exclusive access to the data protected by the mutex lock, -// and the NonNull pointer remains valid for the guard's lifetime. -// This is essential for async tasks that may be moved between threads at .await points. +// SAFETY: The guard exclusively borrows the locked mutex and remains valid for `'a`, so moving it +// across threads is safe when `T: Send`. unsafe impl Send for MappedMutexGuard<'_, T> {} -// SAFETY: MappedMutexGuard can be safely shared between threads (Sync) when T: Sync. -// Through &MappedMutexGuard, you can only get &T, so if T itself allows sharing references -// across threads, then sharing MappedMutexGuard references is also safe. +// SAFETY: `&MappedMutexGuard<'_, T>` yields only `&T`, so sharing it across threads is safe when +// `T: Sync`. unsafe impl Sync for MappedMutexGuard<'_, T> {} impl Drop for MappedMutexGuard<'_, T> { @@ -706,14 +665,14 @@ impl fmt::Display for MappedMutexGuard<'_, T> { impl Deref for MappedMutexGuard<'_, T> { type Target = T; fn deref(&self) -> &Self::Target { - // SAFETY: we hold the lock and the NonNull pointer is valid for the guard's lifetime + // SAFETY: `self` holds exclusive mutex access and `d` remains valid for `'a`. unsafe { self.d.as_ref() } } } impl DerefMut for MappedMutexGuard<'_, T> { fn deref_mut(&mut self) -> &mut Self::Target { - // SAFETY: we hold the lock and the NonNull pointer is valid for the guard's lifetime + // SAFETY: `self` holds exclusive mutex access and `d` remains valid for `'a`. unsafe { self.d.as_mut() } } } @@ -739,35 +698,26 @@ impl<'a, T: ?Sized> MappedMutexGuard<'a, T> { /// use asyncband::mutex::Mutex; /// use asyncband::mutex::MutexGuard; /// - /// #[derive(Debug)] - /// struct User { - /// id: u32, - /// profile: UserProfile, + /// struct Counters { + /// inner: InnerCounters, /// } /// - /// #[derive(Debug)] - /// struct UserProfile { - /// email: String, - /// name: String, + /// struct InnerCounters { + /// accepted: u64, /// } /// - /// let user = User { - /// id: 1, - /// profile: UserProfile { - /// email: "user@example.com".to_owned(), - /// name: "Alice".to_owned(), - /// }, - /// }; - /// - /// let mutex = Mutex::new(user); - /// let guard = mutex.lock().await; + /// let counters = Mutex::new(Counters { + /// inner: InnerCounters { accepted: 0 }, + /// }); /// - /// // First map to user profile - /// let profile_guard = MutexGuard::map(guard, |user| &mut user.profile); - /// // Then map to the email field specifically - /// let email_guard = MappedMutexGuard::map(profile_guard, |profile| &mut profile.email); + /// { + /// let guard = counters.lock().await; + /// let inner = MutexGuard::map(guard, |c| &mut c.inner); + /// let mut accepted = MappedMutexGuard::map(inner, |i| &mut i.accepted); + /// *accepted += 1; + /// } /// - /// assert_eq!(&*email_guard, "user@example.com"); + /// assert_eq!(counters.lock().await.inner.accepted, 1); /// # } /// ``` pub fn map(mut orig: Self, f: F) -> MappedMutexGuard<'a, U> @@ -786,9 +736,8 @@ impl<'a, T: ?Sized> MappedMutexGuard<'a, T> { /// Attempts to project an already mapped guard to a deeper mutable component. /// - /// Call this as - /// `MappedMutexGuard::filter_map(...)` so a method with the same name on `T` remains - /// accessible. + /// Call this as `MappedMutexGuard::filter_map(...)` so a method with the same name on `T` + /// remains accessible. /// /// # Errors /// @@ -809,27 +758,18 @@ impl<'a, T: ?Sized> MappedMutexGuard<'a, T> { /// use asyncband::mutex::Mutex; /// use asyncband::mutex::MutexGuard; /// - /// #[derive(Debug)] - /// struct Data { - /// id: u32, - /// value: Option, + /// struct Slot { + /// pending: Option, /// } /// - /// let data = Data { - /// id: 1, - /// value: Some("hello".to_owned()), - /// }; - /// - /// let mutex = Mutex::new(data); - /// let guard = mutex.lock().await; + /// let slot = Mutex::new(Slot { pending: Some(4) }); + /// let guard = slot.lock().await; + /// let pending = MutexGuard::map(guard, |s| &mut s.pending); + /// let mut value = MappedMutexGuard::filter_map(pending, |opt| opt.as_mut()).unwrap(); + /// *value *= 2; + /// drop(value); /// - /// // First map to the value field - /// let value_guard = MutexGuard::map(guard, |data| &mut data.value); - /// // Then try to map to the inner string if it exists - /// let string_guard = - /// MappedMutexGuard::filter_map(value_guard, |opt| opt.as_mut()).expect("value should exist"); - /// - /// assert_eq!(&*string_guard, "hello"); + /// assert_eq!(slot.lock().await.pending, Some(8)); /// # } /// ``` pub fn filter_map(mut orig: Self, f: F) -> Result, Self> @@ -867,16 +807,18 @@ impl<'a, T: ?Sized> MappedMutexGuard<'a, T> { /// use asyncband::mutex::Mutex; /// use asyncband::mutex::OwnedMutexGuard; /// -/// struct Data { -/// value: u32, +/// struct Metrics { +/// dropped: usize, /// } /// -/// let data = Data { value: 42 }; -/// let mutex = Arc::new(Mutex::new(data)); -/// let guard = mutex.clone().lock_owned().await; -/// let value_guard = OwnedMutexGuard::map(guard, |data| &mut data.value); +/// let metrics = Arc::new(Mutex::new(Metrics { dropped: 0 })); +/// { +/// let guard = metrics.clone().lock_owned().await; +/// let mut dropped = OwnedMutexGuard::map(guard, |m| &mut m.dropped); +/// *dropped += 3; +/// } /// -/// assert_eq!(*value_guard, 42); +/// assert_eq!(metrics.lock().await.dropped, 3); /// # } /// ``` #[must_use = "dropping the guard releases the mutex immediately"] @@ -887,16 +829,12 @@ pub struct OwnedMappedMutexGuard { variance: PhantomData<*mut U>, } -// SAFETY: OwnedMappedMutexGuard can be safely sent between threads when T: Send and U: Send. -// It holds exclusive access to the data protected by the mutex lock, and the raw pointer -// remains valid for the guard's lifetime. This is essential for async tasks that may be -// moved between threads at .await points. +// SAFETY: The guard holds exclusive mutex access and keeps the `Arc` alive, so moving it across +// threads is safe when `T: Send` and `U: Send`. unsafe impl Send for OwnedMappedMutexGuard {} -// SAFETY: OwnedMappedMutexGuard can be safely shared between threads (Sync) when T: Send + Sync and -// U: Send + Sync. Through &OwnedMappedMutexGuard, you can only get &U, so if U itself allows -// sharing references across threads, then sharing OwnedMappedMutexGuard references is also safe. -// We require T: Send + Sync for maximum safety and ecosystem compatibility. +// SAFETY: `&OwnedMappedMutexGuard` yields only `&U`, and dropping/cloning lock state requires +// `Mutex: Sync`, so sharing across threads is safe when `T: Send + Sync` and `U: Send + Sync`. unsafe impl Sync for OwnedMappedMutexGuard {} impl Drop for OwnedMappedMutexGuard { @@ -920,16 +858,16 @@ impl fmt::Display for OwnedMappedMutexGuard impl Deref for OwnedMappedMutexGuard { type Target = U; fn deref(&self) -> &Self::Target { - // SAFETY: we hold the lock and the NonNull pointer is valid for the guard's lifetime - // The Arc ensures the underlying data remains valid + // SAFETY: `self` owns the `Arc>` and holds exclusive lock access, so `d` stays + // valid. unsafe { self.d.as_ref() } } } impl DerefMut for OwnedMappedMutexGuard { fn deref_mut(&mut self) -> &mut Self::Target { - // SAFETY: we hold the lock and the NonNull pointer is valid for the guard's lifetime - // The Arc ensures the underlying data remains valid + // SAFETY: `self` owns the `Arc>` and holds exclusive lock access, so `d` stays + // valid. unsafe { self.d.as_mut() } } } @@ -957,26 +895,28 @@ impl OwnedMappedMutexGuard { /// use asyncband::mutex::OwnedMappedMutexGuard; /// use asyncband::mutex::OwnedMutexGuard; /// - /// #[derive(Debug)] /// struct Config { - /// host: String, - /// port: u16, + /// endpoint: Endpoint, /// } /// - /// let config = Config { - /// host: "localhost".to_owned(), - /// port: 8080, - /// }; + /// struct Endpoint { + /// host: String, + /// } /// - /// let mutex = Arc::new(Mutex::new(config)); - /// let guard = mutex.clone().lock_owned().await; + /// let config = Arc::new(Mutex::new(Config { + /// endpoint: Endpoint { + /// host: "127.0.0.1".to_owned(), + /// }, + /// })); /// - /// // First map to config - /// let config_guard = OwnedMutexGuard::map(guard, |config| &mut config.host); - /// // Then map to the host string specifically - /// let host_guard = OwnedMappedMutexGuard::map(config_guard, |host| host.as_mut_str()); + /// { + /// let guard = config.clone().lock_owned().await; + /// let endpoint = OwnedMutexGuard::map(guard, |c| &mut c.endpoint); + /// let mut host = OwnedMappedMutexGuard::map(endpoint, |e| &mut e.host); + /// *host = "localhost".to_owned(); + /// } /// - /// assert_eq!(&*host_guard, "localhost"); + /// assert_eq!(config.lock().await.endpoint.host, "localhost"); /// # } /// ``` pub fn map(mut orig: Self, f: F) -> OwnedMappedMutexGuard @@ -1000,9 +940,8 @@ impl OwnedMappedMutexGuard { /// Attempts to project an owned mapped guard to a deeper mutable component. /// - /// Call this as - /// `OwnedMappedMutexGuard::filter_map(...)` so a method with the same name on `U` remains - /// accessible through deref. + /// Call this as `OwnedMappedMutexGuard::filter_map(...)` so a method with the same name on + /// `U` remains accessible through deref. /// /// # Errors /// @@ -1025,35 +964,22 @@ impl OwnedMappedMutexGuard { /// use asyncband::mutex::OwnedMappedMutexGuard; /// use asyncband::mutex::OwnedMutexGuard; /// - /// #[derive(Debug)] - /// struct Node { - /// value: i32, - /// left: Option>, - /// right: Option>, + /// struct Entry { + /// next: Option>, /// } /// - /// let node = Node { - /// value: 10, - /// left: Some(Box::new(Node { - /// value: 5, - /// left: None, - /// right: None, - /// })), - /// right: None, - /// }; - /// - /// let mutex = Arc::new(Mutex::new(node)); - /// let guard = mutex.clone().lock_owned().await; - /// - /// // First map to left child - /// let left_guard = OwnedMutexGuard::map(guard, |node| &mut node.left); - /// // Try to access the left child if it exists - /// let child_guard = OwnedMappedMutexGuard::filter_map(left_guard, |left| { - /// left.as_mut().map(|boxed| boxed.as_mut()) - /// }) - /// .expect("left child should exist"); + /// let head = Arc::new(Mutex::new(Entry { + /// next: Some(Box::new(5)), + /// })); + /// + /// { + /// let guard = head.clone().lock_owned().await; + /// let next = OwnedMutexGuard::map(guard, |e| &mut e.next); + /// let mut tail = OwnedMappedMutexGuard::filter_map(next, |opt| opt.as_deref_mut()).unwrap(); + /// *tail += 7; + /// } /// - /// assert_eq!(child_guard.value, 5); + /// assert_eq!(head.lock().await.next.as_deref(), Some(&12)); /// # } /// ``` pub fn filter_map(mut orig: Self, f: F) -> Result, Self> diff --git a/asyncband/src/once/lazy_cell/mod.rs b/asyncband/src/once/lazy_cell/mod.rs index d3b42a86..e222ebf7 100644 --- a/asyncband/src/once/lazy_cell/mod.rs +++ b/asyncband/src/once/lazy_cell/mod.rs @@ -202,7 +202,7 @@ where /// [`force_pin`](Self::force_pin) for an inline future that is not `Unpin`. /// Concurrent callers wait while one caller drives the initializer. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping a pending call preserves the initialization future in the cell. A later caller /// resumes that same future instead of invoking the initializer again. Already performed @@ -241,7 +241,7 @@ where /// [`force_pin_mut`](Self::force_pin_mut) for an inline future that is not `Unpin`. /// The exclusive borrow prevents another caller from driving initialization. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping a pending call preserves the initialization future in the cell. A later caller /// resumes that same future instead of invoking the initializer again. Already performed @@ -280,7 +280,7 @@ where /// not [`Unpin`]. /// Concurrent callers wait while one caller drives the initializer. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping a pending call preserves the initialization future in the cell. A later caller /// resumes that same future instead of invoking the initializer again. Already performed @@ -337,7 +337,7 @@ where /// not [`Unpin`]. /// The exclusive borrow prevents another caller from driving initialization. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping a pending call preserves the initialization future in the cell. A later caller /// resumes that same future instead of invoking the initializer again. Already performed diff --git a/asyncband/src/once/once/mod.rs b/asyncband/src/once/once/mod.rs index 5fecb846..4c5b2842 100644 --- a/asyncband/src/once/once/mod.rs +++ b/asyncband/src/once/once/mod.rs @@ -24,7 +24,7 @@ use crate::internal::countdown::CountdownState; use crate::internal::wakerset::WakerToken; use crate::semaphore::Semaphore; -/// A synchronization primitive which can be used to run a one-time async initialization. +/// Coordinates a one-time asynchronous initialization step. /// /// Unlike [`std::sync::Once`], this type waits asynchronously rather than blocking a thread. The /// provided closure produces a future, and successful completion prevents later calls from running @@ -50,28 +50,20 @@ use crate::semaphore::Semaphore; /// /// use asyncband::once::Once; /// -/// static ONCE: Once = Once::new(); -/// static COUNTER: AtomicUsize = AtomicUsize::new(0); +/// let once = Once::new(); +/// let calls = AtomicUsize::new(0); /// -/// let handle1 = tokio::spawn(async { -/// ONCE.call_once(async || { -/// COUNTER.fetch_add(1, Ordering::SeqCst); -/// }) -/// .await; -/// }); +/// tokio::join!( +/// once.call_once(async || { +/// calls.fetch_add(1, Ordering::SeqCst); +/// }), +/// once.call_once(async || { +/// calls.fetch_add(1, Ordering::SeqCst); +/// }), +/// ); /// -/// let handle2 = tokio::spawn(async { -/// ONCE.call_once(async || { -/// COUNTER.fetch_add(1, Ordering::SeqCst); -/// }) -/// .await; -/// }); -/// -/// handle1.await.unwrap(); -/// handle2.await.unwrap(); -/// -/// // The counter is incremented only once, even though two tasks called `call_once`. -/// assert_eq!(COUNTER.load(Ordering::SeqCst), 1); +/// assert!(once.is_completed()); +/// assert_eq!(calls.load(Ordering::SeqCst), 1); /// # } /// ``` pub struct Once { @@ -109,7 +101,7 @@ impl Once { } } - /// Returns `true` if some `call_once` has completed successfully. + /// Reports whether an initializer passed to [`call_once`](Self::call_once) has completed. /// /// # Examples /// @@ -136,7 +128,7 @@ impl Once { /// One caller runs its initializer while others wait. After an initializer completes normally, /// all current and future calls return without running another initializer. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping a pending call that is waiting does not interrupt the active initializer. Dropping /// the call running `f` cancels that attempt and allows a waiting or later caller to run its @@ -157,14 +149,24 @@ impl Once { /// ``` /// # #[tokio::main] /// # async fn main() { + /// use std::sync::atomic::AtomicUsize; + /// use std::sync::atomic::Ordering; + /// /// use asyncband::once::Once; /// - /// static ONCE: Once = Once::new(); + /// let once = Once::new(); + /// let state = AtomicUsize::new(0); /// - /// ONCE.call_once(async || { - /// println!("Do some one-time async thing."); + /// once.call_once(async || { + /// state.store(1, Ordering::Relaxed); + /// }) + /// .await; + /// once.call_once(async || { + /// state.store(2, Ordering::Relaxed); /// }) /// .await; + /// + /// assert_eq!(state.load(Ordering::Relaxed), 1); /// # } /// ``` pub async fn call_once(&self, f: F) @@ -196,7 +198,7 @@ impl Once { /// /// This call does not start initialization. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping a pending future unregisters only this wait. It does not cancel initialization, /// change the completed state, or affect other waiters. diff --git a/asyncband/src/once/once_cell/mod.rs b/asyncband/src/once/once_cell/mod.rs index 8ee243d5..e652dae5 100644 --- a/asyncband/src/once/once_cell/mod.rs +++ b/asyncband/src/once/once_cell/mod.rs @@ -116,7 +116,7 @@ impl OnceCell { /// Concurrent callers wait for the active initializer. Once it publishes a value, all callers /// receive a reference to that value without running their own initializer. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping a pending call while it waits leaves the active initializer running. If this call /// is running the initializer, cancellation drops that initializer and leaves the cell empty; @@ -155,7 +155,7 @@ impl OnceCell { /// Returns the error from this call's initializer without storing a value. The error is not /// shared with waiting callers, which may make their own attempts. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping a pending call while it waits leaves the active initializer running. If this call /// is running the initializer, cancellation drops that initializer and leaves the cell empty; @@ -194,7 +194,7 @@ impl OnceCell { /// /// The exclusive borrow prevents concurrent initialization. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping a pending call drops its initializer and leaves the cell empty. A later call can /// start a new attempt; effects already performed by the initializer are not rolled back. @@ -237,7 +237,7 @@ impl OnceCell { /// /// Returns the initializer's error and leaves the cell empty. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping a pending call drops its initializer and leaves the cell empty. A later call can /// start a new attempt; effects already performed by the initializer are not rolled back. @@ -295,7 +295,7 @@ impl OnceCell { /// initializer publishes a value while this call waits. The error contains a reference to the /// stored value and the unused input. The stored value is unchanged. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping a pending future drops `value` without storing it. It does not cancel another /// caller's initialization. @@ -330,7 +330,7 @@ impl OnceCell { /// Returns `Err(value)` if the cell is already initialized, including when another initializer /// publishes a value while this call waits. The stored value is unchanged. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping a pending future drops `value` without storing it. It does not cancel another /// caller's initialization. diff --git a/asyncband/src/once/once_map/mod.rs b/asyncband/src/once/once_map/mod.rs index 74664771..0d30b27d 100644 --- a/asyncband/src/once/once_map/mod.rs +++ b/asyncband/src/once/once_map/mod.rs @@ -303,7 +303,7 @@ where /// cached until removed. Removing an in-flight entry detaches it: existing callers still /// receive its result, but that result is not cached in the map. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping a caller that is waiting for a result does not cancel the active computation. /// Dropping the caller executing `func` drops that attempt; another caller on the entry may @@ -345,7 +345,7 @@ where /// Returns an error from this call's `func` only to this caller. Errors are not stored or /// shared; another caller waiting on the entry may run its own `func`. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping a caller that is waiting for a result does not cancel the active computation. /// Dropping the caller executing `func` drops that attempt; another caller on the entry may @@ -386,14 +386,11 @@ where self.get_value(key) } - /// Removes the given key from the map. + /// Evicts `key` from the map without cloning any stored value. /// - /// If you need to get the value that has been removed, use the [`remove`] method instead. - /// - /// An in-flight computation is detached but continues for callers that already joined it; its - /// result is not stored in the map. - /// - /// [`remove`]: Self::remove + /// Detaching an in-flight entry leaves callers that already joined that attempt unaffected, + /// while preventing its result from being stored in the map and allowing later calls to start + /// fresh work. Use [`remove`](Self::remove) when a clone of a completed value is needed. pub fn discard(&self, key: &Q) where K: Borrow, @@ -402,16 +399,12 @@ where drop(self.remove_entry(key)); } - /// Removes the given key from the map and returns a *clone* of its completed value. - /// - /// If you do not need to get the value that has been removed, use the [`discard`] method - /// instead. - /// - /// Returns `None` when the key is absent or its computation is still in flight. An in-flight - /// computation is detached but continues for callers that already joined it; its result is not - /// stored in the map. + /// Evicts `key` from the map, returning a clone of its value if computation had completed. /// - /// [`discard`]: Self::discard + /// Returns `None` when `key` is absent or its computation is still in flight. Detaching an + /// in-flight entry leaves callers that already joined that attempt unaffected while preventing + /// its result from being stored in the map. Use [`discard`](Self::discard) to evict an entry + /// without cloning its value. pub fn remove(&self, key: &Q) -> Option where K: Borrow, diff --git a/asyncband/src/oneshot/receiver.rs b/asyncband/src/oneshot/receiver.rs index 9f1f862e..921d4ffc 100644 --- a/asyncband/src/oneshot/receiver.rs +++ b/asyncband/src/oneshot/receiver.rs @@ -47,7 +47,7 @@ use crate::oneshot::drop_message_and_deallocate_channel; /// /// Awaiting converts this receiver into a [`Recv`] future linked to the paired [`Sender`]. /// -/// # Cancel safety +/// # Cancellation /// /// Dropping either the receiver or its future disconnects the channel and discards any unread /// message. @@ -84,7 +84,7 @@ impl IntoFuture for Receiver { /// The future reports [`RecvError::Disconnected`] if the sender drops without sending or /// the message was already taken. See [`Recv`]. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping the returned future disconnects the channel and discards any unread message, /// even before its first poll. Retain and borrow the future to keep waiting after selection. @@ -107,17 +107,15 @@ impl Receiver { // `channel_ptr` remains valid. let channel = unsafe { self.channel_ptr.as_ref() }; - // ORDERING: Relaxed is sufficient to enforce the method's contract. - // - // Once true has been observed, it will remain true. However, if false is observed, the - // sender might just have been dropped without this thread observing it yet. + // ORDERING: `DISCONNECTED` is terminal, so a `Relaxed` load suffices to report a permanent + // state; observing `false` makes no synchronization claim against a concurrent drop. matches!(channel.state.load(Ordering::Relaxed), DISCONNECTED) } - /// Returns true if there is a message in the channel, ready to be received. + /// Reports whether a sent message is buffered and ready to be taken. /// - /// If `true` is returned, the next attempt to receive the message is guaranteed to complete - /// immediately. + /// Once this returns `true`, the next receive operation on this handle completes without + /// waiting. pub fn has_message(&self) -> bool { // SAFETY: The existence of `self` guarantees that the receiver is still alive. If the // sender was dropped, it observed the live receiver and left allocation cleanup to it, so @@ -140,26 +138,22 @@ impl Receiver { /// Returns [`TryRecvError::Disconnected`] if the sender dropped without sending or the /// message was already taken. pub fn try_recv(&self) -> Result { - // SAFETY: The channel will not be freed while this method is still running. + // SAFETY: The channel remains allocated until this receiver drops or converts into `Recv`. let channel = unsafe { self.channel_ptr.as_ref() }; - // ORDERING: Relaxed is fine since the only branch that needs synchronization is MESSAGE, - // and that branch has its own synchronization. + // ORDERING: Only the `MESSAGE` branch accesses sender-written storage; it performs the + // conditional `Acquire` synchronization below. match channel.state.load(Ordering::Relaxed) { MESSAGE => { - // It is okay to break up the load and store since once we are in the MESSAGE state, - // the sender no longer modifies the state - // - // ORDERING: The sender has completed, so this receiver-only terminal update does - // not publish data to another thread. + // `MESSAGE` is terminal for the sender, so marking disconnection needs no RMW and + // publishes no data to another thread. channel.state.store(DISCONNECTED, Ordering::Relaxed); - // ORDERING: The preceding Relaxed load read MESSAGE from the sender's Release - // publication. This conditional Acquire makes the message visible before it is - // taken. + // ORDERING: Pairs with the sender's `Release` publication of `MESSAGE` before + // reading the initialized payload. fence(Ordering::Acquire); - // SAFETY: we are in the MESSAGE state so the message is present and synchronized. + // SAFETY: `MESSAGE` is uniquely owned by this receiver after synchronization. Ok(unsafe { channel.take_message() }) } EMPTY => Err(TryRecvError::Empty), @@ -179,32 +173,25 @@ impl Drop for Receiver { // this side. let channel = unsafe { self.channel_ptr.as_ref() }; - // Set the channel state to disconnected and read what state the channel was in. - // - // ORDERING: This is a bidirectional ownership handoff. Release publishes the receiver's - // last access when the sender must reclaim the allocation; Acquire receives a - // sender-published message or disconnected state before receiver-side cleanup. + // ORDERING: Bidirectional ownership handoff: `Release` publishes the receiver's final + // access when the sender must reclaim the channel, while `Acquire` synchronizes + // with a sender-published message or disconnection before receiver-side cleanup. match channel.state.swap(DISCONNECTED, Ordering::AcqRel) { - // The sender has not sent anything, nor is it dropped. The sender is responsible for - // deallocating the channel. + // Sender is still alive; sender-side `send` or `drop` reclaims the empty channel. EMPTY => {} - // The sender already sent something. We must drop it, and free the channel. + // Sender already published a message; drop both the payload and the channel. MESSAGE => { - // SAFETY: The MESSAGE state plus acquire ordering guarantees the sender has - // written a message and that it has a happens-before relationship with this drop. - // In addition, the acquire ordering above synchronizes with the sender's final - // write of the state, so we can safely deallocate the channel. + // SAFETY: `MESSAGE` plus the swap's `Acquire` guarantees the initialized payload + // and sender completion are synchronized before reclamation. unsafe { drop_message_and_deallocate_channel(self.channel_ptr) }; } - // The sender was already dropped. We are responsible for freeing the channel. + // Sender already dropped or this receiver already extracted the message; reclaim here. DISCONNECTED => { - // SAFETY: If the sender published DISCONNECTED, the swap's Acquire half makes its - // preceding accesses happen before reclamation. If this receiver previously wrote - // DISCONNECTED after taking the message, no cross-thread synchronization is needed. + // SAFETY: Any prior sender accesses happened before the `Acquire` half of the swap + // if the sender wrote `DISCONNECTED`. unsafe { deallocate_empty_channel(self.channel_ptr) }; } - // NOTE: the receiver, unless transformed into a future, will never see the RECEIVING or - // AWAKING states, so we can ignore them here. + // A plain `Receiver` never registers a waker, so waking states are unreachable here. state => unreachable!("unexpected channel state: {}", state), } } @@ -220,7 +207,7 @@ impl Drop for Receiver { /// Resolves to [`RecvError::Disconnected`] if the sender drops without sending or the /// message was already taken before conversion. /// -/// # Cancel safety +/// # Cancellation /// /// Dropping this future disconnects the channel and discards any unread message, even if the /// future was never polled. To keep the operation alive when another branch wins a selection, @@ -436,11 +423,11 @@ impl fmt::Display for TryRecvError { impl Error for TryRecvError {} -/// An error returned when awaiting the message via [`Receiver`]. +/// Error returned when awaiting a [`Receiver`] or [`Recv`] future that can no longer yield a +/// message. /// -/// This error indicates that the corresponding [`Sender`] was dropped before sending any message. -/// Note that if a message was already received (e.g., via [`Receiver::try_recv`]), subsequent -/// `try_recv` calls will return [`TryRecvError::Disconnected`] instead. +/// Reported when the paired [`Sender`] was dropped without sending, or when the single message +/// was already extracted via [`Receiver::try_recv`] before awaiting. #[derive(Debug, Clone, Eq, PartialEq)] pub enum RecvError { /// No message can become available because the sender was dropped without sending or the diff --git a/asyncband/src/oneshot/sender.rs b/asyncband/src/oneshot/sender.rs index ea639e16..18ebac78 100644 --- a/asyncband/src/oneshot/sender.rs +++ b/asyncband/src/oneshot/sender.rs @@ -70,57 +70,43 @@ impl Sender { // receiver cancellation transfers allocation cleanup to this sender. let channel = unsafe { channel_ptr.as_ref() }; - // Write the message into the channel on the heap. - // - // SAFETY: The receiver only ever accesses this memory location if we are in the MESSAGE - // state, and since we are responsible for setting that state, we can guarantee that we have - // exclusive access to this memory location to perform this write. + // SAFETY: `message` is only read after `MESSAGE` is published via the atomic transition + // below, giving the sender exclusive write access here. unsafe { channel.write_message(message) }; - // Publish the message directly, or begin awakening a receiving task: + // Publish the message directly, or take ownership of waking a parked receiver: // - // * EMPTY + 1 = MESSAGE - // * RECEIVING + 1 = AWAKING - // * DISCONNECTED + 1 = EMPTY (invalid), however this state is never observed + // * `EMPTY + 1 = MESSAGE` + // * `RECEIVING + 1 = AWAKING` // - // ORDERING: Release publishes the message directly for EMPTY-to-MESSAGE and orders it - // before the waiting path's final publication. The RMW's load half is Relaxed, so - // branches that consume receiver-published resources use an Acquire fence. + // ORDERING: `Release` publishes the initialized message on `EMPTY -> MESSAGE` and orders it + // before the `AWAKING` handoff's final publication. Branches that inspect receiver-side + // state acquire below. match channel.state.fetch_add(1, Ordering::Release) { - // The receiver is alive and has not started waiting. Send done. + // Receiver has not registered a waker yet; it will read and drop the payload. EMPTY => Ok(()), - // The receiver is waiting. Wake it up so it can return the message. + // Receiver is parked; extract its waker and finalize the state transition. RECEIVING => { - // SAFETY: fetch_add observed RECEIVING and changed it to AWAKING, transferring - // exclusive ownership of the published waker to this sender. The message was - // initialized before the RMW and is ready to publish. + // SAFETY: Observing `RECEIVING` and setting `AWAKING` transferred exclusive waker + // ownership to this sender; `message` was initialized before the RMW. let (waker, receiver_owns_allocation) = unsafe { Channel::finish_sender_awakening(channel_ptr, MESSAGE) }; if receiver_owns_allocation { waker.wake(); } else { - // Sending still succeeds because this sender owned the waker before the - // receiving future was cancelled. + // Send succeeded before receiver cancellation transitioned out of `AWAKING`. // - // SAFETY: Receiver cancellation transferred message and allocation cleanup to - // this sender. The original pointer provenance may therefore be reclaimed as a - // Box; the message is initialized and the waker has been moved out. + // SAFETY: Cancellation transferred allocation cleanup to this sender. The + // waker was moved out and the message slot is initialized. unsafe { drop_message_and_deallocate_channel(channel_ptr) }; } Ok(()) } - // The receiver was already dropped. The error is responsible for freeing the channel. + // Receiver already disconnected; return the unsent message inside `SendError`. // - // SAFETY: The acquire fence in this arm synchronizes with the receiver's write of the - // DISCONNECTED state. Since the receiver will no longer access `channel_ptr`, the error - // takes exclusive ownership of the channel's resources. - // Moreover, since we just placed the message in the channel, the channel contains a - // valid message. + // SAFETY: The `Acquire` fence pairs with receiver disconnection so this endpoint now + // owns the channel allocation and its initialized message exclusively. DISCONNECTED => { - // ORDERING: The RMW read DISCONNECTED from the receiver's Release-ordered drop - // transition. - // This Acquire completes the ownership handoff before SendError accesses the - // allocation. fence(Ordering::Acquire); Err(SendError { channel_ptr }) } @@ -133,16 +119,11 @@ impl Sender { /// When this returns `true`, a later call to [`send`](Sender::send) is guaranteed to fail with /// [`SendError`]. A `false` result does not prevent the receiver from dropping concurrently. pub fn is_disconnected(&self) -> bool { - // SAFETY: The channel exists on the heap for the entire duration of this method, and we - // only ever acquire shared references to it. Dropping the receiver does not immediately - // free the channel. + // SAFETY: While `self` is live, dropping the receiver never frees the channel allocation. let channel = unsafe { self.channel_ptr.as_ref() }; - // ORDERING: Relaxed is sufficient for the method's contract: if this returns true, a - // future call to `send` is guaranteed to return an error. - // - // Once true has been observed, it will remain true. However, if false is observed, the - // receiver might just have been dropped without this thread observing it yet. + // ORDERING: `DISCONNECTED` is terminal, so `Relaxed` suffices to observe permanent + // disconnection; `false` makes no synchronization guarantee against concurrent drops. matches!(channel.state.load(Ordering::Relaxed), DISCONNECTED) } @@ -153,58 +134,41 @@ impl Sender { impl Drop for Sender { fn drop(&mut self) { - // SAFETY: The receiver only ever frees the channel if we are in the MESSAGE or - // DISCONNECTED states. - // - // * If we are in the MESSAGE state, then `send` suppressed `Sender::drop`, so we should not - // be in this function call. - // * If we are in the DISCONNECTED state, then the receiver either received the message, - // making this statement unreachable, or was dropped and observed that our side was still - // alive, and thus didn't free the channel. - // Publishing DISCONNECTED can let the receiver reclaim the allocation; no channel access - // may follow unless receiver cancellation transfers allocation cleanup to this sender. + // SAFETY: While `Sender::drop` runs, neither `MESSAGE` (blocked by `ManuallyDrop` in + // `send`) nor receiver-reclaimed `DISCONNECTED` can have freed the allocation. No channel + // access may follow publishing `DISCONNECTED` unless receiver cancellation hands cleanup + // back to this sender. let channel = unsafe { self.channel_ptr.as_ref() }; - // Publish the channel's disconnected state directly, or begin awakening a receiving task: + // Publish disconnection directly, or take ownership of waking a parked receiver: // - // * EMPTY ^ 001 = DISCONNECTED - // * RECEIVING ^ 001 = AWAKING - // * DISCONNECTED ^ 001 = EMPTY (invalid), but this state is never observed + // * `EMPTY ^ 0b001 = DISCONNECTED` + // * `RECEIVING ^ 0b001 = AWAKING` // - // ORDERING: Release publishes the disconnected state and orders it before the waiting - // path's final publication. The RMW's load half is Relaxed, so branches that consume - // receiver-published resources use an Acquire fence. + // ORDERING: `Release` publishes the sender's drop before any waiting handoff; branches that + // reclaim resources acquire below. match channel.state.fetch_xor(0b001, Ordering::Release) { - // The receiver is not waiting, nor is it dropped. The receiver is responsible for - // deallocating the channel. + // Receiver is alive without a waker and will reclaim the empty channel. EMPTY => {} - // The receiver is waiting. Wake it up so it can detect that the channel disconnected. + // Receiver is parked; extract its waker and finalize disconnection. RECEIVING => { - // SAFETY: fetch_xor observed RECEIVING and changed it to AWAKING, transferring - // exclusive ownership of the published waker to this sender. Sender::drop has no - // message to publish. + // SAFETY: Observing `RECEIVING` and setting `AWAKING` transferred exclusive waker + // ownership; no message was written. let (waker, receiver_owns_allocation) = unsafe { Channel::finish_sender_awakening(self.channel_ptr, DISCONNECTED) }; if receiver_owns_allocation { waker.wake(); } else { - // SAFETY: Receiver cancellation transferred allocation cleanup to this sender. - // Both slots are uninitialized, and the original pointer provenance may be - // reclaimed as a Box. + // SAFETY: Cancellation transferred cleanup; both payload and waker slots are + // empty. unsafe { deallocate_empty_channel(self.channel_ptr) }; } } - // The receiver was already dropped. We are responsible for freeing the channel. + // Receiver already disconnected and observed this live sender; reclaim the allocation. DISCONNECTED => { - // ORDERING: The RMW read DISCONNECTED from the receiver's Release-ordered drop - // transition. - // Acquire makes all preceding receiver accesses happen before deallocation. + // ORDERING: Pairs with the receiver's `Release` drop before freeing the channel. fence(Ordering::Acquire); - // SAFETY: when the receiver switches the state to DISCONNECTED they have received - // the message or will no longer be trying to receive the message, and have - // observed that the sender is still alive, meaning that we are responsible for - // freeing the channel allocation. The acquire ordering above synchronizes with - // the receiver's final write of the state. + // SAFETY: Receiver is gone and left an empty channel allocation for this sender. unsafe { deallocate_empty_channel(self.channel_ptr) }; } state => unreachable!("unexpected channel state: {}", state), @@ -212,10 +176,10 @@ impl Drop for Sender { } } -/// An error returned when trying to send on a disconnected channel. Returned from -/// [`Sender::send`] if the corresponding [`Receiver`] has already been dropped. +/// Error returned by [`Sender::send`] when the paired [`Receiver`] was dropped before receiving. /// -/// The message that could not be sent can be retrieved again with [`SendError::into_inner`]. +/// Owns the unsent value; call [`as_inner`](Self::as_inner) to inspect it or +/// [`into_inner`](Self::into_inner) to recover it. pub struct SendError { channel_ptr: NonNull>, } diff --git a/asyncband/src/phaser/mod.rs b/asyncband/src/phaser/mod.rs index a7f6f706..79bf7ecd 100644 --- a/asyncband/src/phaser/mod.rs +++ b/asyncband/src/phaser/mod.rs @@ -296,8 +296,7 @@ impl Phaser { /// participants; yielded handles keep their registrations. The iterator owns a shared handle /// and does not borrow this phaser. /// - /// On an open phaser, a zero-sized batch does nothing. Collect this iterator into a collection - /// of your choice. + /// On an open phaser, a zero-sized batch does nothing. /// /// # Errors /// @@ -344,7 +343,7 @@ impl Phaser { /// Returns [`Closed`] if the observed phase is still current when the phaser closes. A phase /// completed before closure remains successful. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping a pending future unregisters only this wait. It does not record an arrival or /// change the phase; the same observation can be retried. @@ -475,7 +474,7 @@ impl PhaserParticipant { /// Returns [`Closed`] if the phaser closes before this call can record an arrival or before /// its pending phase completes. A phase completed before closure remains successful. /// - /// # Cancel safety + /// # Cancellation /// /// An unpolled call has no effect. Once polled, arrival is committed. Cancelling preserves /// the pending observation, so retrying this method waits for the same phase without diff --git a/asyncband/src/pool/bounded.rs b/asyncband/src/pool/bounded.rs index db574e22..dc36a385 100644 --- a/asyncband/src/pool/bounded.rs +++ b/asyncband/src/pool/bounded.rs @@ -220,7 +220,7 @@ impl Pool { /// Returns an error from [`ManageObject::create`]. Objects created before the failure remain /// in the pool. /// - /// # Cancel safety + /// # Cancellation /// /// Cancelling this operation releases all reserved capacity. Objects created before /// cancellation remain idle in the pool; the in-progress [`ManageObject::create`] future is @@ -280,7 +280,7 @@ impl Pool { /// Returns errors from [`ManageObject::create`]. A failed creation does not add an object to /// the pool. /// - /// # Cancel safety + /// # Cancellation /// /// Cancelling while waiting for capacity or creating a new object restores the reserved pool /// capacity and drops any in-progress creation future. Retrying does not retain a place in diff --git a/asyncband/src/pool/common.rs b/asyncband/src/pool/common.rs index f5b02ee3..5b8fdb3d 100644 --- a/asyncband/src/pool/common.rs +++ b/asyncband/src/pool/common.rs @@ -91,7 +91,7 @@ pub trait ManageObject: Send + Sync { /// Returns an implementation-defined error when an object cannot be created. Pool checkout /// and replenishment propagate this error to their caller. /// - /// # Cancel safety + /// # Cancellation /// /// The pool may drop this future when checkout or replenishment is cancelled. Implementations /// must arrange cleanup of resources owned by an unfinished creation attempt. @@ -105,7 +105,7 @@ pub trait ManageObject: Send + Sync { /// detaches the object and retries checkout; it does not return this error to the caller. /// Record any required diagnostics in this implementation. /// - /// # Cancel safety + /// # Cancellation /// /// The pool may drop this future when checkout is cancelled. Mutations already made to `o` /// are not rolled back. [`RecycleCancelledStrategy`] determines whether the object is detached diff --git a/asyncband/src/pool/mod.rs b/asyncband/src/pool/mod.rs index c4068fa2..2dc6bed4 100644 --- a/asyncband/src/pool/mod.rs +++ b/asyncband/src/pool/mod.rs @@ -86,8 +86,7 @@ //! [`sync.Pool`](https://pkg.go.dev/sync#Pool). //! //! To configure a factory for creating objects when the pool is empty, like `sync.Pool`'s `New`, -//! you can create the unbounded pool via [`Pool::new`](unbounded::Pool::new) with an -//! implementation of [`ManageObject`]. +//! pass a [`ManageObject`] implementation to [`Pool::new`](unbounded::Pool::new). //! //! ## Examples //! diff --git a/asyncband/src/pool/unbounded.rs b/asyncband/src/pool/unbounded.rs index c12c2114..74ea525d 100644 --- a/asyncband/src/pool/unbounded.rs +++ b/asyncband/src/pool/unbounded.rs @@ -23,8 +23,7 @@ //! [`sync.Pool`](https://pkg.go.dev/sync#Pool). //! //! To configure a factory for creating objects when the pool is empty, like `sync.Pool`'s `New`, -//! you can create the unbounded pool via [`Pool::new`](Pool::new) with an -//! implementation of [`ManageObject`]. +//! pass a [`ManageObject`] implementation to [`Pool::new`](Pool::new). //! //! ## Examples //! @@ -278,15 +277,15 @@ impl Pool { /// Retrieves an [`Object`] from this [`Pool`], or creates a new one with the passed-in async /// closure, if the pool is empty. /// - /// This method only exists for [`NeverManageObject`] pools. If you provide a custom - /// [`ManageObject`] implementation, you should use [`Pool::get`] instead, and it will call - /// [`ManageObject::create`] to create a new object if the pool is empty. + /// This method only exists for [`NeverManageObject`] pools. Pools configured with a + /// [`ManageObject`] implementation use [`Pool::get`] instead, which calls + /// [`ManageObject::create`] when no idle object is available. /// /// # Errors /// /// Returns the error from `f` if creation fails. No object is added to the pool on error. /// - /// # Cancel safety + /// # Cancellation /// /// Cancelling while the provided future is pending leaves the pool unchanged. pub async fn get_or_create(self: &Arc, f: F) -> Result, E> @@ -331,7 +330,7 @@ impl> Pool { /// Returns errors from [`ManageObject::create`]. A failed creation does not add an object to /// the pool. /// - /// # Cancel safety + /// # Cancellation /// /// Cancelling during creation drops the creation future without adding an object. Objects /// already detached by failed recycling checks stay detached. Cancelling while diff --git a/asyncband/src/rwlock/owned_read_guard.rs b/asyncband/src/rwlock/owned_read_guard.rs index cad2ba4c..1142679c 100644 --- a/asyncband/src/rwlock/owned_read_guard.rs +++ b/asyncband/src/rwlock/owned_read_guard.rs @@ -37,7 +37,7 @@ impl RwLock { /// Ordering follows [`Self::read`]. The guard keeps the lock alive without borrowing the /// caller's `Arc`. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping a pending future removes the request and returns any permits already assigned to /// it. Retrying joins the back of the queue. The future also releases its owned `Arc`. diff --git a/asyncband/src/rwlock/owned_write_guard.rs b/asyncband/src/rwlock/owned_write_guard.rs index eaaca0fc..2a553d72 100644 --- a/asyncband/src/rwlock/owned_write_guard.rs +++ b/asyncband/src/rwlock/owned_write_guard.rs @@ -38,7 +38,7 @@ impl RwLock { /// /// Ordering follows [`Self::write`]. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping a pending future removes the request and returns any permits already assigned to /// it. Retrying joins the back of the queue. The future also releases its owned `Arc`. diff --git a/asyncband/src/rwlock/read_guard.rs b/asyncband/src/rwlock/read_guard.rs index 88d70c81..dc501294 100644 --- a/asyncband/src/rwlock/read_guard.rs +++ b/asyncband/src/rwlock/read_guard.rs @@ -36,7 +36,7 @@ impl RwLock { /// An earlier queued writer must finish first. See [queue ordering](crate::rwlock) before /// acquiring recursively. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping a pending future removes the request and returns any permits already assigned to /// it. Retrying joins the back of the queue. diff --git a/asyncband/src/rwlock/upgradable.rs b/asyncband/src/rwlock/upgradable.rs index 722fcd1e..1ce3b119 100644 --- a/asyncband/src/rwlock/upgradable.rs +++ b/asyncband/src/rwlock/upgradable.rs @@ -128,7 +128,7 @@ impl UpgradableRwLock { /// /// Writers and upgrades already queued for data permits must finish first. /// - /// # Cancel safety + /// # Cancellation /// /// Cancellation releases any assigned permit and loses the request's queue position. pub async fn read(&self) -> RwLockReadGuard<'_, T> { @@ -144,7 +144,7 @@ impl UpgradableRwLock { /// /// See [ordering](Self#ordering) for how ordinary readers may pass this request. /// - /// # Cancel safety + /// # Cancellation /// /// Cancellation releases the reservation and any assigned permits and loses both queue /// positions. @@ -173,7 +173,7 @@ impl UpgradableRwLock { /// The returned guard excludes other upgradable readers and reserves priority over later /// writers. Ordinary readers can coexist with it. /// - /// # Cancel safety + /// # Cancellation /// /// Cancellation releases the reservation and any assigned permit and loses both queue /// positions. @@ -267,7 +267,7 @@ impl<'a, T: ?Sized> RwLockUpgradableReadGuard<'a, T> { /// The reservation prevents writers from queuing ahead of this upgrade. Ordinary readers /// already ahead in the data-permit queue may finish first; later readers cannot pass it. /// - /// # Cancel safety + /// # Cancellation /// /// This consumes the guard. Dropping the future releases the original reader slot, any /// partially or fully assigned upgrade permits, and the reservation. This also holds if the diff --git a/asyncband/src/rwlock/write_guard.rs b/asyncband/src/rwlock/write_guard.rs index a3c11a64..98d8fb3c 100644 --- a/asyncband/src/rwlock/write_guard.rs +++ b/asyncband/src/rwlock/write_guard.rs @@ -40,7 +40,7 @@ pub fn from_access(access: WriteAccess<&RwLock>) -> RwLockWriteGua impl RwLock { /// Waits for exclusive access and returns a guard borrowing this lock. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping a pending future removes the request and returns any permits already assigned to /// it. Retrying joins the back of the queue. diff --git a/asyncband/src/semaphore/mod.rs b/asyncband/src/semaphore/mod.rs index 01bf503c..5d1a65a2 100644 --- a/asyncband/src/semaphore/mod.rs +++ b/asyncband/src/semaphore/mod.rs @@ -53,7 +53,7 @@ use std::sync::Arc; use crate::internal::semaphore; -/// An async counting semaphore for controlling access to a set of resources. +/// Asynchronous counting semaphore that bounds concurrent access with permits. /// /// See the [module level documentation](self) for more. #[derive(Debug)] @@ -62,14 +62,15 @@ pub struct Semaphore { } impl Semaphore { - /// Creates a new semaphore with the given number of permits. + /// Creates a semaphore initialized with `permits` available permits. /// /// # Examples /// /// ``` /// use asyncband::semaphore::Semaphore; /// - /// let sem = Semaphore::new(5); // Creates a semaphore with 5 permits + /// let sem = Semaphore::new(5); + /// assert_eq!(sem.available_permits(), 5); /// ``` pub const fn new(permits: usize) -> Self { Self { @@ -77,7 +78,7 @@ impl Semaphore { } } - /// Returns the current number of permits available. + /// Returns an instantaneous count of unacquired permits currently available. /// /// # Examples /// @@ -146,7 +147,11 @@ impl Semaphore { self.s.reduce_permits(n); } - /// Adds `permits` new permits to the semaphore. + /// Returns `permits` permits to the semaphore. + /// + /// Released permits first repay any unpaid deficit recorded by + /// [`reduce_permits`](Self::reduce_permits); remaining permits wake queued acquisitions or + /// increase [`available_permits`](Self::available_permits). /// /// # Panics /// @@ -158,7 +163,7 @@ impl Semaphore { /// use asyncband::semaphore::Semaphore; /// /// let sem = Semaphore::new(0); - /// sem.release(2); // Adds 2 permits + /// sem.release(2); /// assert_eq!(sem.available_permits(), 2); /// ``` pub fn release(&self, permits: usize) { @@ -201,7 +206,7 @@ impl Semaphore { /// Pending acquisitions are served in queue order. A larger request can hold up smaller /// requests behind it until enough permits become available. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping a pending future removes the request and returns any permits already assigned to /// it. Retrying joins the back of the queue. @@ -274,7 +279,7 @@ impl Semaphore { /// Pending acquisitions are served in queue order. A larger request can hold up smaller /// requests behind it until enough permits become available. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping a pending future removes the request and returns any permits already assigned to /// it. Retrying joins the back of the queue. The future also releases its owned `Arc`. diff --git a/asyncband/src/shutdown/mod.rs b/asyncband/src/shutdown/mod.rs index 60fa8de1..90c6bdd0 100644 --- a/asyncband/src/shutdown/mod.rs +++ b/asyncband/src/shutdown/mod.rs @@ -92,7 +92,9 @@ pub fn new() -> (Shutdown, ShutdownGuard) { /// A future that requests shutdown and waits for all completion guards to be dropped. /// -/// # Cancel safety +/// See the [module level documentation](self) for more. +/// +/// # Cancellation /// /// The first poll requests shutdown. Dropping an unpolled future does not request it; dropping /// a pending future after its first poll does not withdraw the request or drop completion @@ -123,8 +125,6 @@ pub fn new() -> (Shutdown, ShutdownGuard) { /// worker.await.unwrap(); /// # } /// ``` -/// -/// See the [module level documentation](self) for more. #[must_use = "shutdown is not requested unless this handle is polled or request_shutdown is called"] #[derive(Debug, Clone)] pub struct Shutdown { @@ -189,7 +189,7 @@ impl ShutdownGuard { /// Waits until shutdown is requested. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping a pending future removes only this wait. It does not request shutdown, clear an /// existing request, or drop the handle borrowed by this call. @@ -201,7 +201,7 @@ impl ShutdownGuard { /// /// The future retains the request state but does not keep shutdown completion pending. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping the future removes only this wait. It does not request shutdown or clear an /// existing request. @@ -226,7 +226,7 @@ impl ShutdownWatch { /// Waits until shutdown is requested. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping a pending future removes only this wait. It does not request shutdown, clear an /// existing request, or drop the handle borrowed by this call. @@ -238,7 +238,7 @@ impl ShutdownWatch { /// /// The future retains the request state but does not keep shutdown completion pending. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping the future removes only this wait. It does not request shutdown or clear an /// existing request. diff --git a/asyncband/src/singleflight/mod.rs b/asyncband/src/singleflight/mod.rs index c748e0fe..ba9b3352 100644 --- a/asyncband/src/singleflight/mod.rs +++ b/asyncband/src/singleflight/mod.rs @@ -234,7 +234,7 @@ where /// completes successfully, the entry is removed so later calls can start new work. /// [`forget`](Self::forget) lets new calls start independently of an existing entry. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping a caller that is waiting for a result does not cancel the active computation. /// Dropping the caller executing `func` drops that attempt; another caller on the entry may @@ -257,35 +257,55 @@ where /// use std::sync::Arc; /// use std::sync::atomic::AtomicUsize; /// use std::sync::atomic::Ordering; - /// use std::time::Duration; /// /// use asyncband::singleflight::Group; + /// use tokio::sync::oneshot; /// - /// # #[tokio::main] + /// # #[tokio::main(flavor = "current_thread")] /// # async fn main() { - /// let group = Group::new(); - /// let counter = Arc::new(AtomicUsize::new(0)); - /// - /// let c1 = counter.clone(); - /// let fut1 = group.work("key", || async move { - /// c1.fetch_add(1, Ordering::SeqCst); - /// // simulate heavy work to avoid immediate completion - /// tokio::time::sleep(Duration::from_millis(100)).await; - /// "result" - /// }); - /// - /// let c2 = counter.clone(); - /// let fut2 = group.work("key", || async move { - /// c2.fetch_add(1, Ordering::SeqCst); - /// // simulate heavy work to avoid immediate completion - /// tokio::time::sleep(Duration::from_millis(100)).await; - /// "result" - /// }); - /// - /// let (r1, r2) = tokio::join!(fut1, fut2); - /// - /// assert_eq!(r1, r2); - /// assert_eq!(counter.load(Ordering::SeqCst), 1); + /// let group = Arc::new(Group::new()); + /// let runs = Arc::new(AtomicUsize::new(0)); + /// let (started_tx, started_rx) = oneshot::channel(); + /// let (release_tx, release_rx) = oneshot::channel(); + /// + /// let leader = { + /// let group = group.clone(); + /// let runs = runs.clone(); + /// tokio::spawn(async move { + /// group + /// .work("manifest", || async move { + /// runs.fetch_add(1, Ordering::SeqCst); + /// started_tx.send(()).unwrap(); + /// release_rx.await.unwrap(); + /// "v1" + /// }) + /// .await + /// }) + /// }; + /// + /// started_rx.await.unwrap(); + /// + /// let follower = { + /// let group = group.clone(); + /// let runs = runs.clone(); + /// tokio::spawn(async move { + /// group + /// .work("manifest", || async move { + /// runs.fetch_add(1, Ordering::SeqCst); + /// "unused" + /// }) + /// .await + /// }) + /// }; + /// + /// tokio::task::yield_now().await; + /// release_tx.send(()).unwrap(); + /// + /// assert_eq!(leader.await.unwrap(), "v1"); + /// assert_eq!(follower.await.unwrap(), "v1"); + /// assert_eq!(runs.load(Ordering::SeqCst), 1); + /// + /// assert_eq!(group.work("manifest", || async { "v2" }).await, "v2"); /// # } /// ``` pub async fn work(&self, key: K, func: F) -> V @@ -318,7 +338,7 @@ where /// Returns an error from this call's `func` only to this caller. Errors are not stored or /// shared; another caller waiting on the entry may run its own `func`. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping a caller that is waiting for a result does not cancel the active computation. /// Dropping the caller executing `func` drops that attempt; another caller on the entry may @@ -339,32 +359,45 @@ where /// /// ``` /// use std::sync::Arc; - /// use std::sync::atomic::AtomicUsize; - /// use std::sync::atomic::Ordering; - /// use std::time::Duration; /// /// use asyncband::singleflight::Group; + /// use tokio::sync::oneshot; /// - /// # #[tokio::main] + /// # #[tokio::main(flavor = "current_thread")] /// # async fn main() { - /// let group = Group::new(); - /// - /// let fut1 = group.try_work("key", || async move { - /// // simulate heavy work to avoid immediate completion - /// tokio::time::sleep(Duration::from_millis(100)).await; - /// Err::<_, &'static str>("fut1") - /// }); - /// - /// let fut2 = group.try_work("key", || async move { - /// // simulate heavy work to avoid immediate completion - /// tokio::time::sleep(Duration::from_millis(200)).await; - /// Ok::<_, &'static str>("fut2") - /// }); - /// - /// let (r1, r2) = tokio::join!(fut1, fut2); - /// - /// assert_eq!(r1, Err("fut1")); - /// assert_eq!(r2, Ok("fut2")); + /// let group = Arc::new(Group::new()); + /// let (started_tx, started_rx) = oneshot::channel(); + /// let (release_tx, release_rx) = oneshot::channel(); + /// + /// let leader = { + /// let group = group.clone(); + /// tokio::spawn(async move { + /// group + /// .try_work("manifest", || async move { + /// started_tx.send(()).unwrap(); + /// release_rx.await.unwrap(); + /// Err::<&str, &str>("transient") + /// }) + /// .await + /// }) + /// }; + /// + /// started_rx.await.unwrap(); + /// + /// let follower = { + /// let group = group.clone(); + /// tokio::spawn(async move { + /// group + /// .try_work("manifest", || async { Ok::<&str, &str>("recovered") }) + /// .await + /// }) + /// }; + /// + /// tokio::task::yield_now().await; + /// release_tx.send(()).unwrap(); + /// + /// assert_eq!(leader.await.unwrap(), Err("transient")); + /// assert_eq!(follower.await.unwrap(), Ok("recovered")); /// # } /// ``` pub async fn try_work(&self, key: K, func: F) -> Result @@ -386,10 +419,11 @@ where Ok(result) } - /// Forgets about the given key. + /// Detaches any in-flight entry for `key` from the group. /// - /// Future calls to `work` for this key will call the function rather than waiting for an - /// earlier call to complete. Existing calls to `work` for this key are not affected. + /// Callers that already joined the detached entry continue waiting for its outcome, while later + /// calls to [`work`](Self::work) or [`try_work`](Self::try_work) for an equivalent key start a + /// separate entry immediately. Has no effect if `key` has no active entry. pub fn forget(&self, key: &Q) where K: Borrow, diff --git a/asyncband/src/spmc/bounded.rs b/asyncband/src/spmc/bounded.rs index 99766fce..07f55a00 100644 --- a/asyncband/src/spmc/bounded.rs +++ b/asyncband/src/spmc/bounded.rs @@ -75,7 +75,7 @@ impl BoundedSender { /// /// Returns [`SendError`] containing the unsent value if all receivers have been dropped. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping a pending `send` future removes its waiter and drops `value`. A future that has /// returned `Pending` has not sent the value. Cancellation releases the exclusive sender borrow @@ -136,7 +136,7 @@ impl BoundedReceiver { /// /// Returns [`RecvError::Disconnected`] once the sender has been dropped and the queue is empty. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping a pending future does not remove a value from the queue or prevent another receiver /// from receiving it. diff --git a/asyncband/src/spmc/unbounded.rs b/asyncband/src/spmc/unbounded.rs index a08d522a..09c222a1 100644 --- a/asyncband/src/spmc/unbounded.rs +++ b/asyncband/src/spmc/unbounded.rs @@ -116,7 +116,7 @@ impl UnboundedReceiver { /// /// Returns [`RecvError::Disconnected`] once the sender has been dropped and the queue is empty. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping a pending future does not remove a value from the queue or prevent another receiver /// from receiving it. diff --git a/asyncband/src/waitgroup/mod.rs b/asyncband/src/waitgroup/mod.rs index f48a0d31..b50c115e 100644 --- a/asyncband/src/waitgroup/mod.rs +++ b/asyncband/src/waitgroup/mod.rs @@ -214,7 +214,7 @@ impl IntoFuture for WaitGroup { /// Marks this participant complete and returns a future observing group completion. /// - /// # Cancel safety + /// # Cancellation /// /// Conversion marks this participant complete immediately. Dropping the returned future does /// not undo that completion; see [`Wait`] for observer cancellation. @@ -230,7 +230,7 @@ impl IntoFuture for WaitGroup { /// Converting a [`WaitGroup`] into this future immediately marks that participant complete, /// even before the future is polled. Cloning `Wait` adds an observer without a participant. /// -/// # Cancel safety +/// # Cancellation /// /// Dropping a pending future unregisters only that observer. The participant remains complete /// and other observers are unaffected. @@ -245,7 +245,7 @@ impl Clone for Wait { /// /// This does not register another participant. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping the returned future unregisters only that observer; see [`Wait`]. fn clone(&self) -> Self { diff --git a/asyncband/src/watch/mod.rs b/asyncband/src/watch/mod.rs index 57020256..8a14a958 100644 --- a/asyncband/src/watch/mod.rs +++ b/asyncband/src/watch/mod.rs @@ -310,7 +310,7 @@ impl Receiver { /// Returns [`RecvError::Disconnected`] when all senders have been dropped and no unseen /// version remains. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping a pending future leaves the observed version unchanged. A later call can still /// observe the latest unseen version. @@ -337,7 +337,7 @@ impl Receiver { /// Returns [`RecvError::Disconnected`] when all senders have been dropped and no unseen /// version remains. /// - /// # Cancel safety + /// # Cancellation /// /// Dropping a pending future leaves the observed version unchanged. A later call can still /// observe the latest unseen version.