diff --git a/CHANGELOG.md b/CHANGELOG.md index 666c4e8..860923a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ All notable changes to this project will be documented in this file. * Add opt-in `asyncband::once::LazyCell` for values that own one asynchronous initializer and preserve its in-flight future across caller cancellation. * Add opt-in bounded and unbounded runtime-agnostic object pools under `asyncband::pool`. * Add an opt-in `asyncband::blocking::FutureExt` bridge with `block_on` and `wait_timeout` methods for waiting on runtime-agnostic futures from synchronous code. +* Add an opt-in runtime-agnostic `Phaser` with dynamic RAII participants, reusable phases, and cancellation-resilient arrival semantics. ### Bug fixes diff --git a/Cargo.lock b/Cargo.lock index d293387..fe5c9f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -82,6 +82,7 @@ version = "0.6.7" dependencies = [ "hashbrown", "tokio", + "tokio-test", ] [[package]] diff --git a/README.md b/README.md index c55cc8a..10a21e0 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,7 @@ Runnable examples live in the [`examples`](examples) workspace crate. They demon | | [`Completion`](https://docs.rs/asyncband/*/asyncband/completion/struct.Completion.html) | `completion` | Publish one shared result to any number of current and future observers. | | | [`ManualResetEvent`](https://docs.rs/asyncband/*/asyncband/event/struct.ManualResetEvent.html) | `event` | Signal current and future waits until explicitly reset. | | | [`Latch`](https://docs.rs/asyncband/*/asyncband/latch/struct.Latch.html) | `latch` | Wait until a fixed one-way countdown reaches zero. | +| | [`Phaser`](https://docs.rs/asyncband/*/asyncband/phaser/struct.Phaser.html) | `phaser` | Coordinate repeated phases with a dynamic participant set. | | | [`WaitGroup`](https://docs.rs/asyncband/*/asyncband/waitgroup/struct.WaitGroup.html) | `waitgroup` | Wait until all cloned worker handles are dropped. | | | [`Shutdown`](https://docs.rs/asyncband/*/asyncband/shutdown/struct.Shutdown.html) | `shutdown` | Request shutdown and wait until all completion guards are dropped. | | Channels | [`oneshot`](https://docs.rs/asyncband/*/asyncband/oneshot/) | `oneshot` | Send one value from one sender to one receiver. | diff --git a/asyncband/Cargo.toml b/asyncband/Cargo.toml index d76cad3..c9d726e 100644 --- a/asyncband/Cargo.toml +++ b/asyncband/Cargo.toml @@ -58,6 +58,7 @@ once = ["semaphore"] once-cell = ["semaphore"] once-map = ["dep:hashbrown", "once-cell"] oneshot = [] +phaser = [] pool = ["semaphore"] rwlock = [] semaphore = [] @@ -73,6 +74,7 @@ hashbrown = { workspace = true, default-features = false, features = [ [dev-dependencies] tokio = { workspace = true, features = ["full"] } +tokio-test = { workspace = true } [lints] workspace = true diff --git a/asyncband/src/internal/mod.rs b/asyncband/src/internal/mod.rs index 143c60b..4388e5f 100644 --- a/asyncband/src/internal/mod.rs +++ b/asyncband/src/internal/mod.rs @@ -26,6 +26,7 @@ pub(crate) mod atomic_waker; feature = "latch", feature = "mpsc", feature = "mutex", + feature = "phaser", feature = "rwlock", feature = "semaphore", feature = "waitgroup", @@ -56,6 +57,7 @@ pub(crate) mod value_cell; feature = "latch", feature = "mpsc", feature = "mutex", + feature = "phaser", feature = "rwlock", feature = "semaphore", feature = "waitgroup", @@ -94,6 +96,7 @@ pub(crate) mod waitlist; feature = "completion", feature = "latch", feature = "once", + feature = "phaser", feature = "waitgroup", feature = "watch", ))] diff --git a/asyncband/src/lib.rs b/asyncband/src/lib.rs index 934e28a..9859260 100644 --- a/asyncband/src/lib.rs +++ b/asyncband/src/lib.rs @@ -67,6 +67,7 @@ //! | | [`Completion`](completion::Completion) | `completion` | Publish one shared result to any number of current and future observers. | //! | | [`ManualResetEvent`](event::ManualResetEvent) | `event` | Signal current and future waits until explicitly reset. | //! | | [`Latch`](latch::Latch) | `latch` | Wait until a fixed one-way countdown reaches zero. | +//! | | [`Phaser`](phaser::Phaser) | `phaser` | Coordinate repeated phases with a dynamic participant set. | //! | | [`WaitGroup`](waitgroup::WaitGroup) | `waitgroup` | Wait until all cloned worker handles are dropped. | //! | | [`Shutdown`](shutdown::Shutdown) | `shutdown` | Request shutdown and wait until all completion guards are dropped. | //! | Channels | [`oneshot`] | `oneshot` | Send one value from one sender to one receiver. | @@ -146,6 +147,8 @@ pub mod mutex; pub mod once; #[cfg(feature = "oneshot")] pub mod oneshot; +#[cfg(feature = "phaser")] +pub mod phaser; #[cfg(feature = "pool")] pub mod pool; #[cfg(feature = "rwlock")] diff --git a/asyncband/src/phaser/mod.rs b/asyncband/src/phaser/mod.rs new file mode 100644 index 0000000..60d3728 --- /dev/null +++ b/asyncband/src/phaser/mod.rs @@ -0,0 +1,520 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//! A reusable phase barrier with a dynamic participant set. +//! +//! A phaser coordinates repeated rounds of work with dynamically registered parties. +//! +//! Each [`PhaserParticipant`] represents one registered party. +//! +//! A phase advances after every party registered for that phase has arrived or deregistered. +//! +//! Registration increases both the registered and current unarrived counts. +//! +//! Arrival reduces the unarrived count. +//! +//! Deregistration also removes a party from later phases. +//! +//! [`Phaser::arrived_parties`] is the difference between the registered and unarrived counts. +//! +//! All state transitions and waiter registration share one synchronization point. +//! +//! A registration racing with advancement joins either the phase before or after the advancement. +//! +//! Which phase it joins is determined by which operation linearizes first. +//! +//! A completed phase is never reopened. +//! +//! Dropping a registered participant is equivalent to arriving and deregistering. +//! +//! This prevents an abandoned task from permanently blocking phase advancement. +//! +//! Consequently, dropping the last outstanding participant can advance the phase. +//! +//! # Cancellation +//! +//! Waiting with [`Phaser::wait_for_advance`] never registers a party or records an arrival. +//! +//! Cancelling that wait only removes its waker. +//! +//! [`PhaserParticipant::arrive_and_wait`] commits its arrival when first polled. +//! +//! Constructing and dropping that future without polling has no effect. +//! +//! Cancelling after arrival does not retract it. +//! +//! A retry waits for the stored phase, even after advancement, without arriving in the next phase. +//! +//! # Zero parties +//! +//! A phaser with no registered parties is dormant rather than terminated. +//! +//! Completing the last party's phase advances once. +//! +//! A later registration joins the current dormant phase. +//! +//! # Phase identity +//! +//! Phases advance using wrapping arithmetic. +//! +//! [`Phase`] supports equality but no ordering or arithmetic contract across wraparound. +//! +//! Waiters compare phase identity instead of inferring transitions from party counts. +//! +//! # Examples +//! +//! ``` +//! use std::sync::Arc; +//! +//! use asyncband::phaser::Phaser; +//! +//! let phaser = Arc::new(Phaser::new()); +//! let initial = phaser.phase(); +//! let mut first = phaser.register(); +//! let second = phaser.register(); +//! +//! assert_eq!(first.arrive(), initial); +//! assert_eq!(second.arrive_and_deregister(), initial); +//! assert_ne!(phaser.phase(), initial); +//! ``` + +use std::fmt; +use std::future::Future; +use std::pin::Pin; +use std::sync::Arc; +use std::task::Context; +use std::task::Poll; +use std::task::Waker; + +use crate::internal::mutex::Mutex; +use crate::internal::waitset::WaitSet; +use crate::internal::waitset::WakerToken; +use crate::internal::waitset::wake_all; + +#[cfg(test)] +mod tests; + +/// The identity of one phaser generation. +/// +/// Phases advance with wrapping arithmetic. +/// +/// Equality is meaningful, but ordering across wraparound is not guaranteed. +/// +/// The numeric value is intended for diagnostics rather than synchronization arithmetic. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub struct Phase(u64); + +impl Phase { + /// Returns the underlying wrapping phase counter for diagnostics. + pub const fn get(self) -> u64 { + self.0 + } + + const fn next(self) -> Self { + Self(self.0.wrapping_add(1)) + } +} + +/// A reusable phase barrier with a dynamic participant set. +/// +/// Store a phaser in an [`Arc`] before registering participants. +/// +/// Each participant owns an `Arc` clone and can move into an independently spawned task. +#[derive(Debug)] +pub struct Phaser { + state: Mutex, +} + +struct PhaserState { + phase: Phase, + registered: u32, + unarrived: u32, + waiters: WaitSet, +} + +impl fmt::Debug for PhaserState { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("PhaserState") + .field("phase", &self.phase) + .field("registered", &self.registered) + .field("arrived", &(self.registered - self.unarrived)) + .field("unarrived", &self.unarrived) + .finish_non_exhaustive() + } +} + +impl Default for Phaser { + fn default() -> Self { + Self::new() + } +} + +impl Phaser { + /// Creates a dormant phaser with no registered parties. + /// + /// # Examples + /// + /// ``` + /// use asyncband::phaser::Phaser; + /// + /// let phaser = Phaser::new(); + /// assert_eq!(phaser.phase().get(), 0); + /// assert_eq!(phaser.registered_parties(), 0); + /// ``` + pub const fn new() -> Self { + Self { + state: Mutex::new(PhaserState { + phase: Phase(0), + registered: 0, + unarrived: 0, + waiters: WaitSet::new(), + }), + } + } + + /// Returns the current phase identity. + pub fn phase(&self) -> Phase { + self.state.lock().phase + } + + /// Returns the number of currently registered parties. + /// + /// This is an instantaneous observation and may change immediately after the method returns. + pub fn registered_parties(&self) -> u32 { + self.state.lock().registered + } + + /// Returns the number of registered parties that have arrived in the current phase. + /// + /// This is an instantaneous observation and may change immediately after the method returns. + pub fn arrived_parties(&self) -> u32 { + let state = self.state.lock(); + state.registered - state.unarrived + } + + /// Returns the number of registered parties that have not arrived in the current phase. + /// + /// This is an instantaneous observation and may change immediately after the method returns. + pub fn unarrived_parties(&self) -> u32 { + self.state.lock().unarrived + } + + /// Registers one unarrived party in the current phase. + /// + /// The returned participant owns an [`Arc`] clone of this phaser. + /// + /// Registration is linearized with phase advancement. + /// + /// A concurrent advancement places the participant in either adjacent phase, never both. + /// + /// # Panics + /// + /// Panics if the registered-party count would overflow `u32`. + pub fn register(self: &Arc) -> PhaserParticipant { + let phaser = Arc::clone(self); + let phase = self.register_inner(1); + PhaserParticipant { + phaser, + phase, + arrived: false, + registered: true, + pending_wait: None, + } + } + + /// Registers `parties` unarrived parties in one current-phase state transition. + /// + /// One participant handle is returned for each party. + /// + /// Passing zero returns an empty vector and leaves the phaser unchanged. + /// + /// Storage for all handles is reserved before registration is committed. + /// + /// # Panics + /// + /// Panics if either count cannot be represented by its public integer type. + pub fn register_many(self: &Arc, parties: u32) -> Vec { + let capacity = usize::try_from(parties) + .expect("Phaser participant count must fit in the platform's usize"); + let mut participants = Vec::with_capacity(capacity); + if parties == 0 { + return participants; + } + + let phase = self.register_inner(parties); + participants.extend((0..parties).map(|_| PhaserParticipant { + phaser: Arc::clone(self), + phase, + arrived: false, + registered: true, + pending_wait: None, + })); + participants + } + + /// Waits until the current phase differs from `observed`. + /// + /// This operation does not register a party and does not record an arrival. + /// + /// It resolves immediately when `observed` is no longer current. + /// + /// # Cancellation + /// + /// Cancelling only unregisters the current waker. + /// + /// It does not change any party count or committed arrival. + pub async fn wait_for_advance(&self, observed: Phase) -> Phase { + PhaserWait { + token: None, + observed, + phaser: self, + } + .await + } + + fn register_inner(&self, parties: u32) -> Phase { + let mut state = self.state.lock(); + let registered = state + .registered + .checked_add(parties) + .expect("Phaser registered-party count overflow"); + let unarrived = state + .unarrived + .checked_add(parties) + .expect("Phaser unarrived-party count overflow"); + state.registered = registered; + state.unarrived = unarrived; + state.phase + } + + fn record_arrival( + &self, + participant_phase: &mut Phase, + participant_arrived: &mut bool, + deregister: bool, + ) -> (Phase, Option>) { + { + let mut state = self.state.lock(); + if *participant_phase != state.phase { + *participant_phase = state.phase; + *participant_arrived = false; + } + + let arrival_phase = state.phase; + let discharged = if deregister { + state.registered = state + .registered + .checked_sub(1) + .expect("registered Phaser participant must have a registered party"); + if *participant_arrived { + false + } else { + state.unarrived = state + .unarrived + .checked_sub(1) + .expect("unarrived Phaser participant must have an arrival obligation"); + true + } + } else if *participant_arrived { + false + } else { + state.unarrived = state + .unarrived + .checked_sub(1) + .expect("unarrived Phaser participant must have an arrival obligation"); + *participant_arrived = true; + true + }; + + debug_assert!(state.unarrived <= state.registered); + let wakers = if discharged && state.unarrived == 0 { + state.phase = state.phase.next(); + state.unarrived = state.registered; + *participant_phase = state.phase; + *participant_arrived = false; + Some(state.waiters.drain().collect()) + } else { + None + }; + (arrival_phase, wakers) + } + } + + fn arrive( + &self, + participant_phase: &mut Phase, + participant_arrived: &mut bool, + deregister: bool, + ) -> Phase { + let (arrival_phase, wakers) = + self.record_arrival(participant_phase, participant_arrived, deregister); + if let Some(wakers) = wakers { + wake_all(wakers.into_iter()); + } + arrival_phase + } + + fn poll_wait( + &self, + token: &mut Option, + observed: Phase, + cx: &mut Context<'_>, + ) -> Poll { + let waker = cx.waker().clone(); + let _retired_waker = { + let mut state = self.state.lock(); + if state.phase != observed { + let phase = state.phase; + *token = None; + return Poll::Ready(phase); + } + state.waiters.register(token, waker) + }; + Poll::Pending + } + + fn unregister_waker(&self, token: &mut Option) { + if token.is_some() { + let _removed_waker = { + let mut state = self.state.lock(); + state.waiters.unregister(token) + }; + } + } +} + +/// A capability representing one registered party in a [`Phaser`]. +/// +/// A participant contributes at most one arrival to each phase. +/// +/// It owns an [`Arc`] that keeps its phaser alive. +/// +/// Dropping a registered participant is equivalent to arriving and deregistering. +#[must_use = "dropping a participant arrives and deregisters it from the phaser"] +#[derive(Debug)] +pub struct PhaserParticipant { + phaser: Arc, + phase: Phase, + arrived: bool, + registered: bool, + pending_wait: Option, +} + +impl PhaserParticipant { + /// Arrives in the current phase without waiting for it to advance. + /// + /// Repeated calls in one phase return its identity without changing counts again. + /// + /// Calling this after a cancelled `arrive_and_wait` abandons that pending wait. + /// + /// If advancement occurred, this records an arrival in the new current phase. + pub fn arrive(&mut self) -> Phase { + self.pending_wait = None; + self.phaser + .arrive(&mut self.phase, &mut self.arrived, false) + } + + /// Arrives in the current phase and waits for that phase to advance. + /// + /// The returned value is the new current phase. + /// + /// # Cancellation + /// + /// Arrival is committed when the returned future is first polled. + /// + /// Constructing and dropping an unpolled future has no effect. + /// + /// Cancelling after arrival removes only the waiter's waker. + /// + /// Retrying waits for the stored phase without arriving in a later phase. + pub async fn arrive_and_wait(&mut self) -> Phase { + let observed = match self.pending_wait { + Some(phase) => phase, + None => { + let (phase, wakers) = + self.phaser + .record_arrival(&mut self.phase, &mut self.arrived, false); + self.pending_wait = Some(phase); + if let Some(wakers) = wakers { + wake_all(wakers.into_iter()); + } + phase + } + }; + + let next = self.phaser.wait_for_advance(observed).await; + self.pending_wait = None; + self.phase = next; + self.arrived = false; + next + } + + /// Arrives in the current phase and deregisters from later phases. + /// + /// This consumes the participant and returns its final arrival phase. + /// + /// Any pending wait from a cancelled `arrive_and_wait` is abandoned. + pub fn arrive_and_deregister(mut self) -> Phase { + self.registered = false; + self.pending_wait = None; + self.phaser.arrive(&mut self.phase, &mut self.arrived, true) + } +} + +impl Drop for PhaserParticipant { + fn drop(&mut self) { + if self.registered { + self.registered = false; + self.pending_wait = None; + self.phaser.arrive(&mut self.phase, &mut self.arrived, true); + } + } +} + +#[must_use = "futures do nothing unless you `.await` or poll them"] +struct PhaserWait<'a> { + token: Option, + observed: Phase, + phaser: &'a Phaser, +} + +impl fmt::Debug for PhaserWait<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("PhaserWait") + .field("observed", &self.observed) + .finish_non_exhaustive() + } +} + +impl Future for PhaserWait<'_> { + type Output = Phase; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let Self { + token, + observed, + phaser, + } = self.get_mut(); + phaser.poll_wait(token, *observed, cx) + } +} + +impl Drop for PhaserWait<'_> { + fn drop(&mut self) { + self.phaser.unregister_waker(&mut self.token); + } +} diff --git a/asyncband/src/phaser/tests.rs b/asyncband/src/phaser/tests.rs new file mode 100644 index 0000000..3c0186e --- /dev/null +++ b/asyncband/src/phaser/tests.rs @@ -0,0 +1,410 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use std::future::Future; +use std::panic; +use std::sync::Arc; +use std::sync::atomic::AtomicUsize; +use std::sync::atomic::Ordering; +use std::task::Context; +use std::task::Poll; +use std::task::Wake; +use std::task::Waker; + +use tokio_test::assert_pending; +use tokio_test::assert_ready; +use tokio_test::task::spawn; + +use super::Phaser; + +struct CountWake(AtomicUsize); + +impl Wake for CountWake { + fn wake(self: Arc) { + self.0.fetch_add(1, Ordering::Relaxed); + } +} + +struct PanicWake; + +impl Wake for PanicWake { + fn wake(self: Arc) { + panic!("wake failed"); + } +} + +#[test] +fn register_many_joins_one_observed_phase() { + let phaser = Arc::new(Phaser::new()); + let participants = phaser.register_many(3); + + assert_eq!(participants.len(), 3); + assert_eq!(phaser.registered_parties(), 3); + assert_eq!(phaser.unarrived_parties(), 3); +} + +#[test] +fn registering_zero_parties_is_a_noop() { + let phaser = Arc::new(Phaser::new()); + let phase = phaser.phase(); + + assert!(phaser.register_many(0).is_empty()); + assert_eq!(phaser.phase(), phase); + assert_eq!(phaser.registered_parties(), 0); + assert_eq!(phaser.unarrived_parties(), 0); +} + +#[test] +fn participants_advance_across_repeated_phases() { + let phaser = Arc::new(Phaser::new()); + let phase0 = phaser.phase(); + let mut first = phaser.register(); + let mut second = phaser.register(); + + assert_eq!(first.arrive(), phase0); + assert_eq!(phaser.arrived_parties(), 1); + assert_eq!(second.arrive(), phase0); + let phase1 = phaser.phase(); + assert_ne!(phase1, phase0); + assert_eq!(phaser.arrived_parties(), 0); + + assert_eq!(first.arrive(), phase1); + assert_eq!(second.arrive(), phase1); + assert_ne!(phaser.phase(), phase1); +} + +#[test] +fn unpolled_arrive_and_wait_future_does_not_arrive() { + let phaser = Arc::new(Phaser::new()); + let mut participant = phaser.register(); + + let wait = participant.arrive_and_wait(); + + assert_eq!(phaser.arrived_parties(), 0); + drop(wait); + assert_eq!(phaser.arrived_parties(), 0); +} + +#[test] +fn cancelled_arrive_and_wait_retry_waits_for_original_phase_after_advance() { + let phaser = Arc::new(Phaser::new()); + let phase0 = phaser.phase(); + let mut first = phaser.register(); + let mut second = phaser.register(); + + { + let mut cancelled = spawn(first.arrive_and_wait()); + assert_pending!(cancelled.poll()); + } + + assert_eq!(phaser.arrived_parties(), 1); + assert_eq!(second.arrive(), phase0); + let phase1 = phaser.phase(); + assert_ne!(phase1, phase0); + assert_eq!(phaser.arrived_parties(), 0); + + let mut retry = spawn(first.arrive_and_wait()); + assert_eq!(assert_ready!(retry.poll()), phase1); + assert_eq!(phaser.arrived_parties(), 0); +} + +#[test] +fn cancelled_arrive_and_wait_retry_before_advance_does_not_arrive_twice() { + let phaser = Arc::new(Phaser::new()); + let mut first = phaser.register(); + let mut second = phaser.register(); + + { + let mut cancelled = spawn(first.arrive_and_wait()); + assert_pending!(cancelled.poll()); + } + + let mut retry = spawn(first.arrive_and_wait()); + assert_pending!(retry.poll()); + assert_eq!(phaser.arrived_parties(), 1); + + second.arrive(); + assert_ready!(retry.poll()); +} + +#[test] +fn dropping_last_participant_advances_once_and_dormant_phaser_can_be_reused() { + let phaser = Arc::new(Phaser::new()); + let phase0 = phaser.phase(); + let participant = phaser.register(); + + drop(participant); + let phase1 = phaser.phase(); + assert_ne!(phase1, phase0); + assert_eq!(phaser.registered_parties(), 0); + assert_eq!(phaser.arrived_parties(), 0); + + let mut participant = phaser.register(); + assert_eq!(participant.arrive(), phase1); + assert_ne!(phaser.phase(), phase1); +} + +#[test] +fn dropping_an_arrived_participant_only_removes_its_next_phase_registration() { + let phaser = Arc::new(Phaser::new()); + let phase0 = phaser.phase(); + let mut first = phaser.register(); + let mut second = phaser.register(); + + first.arrive(); + drop(first); + assert_eq!(phaser.phase(), phase0); + assert_eq!(phaser.registered_parties(), 1); + assert_eq!(phaser.unarrived_parties(), 1); + + second.arrive(); + assert_ne!(phaser.phase(), phase0); +} + +#[test] +fn registration_before_last_arrival_joins_and_delays_current_phase() { + let phaser = Arc::new(Phaser::new()); + let phase = phaser.phase(); + let mut first = phaser.register(); + let mut second = phaser.register(); + + first.arrive(); + let mut joining = phaser.register(); + second.arrive(); + + assert_eq!(phaser.phase(), phase); + assert_eq!(phaser.unarrived_parties(), 1); + joining.arrive(); + assert_ne!(phaser.phase(), phase); +} + +#[test] +fn registration_after_last_arrival_joins_the_advanced_phase() { + let phaser = Arc::new(Phaser::new()); + let phase0 = phaser.phase(); + let mut first = phaser.register(); + + first.arrive(); + let phase1 = phaser.phase(); + assert_ne!(phase1, phase0); + + let mut joining = phaser.register(); + assert_eq!(phaser.registered_parties(), 2); + assert_eq!(phaser.unarrived_parties(), 2); + assert_eq!(joining.arrive(), phase1); + assert_eq!(phaser.phase(), phase1); +} + +#[test] +fn registration_before_last_participant_drop_joins_the_current_phase() { + let phaser = Arc::new(Phaser::new()); + let phase0 = phaser.phase(); + let participant = phaser.register(); + let joining = phaser.register(); + + drop(participant); + + assert_eq!(phaser.phase(), phase0); + assert_eq!(phaser.registered_parties(), 1); + assert_eq!(phaser.unarrived_parties(), 1); + assert_eq!(joining.arrive_and_deregister(), phase0); + assert_ne!(phaser.phase(), phase0); +} + +#[test] +fn registration_after_last_participant_drop_joins_the_advanced_phase() { + let phaser = Arc::new(Phaser::new()); + let phase0 = phaser.phase(); + let participant = phaser.register(); + + drop(participant); + let phase1 = phaser.phase(); + let joining = phaser.register(); + + assert_ne!(phase1, phase0); + assert_eq!(phaser.registered_parties(), 1); + assert_eq!(phaser.unarrived_parties(), 1); + assert_eq!(joining.arrive_and_deregister(), phase1); + assert_ne!(phaser.phase(), phase1); +} + +#[test] +fn wait_for_advance_is_a_cancel_safe_non_participant_observer() { + let phaser = Arc::new(Phaser::new()); + let phase = phaser.phase(); + let counter = Arc::new(CountWake(AtomicUsize::new(0))); + let waker = Waker::from(Arc::clone(&counter)); + let mut context = Context::from_waker(&waker); + + { + let mut wait = Box::pin(phaser.wait_for_advance(phase)); + assert_eq!(Future::poll(wait.as_mut(), &mut context), Poll::Pending); + assert_eq!(phaser.registered_parties(), 0); + } + + assert_eq!(phaser.registered_parties(), 0); + let participant = phaser.register(); + drop(participant); + assert_eq!(counter.0.load(Ordering::Relaxed), 0); +} + +#[test] +fn advancing_a_phase_wakes_every_registered_waiter_once() { + let phaser = Arc::new(Phaser::new()); + let observed = phaser.phase(); + let participant = phaser.register(); + let first_counter = Arc::new(CountWake(AtomicUsize::new(0))); + let second_counter = Arc::new(CountWake(AtomicUsize::new(0))); + let first_waker = Waker::from(Arc::clone(&first_counter)); + let second_waker = Waker::from(Arc::clone(&second_counter)); + let mut first_context = Context::from_waker(&first_waker); + let mut second_context = Context::from_waker(&second_waker); + let mut first_wait = Box::pin(phaser.wait_for_advance(observed)); + let mut second_wait = Box::pin(phaser.wait_for_advance(observed)); + + assert_eq!( + Future::poll(first_wait.as_mut(), &mut first_context), + Poll::Pending + ); + assert_eq!( + Future::poll(second_wait.as_mut(), &mut second_context), + Poll::Pending + ); + + drop(participant); + assert_eq!(first_counter.0.load(Ordering::Relaxed), 1); + assert_eq!(second_counter.0.load(Ordering::Relaxed), 1); + assert!(matches!( + Future::poll(first_wait.as_mut(), &mut first_context), + Poll::Ready(_) + )); + assert!(matches!( + Future::poll(second_wait.as_mut(), &mut second_context), + Poll::Ready(_) + )); +} + +#[test] +fn cancelling_a_woken_waiter_does_not_unregister_a_next_phase_waiter() { + let phaser = Arc::new(Phaser::new()); + let phase0 = phaser.phase(); + let participant = phaser.register(); + let stale_counter = Arc::new(CountWake(AtomicUsize::new(0))); + let stale_waker = Waker::from(Arc::clone(&stale_counter)); + let mut stale_context = Context::from_waker(&stale_waker); + let mut stale_wait = Box::pin(phaser.wait_for_advance(phase0)); + + assert_eq!( + Future::poll(stale_wait.as_mut(), &mut stale_context), + Poll::Pending + ); + drop(participant); + let phase1 = phaser.phase(); + assert_ne!(phase1, phase0); + assert_eq!(stale_counter.0.load(Ordering::Relaxed), 1); + + let participant = phaser.register(); + let current_counter = Arc::new(CountWake(AtomicUsize::new(0))); + let current_waker = Waker::from(Arc::clone(¤t_counter)); + let mut current_context = Context::from_waker(¤t_waker); + let mut current_wait = Box::pin(phaser.wait_for_advance(phase1)); + assert_eq!( + Future::poll(current_wait.as_mut(), &mut current_context), + Poll::Pending + ); + + drop(stale_wait); + drop(participant); + assert_eq!(current_counter.0.load(Ordering::Relaxed), 1); + assert!(matches!( + Future::poll(current_wait.as_mut(), &mut current_context), + Poll::Ready(_) + )); +} + +#[test] +fn panicking_waker_does_not_lose_an_arrive_and_wait_phase() { + let phaser = Arc::new(Phaser::new()); + let phase0 = phaser.phase(); + let mut first = phaser.register(); + let mut second = phaser.register(); + let panic_waker = Waker::from(Arc::new(PanicWake)); + let mut panic_context = Context::from_waker(&panic_waker); + let mut observer = Box::pin(phaser.wait_for_advance(phase0)); + + assert_eq!( + Future::poll(observer.as_mut(), &mut panic_context), + Poll::Pending + ); + assert_eq!(first.arrive(), phase0); + + let polling_waker = Waker::from(Arc::new(CountWake(AtomicUsize::new(0)))); + let mut polling_context = Context::from_waker(&polling_waker); + let mut wait = Box::pin(second.arrive_and_wait()); + let result = panic::catch_unwind(panic::AssertUnwindSafe(|| { + Future::poll(wait.as_mut(), &mut polling_context) + })); + + assert!(result.is_err()); + drop(wait); + drop(observer); + + let phase1 = phaser.phase(); + assert_ne!(phase1, phase0); + assert_eq!(phaser.arrived_parties(), 0); + + let mut retry = spawn(second.arrive_and_wait()); + assert_eq!(assert_ready!(retry.poll()), phase1); + assert_eq!(phaser.arrived_parties(), 0); +} + +#[test] +fn a_late_waiter_for_a_completed_phase_is_immediately_ready() { + let phaser = Arc::new(Phaser::new()); + let observed = phaser.phase(); + let participant = phaser.register(); + drop(participant); + + let mut wait = spawn(phaser.wait_for_advance(observed)); + assert_eq!(assert_ready!(wait.poll()), phaser.phase()); +} + +#[test] +fn phase_identity_wraps_without_an_ordering_contract() { + let phaser = Arc::new(Phaser::new()); + phaser.state.lock().phase = super::Phase(u64::MAX); + let observed = phaser.phase(); + let mut participant = phaser.register(); + + assert_eq!(participant.arrive(), observed); + assert_eq!(phaser.phase().get(), 0); + assert_ne!(phaser.phase(), observed); +} + +#[test] +fn registration_overflow_panics_without_partially_updating_state() { + let phaser = Arc::new(Phaser::new()); + { + let mut state = phaser.state.lock(); + state.registered = u32::MAX; + state.unarrived = u32::MAX; + } + + assert!(panic::catch_unwind(|| phaser.register()).is_err()); + assert_eq!(phaser.registered_parties(), u32::MAX); + assert_eq!(phaser.unarrived_parties(), u32::MAX); +} diff --git a/tests-integration/Cargo.toml b/tests-integration/Cargo.toml index 04df589..eb419c3 100644 --- a/tests-integration/Cargo.toml +++ b/tests-integration/Cargo.toml @@ -41,6 +41,7 @@ asyncband = { workspace = true, features = [ "once-cell", "once-map", "oneshot", + "phaser", "pool", "rwlock", "semaphore", diff --git a/tests-integration/tests/phaser_test.rs b/tests-integration/tests/phaser_test.rs new file mode 100644 index 0000000..33287b9 --- /dev/null +++ b/tests-integration/tests/phaser_test.rs @@ -0,0 +1,52 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use std::sync::Arc; + +use asyncband::phaser::Phaser; + +#[tokio::test] +async fn participant_can_wait_from_a_spawned_task() { + let phaser = Arc::new(Phaser::new()); + let mut first = phaser.register(); + let mut second = phaser.register(); + + let first_wait = tokio::spawn(async move { first.arrive_and_wait().await }); + + tokio::task::yield_now().await; + let phase = second.arrive(); + assert_eq!(first_wait.await.unwrap(), phaser.phase()); + assert_ne!(phase, phaser.phase()); +} + +#[tokio::test] +async fn observer_waits_without_becoming_a_party() { + let phaser = Arc::new(Phaser::new()); + let observed = phaser.phase(); + let mut first = phaser.register(); + let second = phaser.register(); + let observer_phaser = Arc::clone(&phaser); + let observer = tokio::spawn(async move { observer_phaser.wait_for_advance(observed).await }); + + tokio::task::yield_now().await; + assert_eq!(phaser.registered_parties(), 2); + first.arrive(); + second.arrive_and_deregister(); + + assert_eq!(observer.await.unwrap(), phaser.phase()); + assert_eq!(phaser.registered_parties(), 1); +} diff --git a/tests-integration/tests/traits_test.rs b/tests-integration/tests/traits_test.rs index d868368..b19dd9d 100644 --- a/tests-integration/tests/traits_test.rs +++ b/tests-integration/tests/traits_test.rs @@ -31,6 +31,9 @@ use asyncband::once::Once; use asyncband::once::OnceCell; use asyncband::once::OnceMap; use asyncband::oneshot; +use asyncband::phaser::Phase; +use asyncband::phaser::Phaser; +use asyncband::phaser::PhaserParticipant; use asyncband::pool; use asyncband::pool::ManageObject; use asyncband::pool::ObjectStatus; @@ -100,6 +103,9 @@ fn public_types_are_send_and_sync() { assert_send_and_sync::(); assert_send_and_sync::>(); assert_send_and_sync::>(); + assert_send_and_sync::(); + assert_send_and_sync::(); + assert_send_and_sync::(); assert_send_and_sync::>(); assert_send_and_sync::>(); assert_send_and_sync::>(); @@ -168,6 +174,9 @@ fn public_types_are_unpin() { assert_unpin::>(); assert_unpin::>(); assert_unpin::>(); + assert_unpin::(); + assert_unpin::(); + assert_unpin::(); assert_unpin::>(); assert_unpin::>(); assert_unpin::>();