From 50db04e4b03a52b310a63a98e383accfcf10acb5 Mon Sep 17 00:00:00 2001 From: ButterBright Date: Mon, 31 Aug 2026 00:08:08 +0800 Subject: [PATCH 01/12] feat(phaser): add reusable phase coordination --- CHANGELOG.md | 1 + Cargo.lock | 1 + README.md | 1 + asyncband/Cargo.toml | 2 + asyncband/src/internal/mod.rs | 3 + asyncband/src/lib.rs | 3 + asyncband/src/phaser/mod.rs | 520 +++++++++++++++++++++++++ asyncband/src/phaser/tests.rs | 392 +++++++++++++++++++ tests-integration/Cargo.toml | 1 + tests-integration/tests/phaser_test.rs | 52 +++ tests-integration/tests/traits_test.rs | 9 + 11 files changed, 985 insertions(+) create mode 100644 asyncband/src/phaser/mod.rs create mode 100644 asyncband/src/phaser/tests.rs create mode 100644 tests-integration/tests/phaser_test.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 0543a046..2fc55f81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ This non-ASF release was not approved by the Apache Incubator PMC, is not an act * 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 c6baa36c..457d7eed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -82,6 +82,7 @@ version = "0.7.2" dependencies = [ "hashbrown", "tokio", + "tokio-test", ] [[package]] diff --git a/README.md b/README.md index 1203ae01..47bb7629 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ Runnable examples live in the [`examples`](examples) workspace crate. They demon | | [`Barrier`](https://docs.rs/asyncband/*/asyncband/barrier/struct.Barrier.html) | `barrier` | Synchronize a fixed number of participants at a reusable rendezvous. | | | [`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` | Dynamically register participants and wait until all have completed. | | | [`Shutdown`](https://docs.rs/asyncband/*/asyncband/shutdown/struct.Shutdown.html) | `shutdown` | Request shutdown and wait until all completion guards are dropped. | | Work coalescing | [`Once`](https://docs.rs/asyncband/*/asyncband/once/struct.Once.html) | `once` | Complete one asynchronous initialization; cancelled or panicked attempts may be retried. | diff --git a/asyncband/Cargo.toml b/asyncband/Cargo.toml index d2d74f45..c02e8e43 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 c6363791..d028e899 100644 --- a/asyncband/src/internal/mod.rs +++ b/asyncband/src/internal/mod.rs @@ -60,6 +60,7 @@ pub(crate) mod atomic_waker; feature = "latch", feature = "mpsc", feature = "mutex", + feature = "phaser", feature = "rwlock", feature = "semaphore", feature = "waitgroup", @@ -87,6 +88,7 @@ pub(crate) mod value_cell; feature = "latch", feature = "mpsc", feature = "mutex", + feature = "phaser", feature = "rwlock", feature = "semaphore", feature = "waitgroup", @@ -143,6 +145,7 @@ pub(crate) mod waker_batch; 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 93ce485e..966c20df 100644 --- a/asyncband/src/lib.rs +++ b/asyncband/src/lib.rs @@ -63,6 +63,7 @@ //! | | [`Barrier`](barrier::Barrier) | `barrier` | Synchronize a fixed number of participants at a reusable rendezvous. | //! | | [`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` | Dynamically register participants and wait until all have completed. | //! | | [`Shutdown`](shutdown::Shutdown) | `shutdown` | Request shutdown and wait until all completion guards are dropped. | //! | Work coalescing | [`Once`](once::Once) | `once` | Complete one asynchronous initialization; cancelled or panicked attempts may be retried. | @@ -145,6 +146,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 00000000..44be1324 --- /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 + 'static>) { + { + 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.take_wakers()) + } 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); + } + arrival_phase + } + + fn poll_wait( + &self, + token: &mut Option, + observed: Phase, + cx: &mut Context<'_>, + ) -> Poll { + let replaced_waker = { + let mut state = self.state.lock(); + if state.phase != observed { + *token = None; + return Poll::Ready(state.phase); + } + state.waiters.register_waker(token, cx) + }; + drop(replaced_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_waker(token) + }; + drop(removed_waker); + } + } +} + +/// 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); + } + 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 00000000..7882bfea --- /dev/null +++ b/asyncband/src/phaser/tests.rs @@ -0,0 +1,392 @@ +// 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 mut wait = spawn(phaser.wait_for_advance(phase)); + assert_pending!(wait.poll()); + assert_eq!(phaser.registered_parties(), 0); + assert!(!phaser.state.lock().waiters.is_empty()); + } + + assert_eq!(phaser.registered_parties(), 0); + assert!(phaser.state.lock().waiters.is_empty()); +} + +#[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 mut stale_wait = spawn(phaser.wait_for_advance(phase0)); + + assert_pending!(stale_wait.poll()); + drop(participant); + let phase1 = phaser.phase(); + assert_ne!(phase1, phase0); + + let participant = phaser.register(); + let mut current_wait = spawn(phaser.wait_for_advance(phase1)); + assert_pending!(current_wait.poll()); + assert!(!phaser.state.lock().waiters.is_empty()); + + drop(stale_wait); + assert!(!phaser.state.lock().waiters.is_empty()); + + drop(participant); + assert_ready!(current_wait.poll()); +} + +#[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 04df5894..eb419c32 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 00000000..33287b9e --- /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 bf10a0e7..c05f36dd 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::>(); @@ -169,6 +175,9 @@ fn public_types_are_unpin() { assert_unpin::>(); assert_unpin::>(); assert_unpin::>(); + assert_unpin::(); + assert_unpin::(); + assert_unpin::(); assert_unpin::>(); assert_unpin::>(); assert_unpin::>(); From 62af66909f1996eba1cb5fc92756c72c475ef530 Mon Sep 17 00:00:00 2001 From: ButterBright Date: Mon, 31 Aug 2026 00:36:04 +0800 Subject: [PATCH 02/12] fix(phaser): adapt to current wait set API --- asyncband/src/phaser/mod.rs | 16 ++++++------- asyncband/src/phaser/tests.rs | 42 +++++++++++++++++++++++++---------- 2 files changed, 38 insertions(+), 20 deletions(-) diff --git a/asyncband/src/phaser/mod.rs b/asyncband/src/phaser/mod.rs index 44be1324..780686c8 100644 --- a/asyncband/src/phaser/mod.rs +++ b/asyncband/src/phaser/mod.rs @@ -346,7 +346,7 @@ impl Phaser { state.unarrived = state.registered; *participant_phase = state.phase; *participant_arrived = false; - Some(state.waiters.take_wakers()) + Some(state.waiters.drain()) } else { None }; @@ -374,25 +374,25 @@ impl Phaser { observed: Phase, cx: &mut Context<'_>, ) -> Poll { - let replaced_waker = { + 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(state.phase); + return Poll::Ready(phase); } - state.waiters.register_waker(token, cx) + state.waiters.register(token, waker) }; - drop(replaced_waker); Poll::Pending } fn unregister_waker(&self, token: &mut Option) { if token.is_some() { - let removed_waker = { + let _removed_waker = { let mut state = self.state.lock(); - state.waiters.unregister_waker(token) + state.waiters.unregister(token) }; - drop(removed_waker); } } } diff --git a/asyncband/src/phaser/tests.rs b/asyncband/src/phaser/tests.rs index 7882bfea..3c0186e1 100644 --- a/asyncband/src/phaser/tests.rs +++ b/asyncband/src/phaser/tests.rs @@ -246,16 +246,20 @@ fn registration_after_last_participant_drop_joins_the_advanced_phase() { 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 = spawn(phaser.wait_for_advance(phase)); - assert_pending!(wait.poll()); + 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!(!phaser.state.lock().waiters.is_empty()); } assert_eq!(phaser.registered_parties(), 0); - assert!(phaser.state.lock().waiters.is_empty()); + let participant = phaser.register(); + drop(participant); + assert_eq!(counter.0.load(Ordering::Relaxed), 0); } #[test] @@ -299,23 +303,37 @@ 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 mut stale_wait = spawn(phaser.wait_for_advance(phase0)); + 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_pending!(stale_wait.poll()); + 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 mut current_wait = spawn(phaser.wait_for_advance(phase1)); - assert_pending!(current_wait.poll()); - assert!(!phaser.state.lock().waiters.is_empty()); + 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); - assert!(!phaser.state.lock().waiters.is_empty()); - drop(participant); - assert_ready!(current_wait.poll()); + assert_eq!(current_counter.0.load(Ordering::Relaxed), 1); + assert!(matches!( + Future::poll(current_wait.as_mut(), &mut current_context), + Poll::Ready(_) + )); } #[test] From 2e39b58c5b6104c12526f5910497bd5dfdcf369c Mon Sep 17 00:00:00 2001 From: ButterBright Date: Mon, 31 Aug 2026 19:19:00 +0800 Subject: [PATCH 03/12] fix(phaser): own drained wakers before unlocking --- asyncband/src/phaser/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/asyncband/src/phaser/mod.rs b/asyncband/src/phaser/mod.rs index 780686c8..60d37286 100644 --- a/asyncband/src/phaser/mod.rs +++ b/asyncband/src/phaser/mod.rs @@ -306,7 +306,7 @@ impl Phaser { participant_phase: &mut Phase, participant_arrived: &mut bool, deregister: bool, - ) -> (Phase, Option + 'static>) { + ) -> (Phase, Option>) { { let mut state = self.state.lock(); if *participant_phase != state.phase { @@ -346,7 +346,7 @@ impl Phaser { state.unarrived = state.registered; *participant_phase = state.phase; *participant_arrived = false; - Some(state.waiters.drain()) + Some(state.waiters.drain().collect()) } else { None }; @@ -363,7 +363,7 @@ impl Phaser { let (arrival_phase, wakers) = self.record_arrival(participant_phase, participant_arrived, deregister); if let Some(wakers) = wakers { - wake_all(wakers); + wake_all(wakers.into_iter()); } arrival_phase } @@ -450,7 +450,7 @@ impl PhaserParticipant { .record_arrival(&mut self.phase, &mut self.arrived, false); self.pending_wait = Some(phase); if let Some(wakers) = wakers { - wake_all(wakers); + wake_all(wakers.into_iter()); } phase } From 234b62c19476cd5294b634d5fe9a82ed3be483e1 Mon Sep 17 00:00:00 2001 From: ButterBright Date: Sat, 5 Sep 2026 12:02:02 +0800 Subject: [PATCH 04/12] fix(phaser): adapt waiter lifecycle to waker set --- asyncband/src/internal/mod.rs | 1 + asyncband/src/phaser/mod.rs | 49 +++++++++++++++++++---------------- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/asyncband/src/internal/mod.rs b/asyncband/src/internal/mod.rs index d028e899..a4d50e0d 100644 --- a/asyncband/src/internal/mod.rs +++ b/asyncband/src/internal/mod.rs @@ -129,6 +129,7 @@ pub(crate) mod waitlist; feature = "mpsc", feature = "mutex", feature = "once", + feature = "phaser", feature = "rwlock", feature = "semaphore", feature = "waitgroup", diff --git a/asyncband/src/phaser/mod.rs b/asyncband/src/phaser/mod.rs index 60d37286..09abf882 100644 --- a/asyncband/src/phaser/mod.rs +++ b/asyncband/src/phaser/mod.rs @@ -101,9 +101,9 @@ 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; +use crate::internal::wake_all; +use crate::internal::wakerset::WakerSet; +use crate::internal::wakerset::WakerToken; #[cfg(test)] mod tests; @@ -143,7 +143,7 @@ struct PhaserState { phase: Phase, registered: u32, unarrived: u32, - waiters: WaitSet, + waiters: WakerSet, } impl fmt::Debug for PhaserState { @@ -181,7 +181,7 @@ impl Phaser { phase: Phase(0), registered: 0, unarrived: 0, - waiters: WaitSet::new(), + waiters: WakerSet::new(), }), } } @@ -374,26 +374,31 @@ impl Phaser { 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) - }; + let mut state = self.state.lock(); + if state.phase != observed { + let phase = state.phase; + *token = None; + return Poll::Ready(phase); + } + + let _retired_waker = state.waiters.register(token, cx.waker()); + drop(state); 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) - }; + fn unregister_waker(&self, token: &mut Option, observed: Phase) { + if token.is_none() { + return; } + + let mut state = self.state.lock(); + if state.phase != observed { + *token = None; + return; + } + + let _removed_waker = state.waiters.unregister(token); + drop(state); } } @@ -515,6 +520,6 @@ impl Future for PhaserWait<'_> { impl Drop for PhaserWait<'_> { fn drop(&mut self) { - self.phaser.unregister_waker(&mut self.token); + self.phaser.unregister_waker(&mut self.token, self.observed); } } From afe0027381764fb75e0fd6bb2345dffc433357a6 Mon Sep 17 00:00:00 2001 From: ButterBright Date: Sun, 6 Sep 2026 22:00:15 +0800 Subject: [PATCH 05/12] feat(phaser): expose participant phaser --- CHANGELOG.md | 5 +++- asyncband/src/phaser/mod.rs | 50 +++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fc55f81..34801962 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. ## Unreleased +### New features + +* Add an opt-in runtime-agnostic `Phaser` with dynamic RAII participants, reusable phases, and cancellation-resilient arrival semantics. + ## v0.7.2 ### Improvements @@ -45,7 +49,6 @@ This non-ASF release was not approved by the Apache Incubator PMC, is not an act * 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/asyncband/src/phaser/mod.rs b/asyncband/src/phaser/mod.rs index 09abf882..9b49e910 100644 --- a/asyncband/src/phaser/mod.rs +++ b/asyncband/src/phaser/mod.rs @@ -45,6 +45,17 @@ //! //! Consequently, dropping the last outstanding participant can advance the phase. //! +//! # Synchronization +//! +//! A participant's arrival establishes a happens-before relationship between operations performed +//! before the arrival and operations performed after a wait observes completion of that phase. +//! +//! This guarantee applies to both [`Phaser::wait_for_advance`] and +//! [`PhaserParticipant::arrive_and_wait`], including waits that find the phase already completed. +//! +//! Arriving and deregistering, including when a participant is dropped, provides the same guarantee +//! for operations preceding that arrival. +//! //! # Cancellation //! //! Waiting with [`Phaser::wait_for_advance`] never registers a party or records an arrival. @@ -91,6 +102,36 @@ //! assert_eq!(second.arrive_and_deregister(), initial); //! assert_ne!(phaser.phase(), initial); //! ``` +//! +//! Participants can coordinate several rounds from separate tasks, using either combined or +//! separate arrival and waiting: +//! +//! ``` +//! use std::sync::Arc; +//! +//! use asyncband::phaser::Phaser; +//! +//! # #[tokio::main(flavor = "current_thread")] +//! # async fn main() { +//! let phaser = Arc::new(Phaser::new()); +//! let mut first = phaser.register(); +//! let mut second = phaser.register(); +//! +//! let task = tokio::spawn(async move { +//! for _ in 0..3 { +//! // Finish this round's work before recording arrival. +//! let observed = first.arrive(); +//! // Independent work can run here before waiting for the other party. +//! first.phaser().wait_for_advance(observed).await; +//! } +//! }); +//! +//! for _ in 0..3 { +//! second.arrive_and_wait().await; +//! } +//! task.await.unwrap(); +//! # } +//! ``` use std::fmt; use std::future::Future; @@ -420,6 +461,15 @@ pub struct PhaserParticipant { } impl PhaserParticipant { + /// Returns the phaser this participant is registered with. + /// + /// This borrows the existing phaser without cloning its [`Arc`] or registering another party. + /// + /// Use this to call [`Phaser::wait_for_advance`] after a separate [`Self::arrive`] operation. + pub fn phaser(&self) -> &Phaser { + &self.phaser + } + /// Arrives in the current phase without waiting for it to advance. /// /// Repeated calls in one phase return its identity without changing counts again. From 73ee5f4e5db9f7194b8587f78b12114b7c9d90e7 Mon Sep 17 00:00:00 2001 From: tison Date: Fri, 11 Sep 2026 07:50:58 +0800 Subject: [PATCH 06/12] fixup Signed-off-by: tison --- Cargo.lock | 1 - asyncband/Cargo.toml | 1 - asyncband/src/lib.rs | 5 ++++- asyncband/src/phaser/tests.rs | 31 ++++++++++++++----------------- 4 files changed, 18 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 457d7eed..c6baa36c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -82,7 +82,6 @@ version = "0.7.2" dependencies = [ "hashbrown", "tokio", - "tokio-test", ] [[package]] diff --git a/asyncband/Cargo.toml b/asyncband/Cargo.toml index c02e8e43..19c97562 100644 --- a/asyncband/Cargo.toml +++ b/asyncband/Cargo.toml @@ -74,7 +74,6 @@ 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/lib.rs b/asyncband/src/lib.rs index 966c20df..cb645cee 100644 --- a/asyncband/src/lib.rs +++ b/asyncband/src/lib.rs @@ -163,5 +163,8 @@ pub mod waitgroup; #[cfg(feature = "watch")] pub mod watch; -#[cfg(all(test, any(feature = "once-map", feature = "singleflight")))] +#[cfg(all( + test, + any(feature = "once-map", feature = "phaser", feature = "singleflight") +))] mod test_support; diff --git a/asyncband/src/phaser/tests.rs b/asyncband/src/phaser/tests.rs index 3c0186e1..c111762c 100644 --- a/asyncband/src/phaser/tests.rs +++ b/asyncband/src/phaser/tests.rs @@ -25,11 +25,8 @@ 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; +use crate::test_support::poll_once; struct CountWake(AtomicUsize); @@ -107,8 +104,8 @@ fn cancelled_arrive_and_wait_retry_waits_for_original_phase_after_advance() { let mut second = phaser.register(); { - let mut cancelled = spawn(first.arrive_and_wait()); - assert_pending!(cancelled.poll()); + let mut cancelled = Box::pin(first.arrive_and_wait()); + assert!(poll_once(cancelled.as_mut()).is_pending()); } assert_eq!(phaser.arrived_parties(), 1); @@ -117,8 +114,8 @@ fn cancelled_arrive_and_wait_retry_waits_for_original_phase_after_advance() { 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); + let mut retry = Box::pin(first.arrive_and_wait()); + assert_eq!(poll_once(retry.as_mut()), Poll::Ready(phase1)); assert_eq!(phaser.arrived_parties(), 0); } @@ -129,16 +126,16 @@ fn cancelled_arrive_and_wait_retry_before_advance_does_not_arrive_twice() { let mut second = phaser.register(); { - let mut cancelled = spawn(first.arrive_and_wait()); - assert_pending!(cancelled.poll()); + let mut cancelled = Box::pin(first.arrive_and_wait()); + assert!(poll_once(cancelled.as_mut()).is_pending()); } - let mut retry = spawn(first.arrive_and_wait()); - assert_pending!(retry.poll()); + let mut retry = Box::pin(first.arrive_and_wait()); + assert!(poll_once(retry.as_mut()).is_pending()); assert_eq!(phaser.arrived_parties(), 1); second.arrive(); - assert_ready!(retry.poll()); + assert!(poll_once(retry.as_mut()).is_ready()); } #[test] @@ -367,8 +364,8 @@ fn panicking_waker_does_not_lose_an_arrive_and_wait_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); + let mut retry = Box::pin(second.arrive_and_wait()); + assert_eq!(poll_once(retry.as_mut()), Poll::Ready(phase1)); assert_eq!(phaser.arrived_parties(), 0); } @@ -379,8 +376,8 @@ fn a_late_waiter_for_a_completed_phase_is_immediately_ready() { let participant = phaser.register(); drop(participant); - let mut wait = spawn(phaser.wait_for_advance(observed)); - assert_eq!(assert_ready!(wait.poll()), phaser.phase()); + let mut wait = Box::pin(phaser.wait_for_advance(observed)); + assert_eq!(poll_once(wait.as_mut()), Poll::Ready(phaser.phase())); } #[test] From 382052e556820fba19fa2ee5d70ac7b873fcd89d Mon Sep 17 00:00:00 2001 From: tison Date: Fri, 11 Sep 2026 10:35:36 +0800 Subject: [PATCH 07/12] feat(phaser): redesign participant lifecycle and add usage examples --- CHANGELOG.md | 2 +- asyncband/src/internal/wakerset.rs | 22 + asyncband/src/phaser/mod.rs | 707 +++++++++++-------------- asyncband/src/phaser/tests.rs | 358 ++++++++++--- examples/Cargo.toml | 13 + examples/src/phaser_completion.rs | 167 ++++++ examples/src/phaser_groups.rs | 188 +++++++ examples/src/phaser_rounds.rs | 137 +++++ tests-integration/tests/phaser_test.rs | 80 ++- tests-integration/tests/traits_test.rs | 6 +- 10 files changed, 1184 insertions(+), 496 deletions(-) create mode 100644 examples/src/phaser_completion.rs create mode 100644 examples/src/phaser_groups.rs create mode 100644 examples/src/phaser_rounds.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index a05546f2..84570c7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ All notable changes to this project will be documented in this file. ### New features -* Add an opt-in runtime-agnostic `Phaser` with dynamic RAII participants, reusable phases, and cancellation-resilient arrival semantics. +* Add an opt-in runtime-agnostic `Phaser` with shared observer handles, dynamic RAII participants, `u64` phase numbers, split arrival/wait with cancellation-resilient retries, and explicit closure that releases unfinished waits with `Closed`. * Add bounded MPSC `reserve` and `try_reserve` methods returning a `Permit`, allowing callers to wait for capacity before constructing a message; pending sends and reservations receive capacity in wait-queue order, and unused permits release capacity without claiming message order. ### Bug fixes diff --git a/asyncband/src/internal/wakerset.rs b/asyncband/src/internal/wakerset.rs index c49d49fd..7c9393bb 100644 --- a/asyncband/src/internal/wakerset.rs +++ b/asyncband/src/internal/wakerset.rs @@ -102,6 +102,28 @@ impl WakerSet { None } + /// Registers or replaces a waker cloned before taking the owner's state lock. + /// + /// Returns the previous waker so its destructor can run after releasing that lock. + #[inline] + #[must_use = "drop the returned waker after releasing the state lock"] + pub fn register_owned( + &mut self, + token: &mut Option, + waker: Waker, + ) -> Option { + if let Some(token) = token { + let current = self + .wakers + .get_mut(token.0) + .expect("waker token must refer to an occupied slot"); + return Some(mem::replace(current, waker)); + } + + *token = Some(WakerToken(self.wakers.insert(waker))); + None + } + /// Removes the waker identified by `token`. /// /// The owner must clear stale tokens without calling this method after detaching the set. The diff --git a/asyncband/src/phaser/mod.rs b/asyncband/src/phaser/mod.rs index 9b49e910..f7ac3efd 100644 --- a/asyncband/src/phaser/mod.rs +++ b/asyncband/src/phaser/mod.rs @@ -15,123 +15,103 @@ // specific language governing permissions and limitations // under the License. -//! A reusable phase barrier with a dynamic participant set. +//! Coordinate repeated rounds of work with a dynamic participant set. //! -//! A phaser coordinates repeated rounds of work with dynamically registered parties. +//! A [`Phaser`] is a shared coordination handle. Cloning it creates another observer, without +//! registering a participant. Each [`PhaserParticipant`] owns one arrival obligation per phase. +//! Register participants before starting their tasks, or keep a coordinator participant registered +//! while setting up a group so that the first workers cannot finish the phase prematurely. //! -//! Each [`PhaserParticipant`] represents one registered party. +//! # Arriving and waiting //! -//! A phase advances after every party registered for that phase has arrived or deregistered. +//! [`PhaserParticipant::wait`] arrives and waits for the other participants. To overlap independent +//! work with that wait, call [`arrive`](PhaserParticipant::arrive) first. The subsequent `wait` +//! observes that arrival's phase even if it has already completed. Explicitly arriving again +//! replaces the pending observation with the current phase; repeated arrivals within one phase +//! do not count twice. //! -//! 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. -//! -//! # Synchronization -//! -//! A participant's arrival establishes a happens-before relationship between operations performed -//! before the arrival and operations performed after a wait observes completion of that phase. -//! -//! This guarantee applies to both [`Phaser::wait_for_advance`] and -//! [`PhaserParticipant::arrive_and_wait`], including waits that find the phase already completed. -//! -//! Arriving and deregistering, including when a participant is dropped, provides the same guarantee -//! for operations preceding that arrival. -//! -//! # 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. +//! ``` +//! use asyncband::phaser::Phaser; //! -//! A later registration joins the current dormant phase. +//! # #[tokio::main(flavor = "current_thread")] +//! # async fn main() -> Result<(), asyncband::phaser::Closed> { +//! let phaser = Phaser::new(); +//! let mut participants = phaser.register_many(2)?; +//! let mut worker = participants.pop().unwrap(); +//! let mut coordinator = participants.pop().unwrap(); //! -//! # Phase identity +//! let task = tokio::spawn(async move { +//! for _ in 0..3 { +//! // Finish this round's work before arriving. +//! let completed = worker.arrive()?; +//! // Independent work can run here without delaying the other participants. +//! assert_ne!(worker.wait().await?, completed); +//! } +//! Ok::<_, asyncband::phaser::Closed>(()) +//! }); //! -//! Phases advance using wrapping arithmetic. +//! for _ in 0..3 { +//! coordinator.wait().await?; +//! } +//! task.await.unwrap()?; +//! # Ok(()) +//! # } +//! ``` //! -//! [`Phase`] supports equality but no ordering or arithmetic contract across wraparound. +//! # Membership and cancellation //! -//! Waiters compare phase identity instead of inferring transitions from party counts. +//! Registration joins the phase current at the registration's synchronization point. In +//! particular, [`register_many`](Phaser::register_many) registers its entire batch in one phase. +//! Dropping or [`deregistering`](PhaserParticipant::deregister) a participant removes its future +//! obligations and discharges any outstanding arrival in the current phase. An empty phaser is +//! dormant and can be reused; it does not advance repeatedly or close automatically. //! -//! # Examples +//! A participant's `wait` records arrival on its first poll. Dropping an unpolled future has no +//! effect. Cancelling a polled wait preserves its arrival and pending phase: retrying `wait` on +//! that participant observes the same phase instead of arriving in a later one. Dropping the +//! participant itself withdraws it from the group. Withdrawal does not certify successful work; +//! applications that require all workers to succeed should close the phaser on failure. //! -//! ``` -//! use std::sync::Arc; +//! [`Phaser::wait_for_advance`] is an independent, cancel-safe observation. It never registers a +//! participant or records an arrival. Observers may miss intermediate phases; this is not an +//! event stream with one notification per phase. //! -//! use asyncband::phaser::Phaser; +//! # Closure and synchronization //! -//! let phaser = Arc::new(Phaser::new()); -//! let initial = phaser.phase(); -//! let mut first = phaser.register(); -//! let second = phaser.register(); +//! [`Phaser::close`] permanently freezes the current phase, rejects registration and arrival, +//! and releases waits for the unfinished phase with [`Closed`]. A previously completed phase +//! remains successful even if the phaser closes before its waiter is polled again. //! -//! assert_eq!(first.arrive(), initial); -//! assert_eq!(second.arrive_and_deregister(), initial); -//! assert_ne!(phaser.phase(), initial); -//! ``` +//! Work performed before an arrival or deregistration happens before work performed after a +//! successful wait for that phase's completion. No such all-participants-completed guarantee is +//! provided by a wait that returns `Closed`. //! -//! Participants can coordinate several rounds from separate tasks, using either combined or -//! separate arrival and waiting: +//! Phase numbers start at zero and wrap from `u64::MAX` to zero. Pass a value previously obtained +//! from the same phaser to `wait_for_advance`; it tests for a different phase, not a target number +//! or numeric threshold. An observation must not be retained across a full counter cycle. //! -//! ``` -//! use std::sync::Arc; +//! # Java Phaser use cases //! -//! use asyncband::phaser::Phaser; +//! The following examples are in the repository's `examples` package. Run one with +//! `cargo run -p examples --example `. //! -//! # #[tokio::main(flavor = "current_thread")] -//! # async fn main() { -//! let phaser = Arc::new(Phaser::new()); -//! let mut first = phaser.register(); -//! let mut second = phaser.register(); +//! | Use case | Rust expression | Runnable example | +//! |----------|-----------------|------------------| +//! | Dynamic registration, repeated rounds, and a one-shot start gate | Shared handles, `register_many`, participant `wait`, and `deregister` | `phaser_rounds` | +//! | Split arrival/wait, progress observation, numeric targets, and cancellation retry | `arrive`, participant `wait`, and `wait_for_advance` | `phaser_rounds` | +//! | `onAdvance` aggregation, asynchronous finalization, and stopping at convergence | A coordinator and separate ready/resume phasers | `phaser_completion` | +//! | Aborting the group after a worker fails | `close`, including an application-owned abort guard | `phaser_completion` | +//! | Grouped fan-in before global release | Local ready/resume phasers and one root participant per group | `phaser_groups` | //! -//! let task = tokio::spawn(async move { -//! for _ in 0..3 { -//! // Finish this round's work before recording arrival. -//! let observed = first.arrive(); -//! // Independent work can run here before waiting for the other party. -//! first.phaser().wait_for_advance(observed).await; -//! } -//! }); +//! These compositions do not supply Java's native parent/child phasers, automatic parent +//! registration, a globally shared phase counter across nodes, or an in-primitive `onAdvance` +//! hook. The grouped example has an explicit coordinator task per group; local counters are not +//! global phase numbers. Membership changes in the ready/resume protocols must be applied at +//! coordinated round boundaries. A slow observer cannot run an exactly-once completion hook. +//! Timeouts and task scheduling remain with the caller's runtime. //! -//! for _ in 0..3 { -//! second.arrive_and_wait().await; -//! } -//! task.await.unwrap(); -//! # } -//! ``` +//! For the Java contracts being mapped, see the +//! [Java Phaser documentation](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/Phaser.html). use std::fmt; use std::future::Future; @@ -149,51 +129,65 @@ use crate::internal::wakerset::WakerToken; #[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 - } +/// The phaser was closed before this operation could complete. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Closed; - const fn next(self) -> Self { - Self(self.0.wrapping_add(1)) +impl fmt::Display for Closed { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("phaser is closed") } } -/// A reusable phase barrier with a dynamic participant set. -/// -/// Store a phaser in an [`Arc`] before registering participants. +impl std::error::Error for Closed {} + +/// A shared handle to a reusable phase barrier with dynamic participants. /// -/// Each participant owns an `Arc` clone and can move into an independently spawned task. -#[derive(Debug)] +/// Cloning this handle does not register a participant. Use [`register`](Self::register) to +/// create a participant that can be moved into an independently spawned task. +#[derive(Clone)] pub struct Phaser { - state: Mutex, + state: Arc>, } -struct PhaserState { - phase: Phase, +struct State { + phase: u64, + closed: bool, registered: u32, unarrived: u32, waiters: WakerSet, } -impl fmt::Debug for PhaserState { +impl State { + fn advance_if_ready(&mut self) -> Option + 'static> { + if self.closed || self.unarrived != 0 { + return None; + } + self.phase = self.phase.wrapping_add(1); + self.unarrived = self.registered; + Some(self.waiters.drain()) + } + + fn completion(&self, observed: u64) -> Poll> { + // Completion wins over a later close; the unfinished phase itself never advances on close. + if self.phase != observed { + Poll::Ready(Ok(self.phase)) + } else if self.closed { + Poll::Ready(Err(Closed)) + } else { + Poll::Pending + } + } +} + +impl fmt::Debug for Phaser { 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) + let state = self.state.lock(); + f.debug_struct("Phaser") + .field("phase", &state.phase) + .field("closed", &state.closed) + .field("registered", &state.registered) + .field("unarrived", &state.unarrived) .finish_non_exhaustive() } } @@ -205,130 +199,104 @@ impl Default for Phaser { } 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 { + /// Creates an open, dormant phaser at phase zero with no registered participants. + pub fn new() -> Self { Self { - state: Mutex::new(PhaserState { - phase: Phase(0), + state: Arc::new(Mutex::new(State { + phase: 0, + closed: false, registered: 0, unarrived: 0, waiters: WakerSet::new(), - }), + })), } } - /// Returns the current phase identity. - pub fn phase(&self) -> Phase { + /// Returns the current phase number, which remains fixed after closure. + pub fn phase(&self) -> u64 { self.state.lock().phase } - /// Returns the number of currently registered parties. + /// Returns whether this phaser has been permanently closed. + pub fn is_closed(&self) -> bool { + self.state.lock().closed + } + + /// Closes this phaser and wakes all pending observers without completing the current phase. + /// + /// Closure is idempotent and affects every handle and participant. Existing participants may + /// still deregister or be dropped; their removal no longer advances the phase. + /// + /// # Panics + /// + /// If a waker panics, closure remains committed and notification is attempted for the other + /// waiters before the panic resumes. + pub fn close(&self) { + let wakers = { + let mut state = self.state.lock(); + if state.closed { + return; + } + state.closed = true; + state.waiters.take_all() + }; + wake_all(wakers); + } + + /// Returns an instantaneous count of registered participants, including those already arrived. /// - /// This is an instantaneous observation and may change immediately after the method returns. + /// Counts can change between separate queries. This is not a synchronization operation. 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. + /// Returns an instantaneous count of participants that have arrived in the current phase. 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. + /// Returns an instantaneous count of outstanding arrivals in the current phase. 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. + /// Registers one participant in the current phase, or returns [`Closed`]. /// - /// Registration is linearized with phase advancement. - /// - /// A concurrent advancement places the participant in either adjacent phase, never both. + /// 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. /// /// # 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, - } + /// Panics if the registered count would exceed `u32::MAX`. + pub fn register(&self) -> Result { + let phaser = self.clone(); + self.register_inner(1)?; + Ok(PhaserParticipant::new(phaser)) } - /// 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. + /// Registers an entire batch in one phase, or returns [`Closed`] without registering anyone. /// - /// Storage for all handles is reserved before registration is committed. + /// On an open phaser, a zero-sized batch does nothing. The batch is reserved before any + /// participant count is changed. Every returned handle must be used or dropped. /// /// # Panics /// - /// Panics if either count cannot be represented by its public integer type. - pub fn register_many(self: &Arc, parties: u32) -> Vec { + /// Panics if the batch cannot fit in a vector or the registered count would exceed `u32::MAX`. + pub fn register_many(&self, parties: u32) -> Result, Closed> { 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 + self.register_inner(parties)?; + participants.extend((0..parties).map(|_| PhaserParticipant::new(self.clone()))); + Ok(participants) } - fn register_inner(&self, parties: u32) -> Phase { + fn register_inner(&self, parties: u32) -> Result<(), Closed> { let mut state = self.state.lock(); + if state.closed { + return Err(Closed); + } let registered = state .registered .checked_add(parties) @@ -339,237 +307,190 @@ impl Phaser { .expect("Phaser unarrived-party count overflow"); state.registered = registered; state.unarrived = unarrived; - state.phase + Ok(()) } - 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 mut state = self.state.lock(); - if state.phase != observed { - let phase = state.phase; - *token = None; - return Poll::Ready(phase); - } - - let _retired_waker = state.waiters.register(token, cx.waker()); - drop(state); - Poll::Pending - } - - fn unregister_waker(&self, token: &mut Option, observed: Phase) { - if token.is_none() { - return; - } - - let mut state = self.state.lock(); - if state.phase != observed { - *token = None; - return; + /// Waits until the current phase differs from a phase previously observed on this phaser. + /// + /// Returns the current phase, possibly skipping intermediate phases. This does not wait for + /// a future target number: a number different from the current phase returns immediately. + /// It neither registers a participant nor records an arrival. + /// + /// Returns [`Closed`] if the observed phase is still current when the phaser closes. A phase + /// completed before closure remains successful. Phase numbers wrap; do not retain an + /// observation across a full `u64` cycle or use a number obtained from another phaser. + /// + /// # Cancel safety + /// + /// Cancelling only unregisters this wait's waker. The same observation can be retried. + pub async fn wait_for_advance(&self, observed: u64) -> Result { + PhaserWait { + phaser: self, + observed, + token: None, } - - let _removed_waker = state.waiters.unregister(token); - drop(state); + .await } } -/// 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. +/// One participant's arrival obligation in every phase until it deregisters or is dropped. /// -/// Dropping a registered participant is equivalent to arriving and deregistering. -#[must_use = "dropping a participant arrives and deregisters it from the phaser"] +/// This handle is not cloneable. Dropping it withdraws the participant, including any outstanding +/// current arrival. It does not report successful work or close the other participants. +#[must_use = "dropping a participant withdraws it from the phaser"] #[derive(Debug)] pub struct PhaserParticipant { - phaser: Arc, - phase: Phase, - arrived: bool, + phaser: Phaser, + arrived: Option, + pending: Option, registered: bool, - pending_wait: Option, } impl PhaserParticipant { - /// Returns the phaser this participant is registered with. - /// - /// This borrows the existing phaser without cloning its [`Arc`] or registering another party. - /// - /// Use this to call [`Phaser::wait_for_advance`] after a separate [`Self::arrive`] operation. + fn new(phaser: Phaser) -> Self { + Self { + phaser, + arrived: None, + pending: None, + registered: true, + } + } + + /// Returns the shared coordination handle without registering another participant. pub fn phaser(&self) -> &Phaser { &self.phaser } - /// Arrives in the current phase without waiting for it to advance. - /// - /// Repeated calls in one phase return its identity without changing counts again. + /// Records this participant's arrival and remembers that phase for [`wait`](Self::wait). /// - /// Calling this after a cancelled `arrive_and_wait` abandons that pending wait. + /// Returns the phase in which arrival was recorded, or [`Closed`] without recording one. + /// 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. /// - /// 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) + /// Arrival and the pending observation remain committed if notifying a waker panics. + pub fn arrive(&mut self) -> Result { + let (phase, wakers) = { + let mut state = self.phaser.state.lock(); + if state.closed { + return Err(Closed); + } + let phase = state.phase; + if self.arrived != Some(phase) { + state.unarrived -= 1; + self.arrived = Some(phase); + } + self.pending = Some(phase); + (phase, state.advance_if_ready()) + }; + wake_all(wakers.into_iter().flatten()); + Ok(phase) } - /// Arrives in the current phase and waits for that phase to advance. - /// - /// The returned value is the new current phase. - /// - /// # Cancellation + /// Arrives if necessary and waits for this participant's pending phase to complete. /// - /// Arrival is committed when the returned future is first polled. + /// After an explicit [`arrive`](Self::arrive), this observes that arrival even if the phase + /// 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. /// - /// Constructing and dropping an unpolled future has no effect. + /// # Cancel safety /// - /// 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 { + /// 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 + /// counting another arrival. An explicit new `arrive` replaces that pending observation. + pub async fn wait(&mut self) -> Result { + let observed = match self.pending { 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 - } + None => self.arrive()?, }; - - let next = self.phaser.wait_for_advance(observed).await; - self.pending_wait = None; - self.phase = next; - self.arrived = false; - next + let next = self.phaser.wait_for_advance(observed).await?; + self.pending = None; + Ok(next) } - /// Arrives in the current phase and deregisters from later phases. + /// Withdraws this participant and returns the phase from which it withdrew, or [`Closed`]. /// - /// 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) + /// Any outstanding arrival is discharged without counting an already-arrived participant + /// twice. This always removes the registration, including after closure. The pending + /// observation is abandoned. Dropping a participant has the same membership effect. + pub fn deregister(mut self) -> Result { + self.deregister_inner() + } + + fn deregister_inner(&mut self) -> Result { + let (result, wakers) = { + let mut state = self.phaser.state.lock(); + self.registered = false; + state.registered -= 1; + if self.arrived != Some(state.phase) { + state.unarrived -= 1; + } + let result = if state.closed { + Err(Closed) + } else { + Ok(state.phase) + }; + (result, state.advance_if_ready()) + }; + wake_all(wakers.into_iter().flatten()); + result } } 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); + let _ = self.deregister_inner(); } } } -#[must_use = "futures do nothing unless you `.await` or poll them"] +#[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() - } + observed: u64, + token: Option, } impl Future for PhaserWait<'_> { - type Output = Phase; + type Output = Result; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let Self { - token, - observed, - phaser, - } = self.get_mut(); - phaser.poll_wait(token, *observed, cx) + let this = self.get_mut(); + { + let state = this.phaser.state.lock(); + if let ready @ Poll::Ready(_) = state.completion(this.observed) { + this.token = None; + return ready; + } + } + + // Waker cloning may reenter or close this phaser. Recheck completion before registering. + let waker = cx.waker().clone(); + let mut state = this.phaser.state.lock(); + if let ready @ Poll::Ready(_) = state.completion(this.observed) { + this.token = None; + drop(state); + drop(waker); + return ready; + } + let retired = state.waiters.register_owned(&mut this.token, waker); + drop(state); + drop(retired); + Poll::Pending } } impl Drop for PhaserWait<'_> { fn drop(&mut self) { - self.phaser.unregister_waker(&mut self.token, self.observed); + if self.token.is_none() { + return; + } + let mut state = self.phaser.state.lock(); + if state.completion(self.observed).is_ready() { + return; + } + let retired = state.waiters.unregister(&mut self.token); + drop(state); + drop(retired); } } diff --git a/asyncband/src/phaser/tests.rs b/asyncband/src/phaser/tests.rs index c111762c..d1a27bac 100644 --- a/asyncband/src/phaser/tests.rs +++ b/asyncband/src/phaser/tests.rs @@ -25,6 +25,7 @@ use std::task::Poll; use std::task::Wake; use std::task::Waker; +use super::Closed; use super::Phaser; use crate::test_support::poll_once; @@ -46,8 +47,8 @@ impl Wake for PanicWake { #[test] fn register_many_joins_one_observed_phase() { - let phaser = Arc::new(Phaser::new()); - let participants = phaser.register_many(3); + let phaser = Phaser::new(); + let participants = phaser.register_many(3).unwrap(); assert_eq!(participants.len(), 3); assert_eq!(phaser.registered_parties(), 3); @@ -56,10 +57,10 @@ fn register_many_joins_one_observed_phase() { #[test] fn registering_zero_parties_is_a_noop() { - let phaser = Arc::new(Phaser::new()); + let phaser = Phaser::new(); let phase = phaser.phase(); - assert!(phaser.register_many(0).is_empty()); + assert!(phaser.register_many(0).unwrap().is_empty()); assert_eq!(phaser.phase(), phase); assert_eq!(phaser.registered_parties(), 0); assert_eq!(phaser.unarrived_parties(), 0); @@ -67,29 +68,29 @@ fn registering_zero_parties_is_a_noop() { #[test] fn participants_advance_across_repeated_phases() { - let phaser = Arc::new(Phaser::new()); + let phaser = Phaser::new(); let phase0 = phaser.phase(); - let mut first = phaser.register(); - let mut second = phaser.register(); + let mut first = phaser.register().unwrap(); + let mut second = phaser.register().unwrap(); - assert_eq!(first.arrive(), phase0); + assert_eq!(first.arrive().unwrap(), phase0); assert_eq!(phaser.arrived_parties(), 1); - assert_eq!(second.arrive(), phase0); + assert_eq!(second.arrive().unwrap(), 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_eq!(first.arrive().unwrap(), phase1); + assert_eq!(second.arrive().unwrap(), 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(); +fn unpolled_wait_future_does_not_arrive() { + let phaser = Phaser::new(); + let mut participant = phaser.register().unwrap(); - let wait = participant.arrive_and_wait(); + let wait = participant.wait(); assert_eq!(phaser.arrived_parties(), 0); drop(wait); @@ -97,52 +98,52 @@ fn unpolled_arrive_and_wait_future_does_not_arrive() { } #[test] -fn cancelled_arrive_and_wait_retry_waits_for_original_phase_after_advance() { - let phaser = Arc::new(Phaser::new()); +fn cancelled_wait_retry_waits_for_original_phase_after_advance() { + let phaser = Phaser::new(); let phase0 = phaser.phase(); - let mut first = phaser.register(); - let mut second = phaser.register(); + let mut first = phaser.register().unwrap(); + let mut second = phaser.register().unwrap(); { - let mut cancelled = Box::pin(first.arrive_and_wait()); + let mut cancelled = Box::pin(first.wait()); assert!(poll_once(cancelled.as_mut()).is_pending()); } assert_eq!(phaser.arrived_parties(), 1); - assert_eq!(second.arrive(), phase0); + assert_eq!(second.arrive().unwrap(), phase0); let phase1 = phaser.phase(); assert_ne!(phase1, phase0); assert_eq!(phaser.arrived_parties(), 0); - let mut retry = Box::pin(first.arrive_and_wait()); - assert_eq!(poll_once(retry.as_mut()), Poll::Ready(phase1)); + let mut retry = Box::pin(first.wait()); + assert_eq!(poll_once(retry.as_mut()), Poll::Ready(Ok(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(); +fn cancelled_wait_retry_before_advance_does_not_arrive_twice() { + let phaser = Phaser::new(); + let mut first = phaser.register().unwrap(); + let mut second = phaser.register().unwrap(); { - let mut cancelled = Box::pin(first.arrive_and_wait()); + let mut cancelled = Box::pin(first.wait()); assert!(poll_once(cancelled.as_mut()).is_pending()); } - let mut retry = Box::pin(first.arrive_and_wait()); + let mut retry = Box::pin(first.wait()); assert!(poll_once(retry.as_mut()).is_pending()); assert_eq!(phaser.arrived_parties(), 1); - second.arrive(); + second.arrive().unwrap(); assert!(poll_once(retry.as_mut()).is_ready()); } #[test] fn dropping_last_participant_advances_once_and_dormant_phaser_can_be_reused() { - let phaser = Arc::new(Phaser::new()); + let phaser = Phaser::new(); let phase0 = phaser.phase(); - let participant = phaser.register(); + let participant = phaser.register().unwrap(); drop(participant); let phase1 = phaser.phase(); @@ -150,98 +151,98 @@ fn dropping_last_participant_advances_once_and_dormant_phaser_can_be_reused() { assert_eq!(phaser.registered_parties(), 0); assert_eq!(phaser.arrived_parties(), 0); - let mut participant = phaser.register(); - assert_eq!(participant.arrive(), phase1); + let mut participant = phaser.register().unwrap(); + assert_eq!(participant.arrive().unwrap(), 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 phaser = Phaser::new(); let phase0 = phaser.phase(); - let mut first = phaser.register(); - let mut second = phaser.register(); + let mut first = phaser.register().unwrap(); + let mut second = phaser.register().unwrap(); - first.arrive(); + first.arrive().unwrap(); drop(first); assert_eq!(phaser.phase(), phase0); assert_eq!(phaser.registered_parties(), 1); assert_eq!(phaser.unarrived_parties(), 1); - second.arrive(); + second.arrive().unwrap(); assert_ne!(phaser.phase(), phase0); } #[test] fn registration_before_last_arrival_joins_and_delays_current_phase() { - let phaser = Arc::new(Phaser::new()); + let phaser = Phaser::new(); let phase = phaser.phase(); - let mut first = phaser.register(); - let mut second = phaser.register(); + let mut first = phaser.register().unwrap(); + let mut second = phaser.register().unwrap(); - first.arrive(); - let mut joining = phaser.register(); - second.arrive(); + first.arrive().unwrap(); + let mut joining = phaser.register().unwrap(); + second.arrive().unwrap(); assert_eq!(phaser.phase(), phase); assert_eq!(phaser.unarrived_parties(), 1); - joining.arrive(); + joining.arrive().unwrap(); assert_ne!(phaser.phase(), phase); } #[test] fn registration_after_last_arrival_joins_the_advanced_phase() { - let phaser = Arc::new(Phaser::new()); + let phaser = Phaser::new(); let phase0 = phaser.phase(); - let mut first = phaser.register(); + let mut first = phaser.register().unwrap(); - first.arrive(); + first.arrive().unwrap(); let phase1 = phaser.phase(); assert_ne!(phase1, phase0); - let mut joining = phaser.register(); + let mut joining = phaser.register().unwrap(); assert_eq!(phaser.registered_parties(), 2); assert_eq!(phaser.unarrived_parties(), 2); - assert_eq!(joining.arrive(), phase1); + assert_eq!(joining.arrive().unwrap(), phase1); assert_eq!(phaser.phase(), phase1); } #[test] fn registration_before_last_participant_drop_joins_the_current_phase() { - let phaser = Arc::new(Phaser::new()); + let phaser = Phaser::new(); let phase0 = phaser.phase(); - let participant = phaser.register(); - let joining = phaser.register(); + let participant = phaser.register().unwrap(); + let joining = phaser.register().unwrap(); 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_eq!(joining.deregister().unwrap(), phase0); assert_ne!(phaser.phase(), phase0); } #[test] fn registration_after_last_participant_drop_joins_the_advanced_phase() { - let phaser = Arc::new(Phaser::new()); + let phaser = Phaser::new(); let phase0 = phaser.phase(); - let participant = phaser.register(); + let participant = phaser.register().unwrap(); drop(participant); let phase1 = phaser.phase(); - let joining = phaser.register(); + let joining = phaser.register().unwrap(); assert_ne!(phase1, phase0); assert_eq!(phaser.registered_parties(), 1); assert_eq!(phaser.unarrived_parties(), 1); - assert_eq!(joining.arrive_and_deregister(), phase1); + assert_eq!(joining.deregister().unwrap(), 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 phaser = Phaser::new(); let phase = phaser.phase(); let counter = Arc::new(CountWake(AtomicUsize::new(0))); let waker = Waker::from(Arc::clone(&counter)); @@ -254,16 +255,16 @@ fn wait_for_advance_is_a_cancel_safe_non_participant_observer() { } assert_eq!(phaser.registered_parties(), 0); - let participant = phaser.register(); + let participant = phaser.register().unwrap(); 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 phaser = Phaser::new(); let observed = phaser.phase(); - let participant = phaser.register(); + let participant = phaser.register().unwrap(); 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)); @@ -297,9 +298,9 @@ fn advancing_a_phase_wakes_every_registered_waiter_once() { #[test] fn cancelling_a_woken_waiter_does_not_unregister_a_next_phase_waiter() { - let phaser = Arc::new(Phaser::new()); + let phaser = Phaser::new(); let phase0 = phaser.phase(); - let participant = phaser.register(); + let participant = phaser.register().unwrap(); 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); @@ -314,7 +315,7 @@ fn cancelling_a_woken_waiter_does_not_unregister_a_next_phase_waiter() { assert_ne!(phase1, phase0); assert_eq!(stale_counter.0.load(Ordering::Relaxed), 1); - let participant = phaser.register(); + let participant = phaser.register().unwrap(); 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); @@ -334,11 +335,11 @@ fn cancelling_a_woken_waiter_does_not_unregister_a_next_phase_waiter() { } #[test] -fn panicking_waker_does_not_lose_an_arrive_and_wait_phase() { - let phaser = Arc::new(Phaser::new()); +fn panicking_waker_does_not_lose_a_pending_phase() { + let phaser = Phaser::new(); let phase0 = phaser.phase(); - let mut first = phaser.register(); - let mut second = phaser.register(); + let mut first = phaser.register().unwrap(); + let mut second = phaser.register().unwrap(); 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)); @@ -347,11 +348,11 @@ fn panicking_waker_does_not_lose_an_arrive_and_wait_phase() { Future::poll(observer.as_mut(), &mut panic_context), Poll::Pending ); - assert_eq!(first.arrive(), phase0); + assert_eq!(first.arrive().unwrap(), 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 mut wait = Box::pin(second.wait()); let result = panic::catch_unwind(panic::AssertUnwindSafe(|| { Future::poll(wait.as_mut(), &mut polling_context) })); @@ -364,44 +365,233 @@ fn panicking_waker_does_not_lose_an_arrive_and_wait_phase() { assert_ne!(phase1, phase0); assert_eq!(phaser.arrived_parties(), 0); - let mut retry = Box::pin(second.arrive_and_wait()); - assert_eq!(poll_once(retry.as_mut()), Poll::Ready(phase1)); + let mut retry = Box::pin(second.wait()); + assert_eq!(poll_once(retry.as_mut()), Poll::Ready(Ok(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 phaser = Phaser::new(); let observed = phaser.phase(); - let participant = phaser.register(); + let participant = phaser.register().unwrap(); drop(participant); let mut wait = Box::pin(phaser.wait_for_advance(observed)); - assert_eq!(poll_once(wait.as_mut()), Poll::Ready(phaser.phase())); + assert_eq!(poll_once(wait.as_mut()), Poll::Ready(Ok(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 phaser = Phaser::new(); + phaser.state.lock().phase = u64::MAX; let observed = phaser.phase(); - let mut participant = phaser.register(); + let mut participant = phaser.register().unwrap(); - assert_eq!(participant.arrive(), observed); - assert_eq!(phaser.phase().get(), 0); + assert_eq!(participant.arrive().unwrap(), observed); + assert_eq!(phaser.phase(), 0); assert_ne!(phaser.phase(), observed); } #[test] fn registration_overflow_panics_without_partially_updating_state() { - let phaser = Arc::new(Phaser::new()); + let phaser = 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!(panic::catch_unwind(|| phaser.register().unwrap()).is_err()); assert_eq!(phaser.registered_parties(), u32::MAX); assert_eq!(phaser.unarrived_parties(), u32::MAX); } + +#[test] +fn explicit_arrival_and_wait_observe_the_same_completed_phase() { + let phaser = Phaser::new(); + let mut first = phaser.register().unwrap(); + let mut second = phaser.register().unwrap(); + let observed = first.arrive().unwrap(); + second.arrive().unwrap(); + let next = phaser.phase(); + + assert_ne!(observed, next); + assert_eq!( + poll_once(Box::pin(first.wait()).as_mut()), + Poll::Ready(Ok(next)) + ); + assert_eq!( + poll_once(Box::pin(second.wait()).as_mut()), + Poll::Ready(Ok(next)) + ); + assert_eq!(phaser.arrived_parties(), 0); + + let mut wait = Box::pin(first.wait()); + assert!(poll_once(wait.as_mut()).is_pending()); + second.arrive().unwrap(); + assert_eq!(poll_once(wait.as_mut()), Poll::Ready(Ok(phaser.phase()))); +} + +#[test] +fn explicit_arrival_replaces_a_cancelled_pending_observation() { + let phaser = Phaser::new(); + let mut first = phaser.register().unwrap(); + let mut second = phaser.register().unwrap(); + assert!(poll_once(Box::pin(first.wait()).as_mut()).is_pending()); + second.arrive().unwrap(); + let next = first.arrive().unwrap(); + assert_eq!(next, phaser.phase()); + + let mut wait = Box::pin(first.wait()); + assert!(poll_once(wait.as_mut()).is_pending()); + second.arrive().unwrap(); + assert_eq!(poll_once(wait.as_mut()), Poll::Ready(Ok(phaser.phase()))); +} + +#[test] +fn cloned_handles_observe_without_registering_and_participants_own_the_state() { + let phaser = Phaser::new(); + let observer = phaser.clone(); + let mut participant = phaser.register().unwrap(); + drop(phaser); + assert_eq!(observer.registered_parties(), 1); + let observed = observer.phase(); + participant.arrive().unwrap(); + assert_eq!( + poll_once(Box::pin(observer.wait_for_advance(observed)).as_mut()), + Poll::Ready(Ok(observer.phase())) + ); +} + +#[test] +fn closing_wakes_all_waiters_once_and_rejects_new_obligations() { + let phaser = Phaser::new(); + let mut first = phaser.register().unwrap(); + let second = phaser.register().unwrap(); + let observed = first.arrive().unwrap(); + let counter = Arc::new(CountWake(AtomicUsize::new(0))); + let waker = Waker::from(counter.clone()); + let mut context = Context::from_waker(&waker); + let mut observer = Box::pin(phaser.wait_for_advance(observed)); + let mut wait = Box::pin(first.wait()); + assert!(observer.as_mut().poll(&mut context).is_pending()); + assert!(wait.as_mut().poll(&mut context).is_pending()); + + phaser.clone().close(); + phaser.close(); + assert!(phaser.is_closed()); + assert_eq!(counter.0.load(Ordering::Relaxed), 2); + assert_eq!(poll_once(observer.as_mut()), Poll::Ready(Err(Closed))); + assert_eq!(poll_once(wait.as_mut()), Poll::Ready(Err(Closed))); + drop(wait); + assert_eq!(first.arrive(), Err(Closed)); + assert!(matches!(phaser.register(), Err(Closed))); + assert!(matches!(phaser.register_many(2), Err(Closed))); + assert!(matches!(phaser.register_many(0), Err(Closed))); + assert_eq!(first.deregister(), Err(Closed)); + drop(second); + assert_eq!(phaser.registered_parties(), 0); + assert_eq!(phaser.unarrived_parties(), 0); + assert_eq!(phaser.phase(), observed); +} + +#[test] +fn completed_arrival_remains_successful_after_close_but_cannot_start_another_round() { + let phaser = Phaser::new(); + let mut first = phaser.register().unwrap(); + let mut second = phaser.register().unwrap(); + let observed = first.arrive().unwrap(); + let mut observer = Box::pin(phaser.wait_for_advance(observed)); + assert!(poll_once(observer.as_mut()).is_pending()); + second.arrive().unwrap(); + let completed = phaser.phase(); + phaser.close(); + + assert_eq!(poll_once(observer.as_mut()), Poll::Ready(Ok(completed))); + assert_eq!( + poll_once(Box::pin(first.wait()).as_mut()), + Poll::Ready(Ok(completed)) + ); + assert_eq!( + poll_once(Box::pin(first.wait()).as_mut()), + Poll::Ready(Err(Closed)) + ); + drop(first); + drop(second); + assert_eq!(phaser.phase(), completed); +} + +#[test] +fn close_survives_a_panicking_waker_and_notifies_other_waiters() { + let phaser = Phaser::new(); + let observed = phaser.phase(); + let panic_waker = Waker::from(Arc::new(PanicWake)); + let counter = Arc::new(CountWake(AtomicUsize::new(0))); + let count_waker = Waker::from(counter.clone()); + let mut first = Box::pin(phaser.wait_for_advance(observed)); + let mut second = Box::pin(phaser.wait_for_advance(observed)); + assert!( + first + .as_mut() + .poll(&mut Context::from_waker(&panic_waker)) + .is_pending() + ); + assert!( + second + .as_mut() + .poll(&mut Context::from_waker(&count_waker)) + .is_pending() + ); + + assert!(panic::catch_unwind(|| phaser.close()).is_err()); + assert!(phaser.is_closed()); + assert_eq!(counter.0.load(Ordering::Relaxed), 1); + assert_eq!(poll_once(first.as_mut()), Poll::Ready(Err(Closed))); + assert_eq!(poll_once(second.as_mut()), Poll::Ready(Err(Closed))); +} + +#[test] +fn a_late_waiter_observes_completion_across_counter_wraparound() { + let phaser = Phaser::new(); + phaser.state.lock().phase = u64::MAX; + let mut participant = phaser.register().unwrap(); + participant.arrive().unwrap(); + phaser.close(); + assert_eq!( + poll_once(Box::pin(participant.wait()).as_mut()), + Poll::Ready(Ok(0)) + ); +} + +#[test] +fn closing_during_waker_clone_does_not_register_after_close() { + use std::mem::ManuallyDrop; + use std::task::RawWaker; + use std::task::RawWakerVTable; + + unsafe fn clone_waker(data: *const ()) -> RawWaker { + // SAFETY: Each raw waker owns an Arc; ManuallyDrop preserves this one's reference. + let phaser = ManuallyDrop::new(unsafe { Arc::::from_raw(data.cast()) }); + phaser.close(); + RawWaker::new(Arc::into_raw(Arc::clone(&phaser)).cast(), &VTABLE) + } + unsafe fn drop_waker(data: *const ()) { + // SAFETY: Consuming a raw waker releases exactly its one owned Arc reference. + drop(unsafe { Arc::::from_raw(data.cast()) }); + } + unsafe fn wake_by_ref(_: *const ()) {} + static VTABLE: RawWakerVTable = + RawWakerVTable::new(clone_waker, drop_waker, wake_by_ref, drop_waker); + + let phaser = Phaser::new(); + let data = Arc::into_raw(Arc::new(phaser.clone())).cast(); + // SAFETY: The vtable maintains Arc ownership and every callback is thread-safe. + let waker = unsafe { Waker::from_raw(RawWaker::new(data, &VTABLE)) }; + let mut wait = Box::pin(phaser.wait_for_advance(phaser.phase())); + assert_eq!( + wait.as_mut().poll(&mut Context::from_waker(&waker)), + Poll::Ready(Err(Closed)) + ); + assert!(phaser.is_closed()); +} diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 6e079a89..47a82de5 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -30,6 +30,7 @@ asyncband = { workspace = true, features = [ "completion", "lazy-cell", "once-cell", + "phaser", "shutdown", ] } tokio = { workspace = true, features = [ @@ -58,3 +59,15 @@ path = "src/graceful_shutdown.rs" [[example]] name = "shared_completion" path = "src/shared_completion.rs" + +[[example]] +name = "phaser_rounds" +path = "src/phaser_rounds.rs" + +[[example]] +name = "phaser_completion" +path = "src/phaser_completion.rs" + +[[example]] +name = "phaser_groups" +path = "src/phaser_groups.rs" diff --git a/examples/src/phaser_completion.rs b/examples/src/phaser_completion.rs new file mode 100644 index 00000000..9989b484 --- /dev/null +++ b/examples/src/phaser_completion.rs @@ -0,0 +1,167 @@ +// 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. + +//! Finalize each round before releasing workers, and close the group on failure or cancellation. +//! +//! Java mapping: onAdvance aggregation and convergence become an application coordinator between +//! two rendezvous points. The coordinator may await I/O. Merely running code after one wait, even +//! in a barrier leader, would not stop other workers from starting their next round. +//! +//! Membership is fixed within this protocol; changes must update both groups at a common round +//! boundary. Each phaser has its own counter, distinct from the application's iteration number. +//! +//! Run: cargo run -p examples --example phaser_completion + +use std::sync::Arc; +use std::sync::atomic::AtomicU64; +use std::sync::atomic::Ordering; + +use asyncband::phaser::Closed; +use asyncband::phaser::Phaser; +use asyncband::phaser::PhaserParticipant; + +// Own this guard before constructing a task future so cancelling an unpolled task also aborts. +struct CloseOnDrop([Phaser; 2]); + +impl Drop for CloseOnDrop { + fn drop(&mut self) { + for phaser in &self.0 { + phaser.close(); + } + } +} + +struct Member { + // Fields drop in declaration order: close before withdrawing any arrival obligation. + _close: CloseOnDrop, + ready: PhaserParticipant, + resume: PhaserParticipant, +} + +impl Member { + fn register(ready: &Phaser, resume: &Phaser) -> Result { + Ok(Self { + _close: CloseOnDrop([ready.clone(), resume.clone()]), + ready: ready.register()?, + resume: resume.register()?, + }) + } +} + +#[tokio::main(flavor = "current_thread")] +async fn main() -> Result<(), Closed> { + finalize_until_converged().await?; + failure_closes_the_group().await?; + cancelling_an_unpolled_task_closes_the_group().await?; + Ok(()) +} + +async fn compute( + mut member: Member, + id: usize, + values: Arc>, + published: Arc, +) -> Result<(), Closed> { + for round in 1_u64.. { + // The resume rendezvous must publish the previous aggregate before this read. + assert_eq!(published.load(Ordering::Relaxed), (round - 1) * 6); + values[id].store((id as u64 + 1) * round, Ordering::Relaxed); + member.ready.wait().await?; + member.resume.wait().await?; + } + unreachable!() +} + +async fn finalize_until_converged() -> Result<(), Closed> { + let ready = Phaser::new(); + let resume = Phaser::new(); + let mut coordinator = Member::register(&ready, &resume)?; + let values = Arc::new((0..3).map(|_| AtomicU64::new(0)).collect::>()); + let published = Arc::new(AtomicU64::new(0)); + let mut tasks = Vec::new(); + // Register everyone before polling any worker; the coordinator also keeps both phases open. + for id in 0..3 { + tasks.push(tokio::spawn(compute( + Member::register(&ready, &resume)?, + id, + values.clone(), + published.clone(), + ))); + } + + loop { + coordinator.ready.wait().await?; + let sum: u64 = values + .iter() + .map(|value| value.load(Ordering::Relaxed)) + .sum(); + // An async checkpoint can be awaited here while workers wait at resume. + tokio::task::yield_now().await; + published.store(sum, Ordering::Relaxed); + println!("coordinator: published aggregate {sum}"); + if sum >= 18 { + // Stop without releasing anyone into another computation round. + ready.close(); + resume.close(); + break; + } + coordinator.resume.wait().await?; + } + for task in tasks { + assert_eq!(task.await.expect("worker panicked"), Err(Closed)); + } + assert_eq!(published.load(Ordering::Relaxed), 18); + println!("convergence: all workers stopped after the third aggregate"); + Ok(()) +} + +async fn wait_once(mut member: Member) -> Result<(), Closed> { + member.ready.wait().await?; + member.resume.wait().await?; + Ok(()) +} + +async fn fail(_member: Member) -> Result<(), &'static str> { + // The job error stays in its result; Closed tells peers that no next round is available. + Err("input validation failed") +} + +async fn failure_closes_the_group() -> Result<(), Closed> { + let ready = Phaser::new(); + let resume = Phaser::new(); + let healthy = Member::register(&ready, &resume)?; + let failing = Member::register(&ready, &resume)?; + let (peer, failure) = tokio::join!(wait_once(healthy), fail(failing)); + assert_eq!(peer, Err(Closed)); + assert_eq!(failure, Err("input validation failed")); + assert_eq!(ready.phase(), 0); + assert_eq!(resume.phase(), 0); + println!("failure: peers observed Closed, not successful phase completion"); + Ok(()) +} + +async fn cancelling_an_unpolled_task_closes_the_group() -> Result<(), Closed> { + let ready = Phaser::new(); + let resume = Phaser::new(); + let peer = Member::register(&ready, &resume)?; + let cancelled = wait_once(Member::register(&ready, &resume)?); + drop(cancelled); + assert_eq!(wait_once(peer).await, Err(Closed)); + assert_eq!(ready.phase(), 0); + println!("cancellation: dropping an unpolled task closed both gates"); + Ok(()) +} diff --git a/examples/src/phaser_groups.rs b/examples/src/phaser_groups.rs new file mode 100644 index 00000000..7d171bc1 --- /dev/null +++ b/examples/src/phaser_groups.rs @@ -0,0 +1,188 @@ +// 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. + +//! Group local arrivals before one global rendezvous, then release the local workers. +//! +//! Java mapping: a group representative contributes one root participant. Unlike a native child +//! Phaser, this composition runs an explicit driver task and uses separate local counters. A +//! local ready phase must never authorize the next round until the root has also completed. +//! The example uses a fixed cohort for three rounds; automatic parent registration and arbitrary +//! concurrent changes to a hierarchical participant set are not provided by this composition. +//! No performance advantage over a flat Phaser is claimed without workload-specific measurement. +//! +//! Run: cargo run -p examples --example phaser_groups + +use std::sync::Arc; +use std::sync::atomic::AtomicU64; +use std::sync::atomic::Ordering; + +use asyncband::phaser::Closed; +use asyncband::phaser::Phaser; +use asyncband::phaser::PhaserParticipant; + +const GROUPS: usize = 2; +const WORKERS_PER_GROUP: usize = 2; +const ROUNDS: u64 = 3; + +struct AbortOnDrop { + phasers: [Phaser; 3], + armed: bool, +} + +impl Drop for AbortOnDrop { + fn drop(&mut self) { + if self.armed { + for phaser in &self.phasers { + phaser.close(); + } + } + } +} + +struct LocalMember { + // Abort before dropping either participant, including when a task is never polled. + abort: AbortOnDrop, + ready: PhaserParticipant, + resume: PhaserParticipant, +} + +impl LocalMember { + fn register(root: &Phaser, ready: &Phaser, resume: &Phaser) -> Result { + Ok(Self { + abort: AbortOnDrop { + phasers: [root.clone(), ready.clone(), resume.clone()], + armed: true, + }, + ready: ready.register()?, + resume: resume.register()?, + }) + } +} + +async fn worker( + mut member: LocalMember, + id: usize, + values: Arc>, + fail: bool, +) -> Result<(), Closed> { + for round in 1..=ROUNDS { + if fail && round == 2 { + // The abort guard closes the root before any participant is withdrawn. + return Err(Closed); + } + values[id].store(round, Ordering::Relaxed); + member.ready.wait().await?; + member.resume.wait().await?; + // Every group, not merely this worker's local group, must have published this round. + assert!( + values + .iter() + .all(|value| value.load(Ordering::Relaxed) >= round) + ); + } + member.abort.armed = false; + Ok(()) +} + +struct GroupDriver { + // Keep the root obligation behind the local abort guard in the same owned task argument. + local: LocalMember, + root: PhaserParticipant, +} + +async fn drive_group(mut driver: GroupDriver) -> Result<(), Closed> { + for _ in 0..ROUNDS { + driver.local.ready.wait().await?; + driver.root.wait().await?; + driver.local.resume.wait().await?; + } + driver.local.abort.armed = false; + Ok(()) +} + +struct CloseRootOnDrop(Phaser); + +impl Drop for CloseRootOnDrop { + fn drop(&mut self) { + self.0.close(); + } +} + +#[tokio::main(flavor = "current_thread")] +async fn main() -> Result<(), Closed> { + run_groups(false).await?; + assert_eq!(run_groups(true).await, Err(Closed)); + println!("group failure: root closure propagated to every local group"); + Ok(()) +} + +async fn run_groups(fail_one_worker: bool) -> Result<(), Closed> { + let root = Phaser::new(); + let mut coordinator = root.register()?; + // Created after the participant so cancellation closes the root before withdrawing it. + let _close_root = CloseRootOnDrop(root.clone()); + let values = Arc::new( + (0..GROUPS * WORKERS_PER_GROUP) + .map(|_| AtomicU64::new(0)) + .collect::>(), + ); + let mut tasks = Vec::new(); + for group in 0..GROUPS { + let ready = Phaser::new(); + let resume = Phaser::new(); + let driver = LocalMember::register(&root, &ready, &resume)?; + let representative = root.register()?; + for worker_id in 0..WORKERS_PER_GROUP { + let member = LocalMember::register(&root, &ready, &resume)?; + let id = group * WORKERS_PER_GROUP + worker_id; + tasks.push(tokio::spawn(worker( + member, + id, + values.clone(), + fail_one_worker && id == 0, + ))); + } + tasks.push(tokio::spawn(drive_group(GroupDriver { + local: driver, + root: representative, + }))); + } + assert_eq!(root.registered_parties(), GROUPS as u32 + 1); + + for round in 1..=ROUNDS { + if let Err(error) = coordinator.wait().await { + // Root closure propagates through the group drivers to their local waiters. + root.close(); + for task in tasks { + let _ = task.await.expect("group task panicked"); + } + assert_eq!(root.phase(), 1); + return Err(error); + } + println!("root: all {GROUPS} groups completed round {round}"); + } + for task in tasks { + task.await.expect("group task panicked")?; + } + assert!( + values + .iter() + .all(|value| value.load(Ordering::Relaxed) == ROUNDS) + ); + println!("grouped fan-in: four workers synchronized through two root representatives"); + Ok(()) +} diff --git a/examples/src/phaser_rounds.rs b/examples/src/phaser_rounds.rs new file mode 100644 index 00000000..bcec1a89 --- /dev/null +++ b/examples/src/phaser_rounds.rs @@ -0,0 +1,137 @@ +// 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 start gate, changing membership, split arrival/wait, observers, and cancellation retry. +//! +//! Java mappings: register/bulkRegister become owned participant handles; arriveAndAwaitAdvance +//! becomes participant.wait; arrive/awaitAdvance become arrive/wait or an independent observer. +//! The setup participant prevents early workers from completing the initial phase before the +//! whole batch is registered. Unlike Java's default policy, an empty phaser stays reusable. +//! +//! Run: cargo run -p examples --example phaser_rounds + +use asyncband::phaser::Closed; +use asyncband::phaser::Phaser; +use asyncband::phaser::PhaserParticipant; + +#[tokio::main(flavor = "current_thread")] +async fn main() -> Result<(), Closed> { + start_gate().await?; + changing_membership().await?; + cancellation_retry().await?; + Ok(()) +} + +async fn start_gate() -> Result<(), Closed> { + let phaser = Phaser::new(); + let setup = phaser.register()?; + let mut tasks = Vec::new(); + for mut participant in phaser.register_many(3)? { + tasks.push(tokio::spawn(async move { + participant.wait().await?; + // Initialization is complete; real work may now start. + Ok::<_, Closed>(()) + })); + } + setup.deregister()?; + for task in tasks { + task.await.expect("worker panicked")?; + } + assert_eq!(phaser.registered_parties(), 0); + assert!(!phaser.is_closed()); + println!("start gate: all three workers released; the empty phaser remains reusable"); + Ok(()) +} + +async fn work(mut participant: PhaserParticipant, rounds: usize) -> Result<(), Closed> { + for _ in 0..rounds { + let observed = participant.arrive()?; + // This work does not hold up the other participants' arrivals. + tokio::task::yield_now().await; + let next = participant.wait().await?; + assert_ne!(observed, next); + } + participant.deregister()?; + Ok(()) +} + +async fn changing_membership() -> Result<(), Closed> { + let phaser = Phaser::new(); + let mut coordinator = phaser.register()?; + let worker = tokio::spawn(work(phaser.register()?, 3)); + + let progress = phaser.clone(); + let observer = tokio::spawn(async move { + let mut observed = progress.phase(); + while let Ok(next) = progress.wait_for_advance(observed).await { + println!("observer: phase {observed} -> {next}"); + // A slow observer may skip phases; it never delays workers. + observed = next; + } + }); + let target = phaser.clone(); + let target_wait = tokio::spawn(async move { wait_until(&target, 2).await }); + + coordinator.wait().await?; + // The coordinator has not arrived in the next phase, so this registration joins that phase. + let joining_worker = tokio::spawn(work(phaser.register()?, 2)); + assert_eq!(phaser.registered_parties(), 3); + coordinator.wait().await?; + coordinator.wait().await?; + + worker.await.expect("worker panicked")?; + joining_worker.await.expect("joining worker panicked")?; + assert!(target_wait.await.expect("target observer panicked")? >= 2); + coordinator.deregister()?; + phaser.close(); + observer.await.expect("progress observer panicked"); + println!("dynamic membership: a second worker joined after the first round"); + Ok(()) +} + +/// A caller-side numeric threshold, for a run known not to cross counter wraparound. +/// Java's awaitPhase example uses the same loop over observed advances. This observer does not +/// register, drive the computation, or guarantee one notification for every intermediate phase. +async fn wait_until(phaser: &Phaser, target: u64) -> Result { + let mut observed = phaser.phase(); + while observed < target { + observed = phaser.wait_for_advance(observed).await?; + } + Ok(observed) +} + +async fn cancellation_retry() -> Result<(), Closed> { + let phaser = Phaser::new(); + let mut participant = phaser.register()?; + let mut peer = phaser.register()?; + let observed = phaser.phase(); + + tokio::select! { + biased; + result = participant.wait() => panic!("peer has not arrived: {result:?}"), + // Poll the wait once, then cancel it deterministically without a timer or sleep. + _ = std::future::ready(()) => {} + } + assert_eq!(phaser.arrived_parties(), 1); + peer.arrive()?; + assert_ne!(phaser.phase(), observed); + + assert_eq!(participant.wait().await?, phaser.phase()); + assert_eq!(phaser.arrived_parties(), 0); + println!("cancellation: retry observed the completed round without arriving in the next one"); + Ok(()) +} diff --git a/tests-integration/tests/phaser_test.rs b/tests-integration/tests/phaser_test.rs index 33287b9e..eea7e8ac 100644 --- a/tests-integration/tests/phaser_test.rs +++ b/tests-integration/tests/phaser_test.rs @@ -15,38 +15,88 @@ // 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 phaser = Phaser::new(); + let mut first = phaser.register().unwrap(); + let mut second = phaser.register().unwrap(); - let first_wait = tokio::spawn(async move { first.arrive_and_wait().await }); + let first_wait = tokio::spawn(async move { first.wait().await }); tokio::task::yield_now().await; - let phase = second.arrive(); - assert_eq!(first_wait.await.unwrap(), phaser.phase()); + let phase = second.arrive().unwrap(); + assert_eq!(first_wait.await.unwrap().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 phaser = Phaser::new(); let observed = phaser.phase(); - let mut first = phaser.register(); - let second = phaser.register(); - let observer_phaser = Arc::clone(&phaser); + let mut first = phaser.register().unwrap(); + let second = phaser.register().unwrap(); + let observer_phaser = phaser.clone(); 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(); + first.arrive().unwrap(); + second.deregister().unwrap(); - assert_eq!(observer.await.unwrap(), phaser.phase()); + assert_eq!(observer.await.unwrap().unwrap(), phaser.phase()); assert_eq!(phaser.registered_parties(), 1); } + +#[test] +fn arrivals_publish_each_workers_writes_across_threads() { + use std::sync::atomic::AtomicUsize; + use std::sync::atomic::Ordering; + + let phaser = Phaser::new(); + let values = std::array::from_fn::<_, 4, _>(|_| AtomicUsize::new(0)); + let participants = phaser.register_many(4).unwrap(); + std::thread::scope(|scope| { + for (id, mut participant) in participants.into_iter().enumerate() { + let values = &values; + scope.spawn(move || { + pollster::block_on(async { + for round in 1..=16 { + values[id].store(round, Ordering::Relaxed); + participant.wait().await.unwrap(); + assert!( + values + .iter() + .all(|value| value.load(Ordering::Relaxed) == round) + ); + // Keep the next round's writers behind this read-side rendezvous. + participant.wait().await.unwrap(); + } + }); + }); + } + }); +} + +#[tokio::test] +async fn a_failed_task_can_close_the_group_without_reporting_phase_completion() { + use asyncband::phaser::Closed; + + let phaser = Phaser::new(); + let mut worker = phaser.register().unwrap(); + let failing = phaser.register().unwrap(); + let observed = phaser.phase(); + let (arrived, arrival) = tokio::sync::oneshot::channel(); + let task = tokio::spawn(async move { + worker.arrive().unwrap(); + arrived.send(()).unwrap(); + worker.wait().await + }); + arrival.await.unwrap(); + failing.phaser().close(); + drop(failing); + assert_eq!(task.await.unwrap(), Err(Closed)); + assert_eq!(phaser.phase(), observed); + assert_eq!(phaser.registered_parties(), 0); +} diff --git a/tests-integration/tests/traits_test.rs b/tests-integration/tests/traits_test.rs index b7689c3e..a9b2efbd 100644 --- a/tests-integration/tests/traits_test.rs +++ b/tests-integration/tests/traits_test.rs @@ -34,7 +34,7 @@ use asyncband::once::Once; use asyncband::once::OnceCell; use asyncband::once::OnceMap; use asyncband::oneshot; -use asyncband::phaser::Phase; +use asyncband::phaser::Closed; use asyncband::phaser::Phaser; use asyncband::phaser::PhaserParticipant; use asyncband::pool; @@ -106,7 +106,7 @@ 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::>(); @@ -178,7 +178,7 @@ fn public_types_are_unpin() { assert_unpin::>(); assert_unpin::>(); assert_unpin::>(); - assert_unpin::(); + assert_unpin::(); assert_unpin::(); assert_unpin::(); assert_unpin::>(); From 6ebd6d28847aeda3d2e8c55d28cd43718fbffeda Mon Sep 17 00:00:00 2001 From: tison Date: Fri, 11 Sep 2026 11:34:54 +0800 Subject: [PATCH 08/12] refactor(phaser): simplify participant registration Signed-off-by: tison --- CHANGELOG.md | 2 +- asyncband/src/phaser/mod.rs | 145 ++++++++++++------- asyncband/src/phaser/tests.rs | 185 ++++++++++++++++++------- examples/src/phaser_completion.rs | 10 +- examples/src/phaser_groups.rs | 16 +-- examples/src/phaser_rounds.rs | 22 ++- tests-integration/tests/phaser_test.rs | 16 +-- tests-integration/tests/traits_test.rs | 3 + 8 files changed, 262 insertions(+), 137 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 84570c7a..5c1b21ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ All notable changes to this project will be documented in this file. ### New features -* Add an opt-in runtime-agnostic `Phaser` with shared observer handles, dynamic RAII participants, `u64` phase numbers, split arrival/wait with cancellation-resilient retries, and explicit closure that releases unfinished waits with `Closed`. +* Add an opt-in runtime-agnostic `Phaser` with shared observer handles, dynamic RAII participants registered individually or in batches through an owning iterator, `u64` phase numbers, split arrival/wait with cancellation-resilient retries, and explicit closure that releases unfinished waits with `Closed`. * Add bounded MPSC `reserve` and `try_reserve` methods returning a `Permit`, allowing callers to wait for capacity before constructing a message; pending sends and reservations receive capacity in wait-queue order, and unused permits release capacity without claiming message order. ### Bug fixes diff --git a/asyncband/src/phaser/mod.rs b/asyncband/src/phaser/mod.rs index f7ac3efd..ae9d509e 100644 --- a/asyncband/src/phaser/mod.rs +++ b/asyncband/src/phaser/mod.rs @@ -36,9 +36,8 @@ //! # #[tokio::main(flavor = "current_thread")] //! # async fn main() -> Result<(), asyncband::phaser::Closed> { //! let phaser = Phaser::new(); -//! let mut participants = phaser.register_many(2)?; -//! let mut worker = participants.pop().unwrap(); -//! let mut coordinator = participants.pop().unwrap(); +//! let mut worker = phaser.register_one()?; +//! let mut coordinator = phaser.register_one()?; //! //! let task = tokio::spawn(async move { //! for _ in 0..3 { @@ -61,7 +60,7 @@ //! # Membership and cancellation //! //! Registration joins the phase current at the registration's synchronization point. In -//! particular, [`register_many`](Phaser::register_many) registers its entire batch in one phase. +//! particular, [`register`](Phaser::register) registers its entire batch in one phase. //! Dropping or [`deregistering`](PhaserParticipant::deregister) a participant removes its future //! obligations and discharges any outstanding arrival in the current phase. An empty phaser is //! dormant and can be reused; it does not advance repeatedly or close automatically. @@ -89,32 +88,10 @@ //! Phase numbers start at zero and wrap from `u64::MAX` to zero. Pass a value previously obtained //! from the same phaser to `wait_for_advance`; it tests for a different phase, not a target number //! or numeric threshold. An observation must not be retained across a full counter cycle. -//! -//! # Java Phaser use cases -//! -//! The following examples are in the repository's `examples` package. Run one with -//! `cargo run -p examples --example `. -//! -//! | Use case | Rust expression | Runnable example | -//! |----------|-----------------|------------------| -//! | Dynamic registration, repeated rounds, and a one-shot start gate | Shared handles, `register_many`, participant `wait`, and `deregister` | `phaser_rounds` | -//! | Split arrival/wait, progress observation, numeric targets, and cancellation retry | `arrive`, participant `wait`, and `wait_for_advance` | `phaser_rounds` | -//! | `onAdvance` aggregation, asynchronous finalization, and stopping at convergence | A coordinator and separate ready/resume phasers | `phaser_completion` | -//! | Aborting the group after a worker fails | `close`, including an application-owned abort guard | `phaser_completion` | -//! | Grouped fan-in before global release | Local ready/resume phasers and one root participant per group | `phaser_groups` | -//! -//! These compositions do not supply Java's native parent/child phasers, automatic parent -//! registration, a globally shared phase counter across nodes, or an in-primitive `onAdvance` -//! hook. The grouped example has an explicit coordinator task per group; local counters are not -//! global phase numbers. Membership changes in the ready/resume protocols must be applied at -//! coordinated round boundaries. A slow observer cannot run an exactly-once completion hook. -//! Timeouts and task scheduling remain with the caller's runtime. -//! -//! For the Java contracts being mapped, see the -//! [Java Phaser documentation](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/Phaser.html). use std::fmt; use std::future::Future; +use std::iter::FusedIterator; use std::pin::Pin; use std::sync::Arc; use std::task::Context; @@ -143,7 +120,7 @@ impl std::error::Error for Closed {} /// A shared handle to a reusable phase barrier with dynamic participants. /// -/// Cloning this handle does not register a participant. Use [`register`](Self::register) to +/// Cloning this handle does not register a participant. Use [`register_one`](Self::register_one) to /// create a participant that can be moved into an independently spawned task. #[derive(Clone)] pub struct Phaser { @@ -153,8 +130,8 @@ pub struct Phaser { struct State { phase: u64, closed: bool, - registered: u32, - unarrived: u32, + registered: usize, + unarrived: usize, waiters: WakerSet, } @@ -246,18 +223,18 @@ impl Phaser { /// Returns an instantaneous count of registered participants, including those already arrived. /// /// Counts can change between separate queries. This is not a synchronization operation. - pub fn registered_parties(&self) -> u32 { + pub fn registered_parties(&self) -> usize { self.state.lock().registered } /// Returns an instantaneous count of participants that have arrived in the current phase. - pub fn arrived_parties(&self) -> u32 { + pub fn arrived_parties(&self) -> usize { let state = self.state.lock(); state.registered - state.unarrived } /// Returns an instantaneous count of outstanding arrivals in the current phase. - pub fn unarrived_parties(&self) -> u32 { + pub fn unarrived_parties(&self) -> usize { self.state.lock().unarrived } @@ -268,31 +245,44 @@ impl Phaser { /// /// # Panics /// - /// Panics if the registered count would exceed `u32::MAX`. - pub fn register(&self) -> Result { + /// Panics if the registered count would exceed `usize::MAX`. + pub fn register_one(&self) -> Result { let phaser = self.clone(); - self.register_inner(1)?; + self.do_register(1)?; Ok(PhaserParticipant::new(phaser)) } - /// Registers an entire batch in one phase, or returns [`Closed`] without registering anyone. + /// Registers an entire batch in one phase and returns an iterator over its participants. /// - /// On an open phaser, a zero-sized batch does nothing. The batch is reserved before any - /// participant count is changed. Every returned handle must be used or dropped. + /// Registration is immediate, including participants not yet yielded by the iterator. No + /// storage is allocated for the batch. Dropping the iterator withdraws its remaining + /// 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. + /// + /// ``` + /// use asyncband::phaser::Phaser; + /// + /// let phaser = Phaser::new(); + /// let participants: Vec<_> = phaser.register(3)?.collect(); + /// # Ok::<(), asyncband::phaser::Closed>(()) + /// ``` /// /// # Panics /// - /// Panics if the batch cannot fit in a vector or the registered count would exceed `u32::MAX`. - pub fn register_many(&self, parties: u32) -> Result, Closed> { - let capacity = usize::try_from(parties) - .expect("Phaser participant count must fit in the platform's usize"); - let mut participants = Vec::with_capacity(capacity); - self.register_inner(parties)?; - participants.extend((0..parties).map(|_| PhaserParticipant::new(self.clone()))); - Ok(participants) + /// Panics if the registered count would exceed `usize::MAX`. + pub fn register(&self, parties: usize) -> Result { + let phaser = self.clone(); + self.do_register(parties)?; + Ok(PhaserParticipants { + phaser, + remaining: parties, + }) } - fn register_inner(&self, parties: u32) -> Result<(), Closed> { + fn do_register(&self, parties: usize) -> Result<(), Closed> { let mut state = self.state.lock(); if state.closed { return Err(Closed); @@ -333,6 +323,59 @@ impl Phaser { } } +/// An owning iterator over a batch registered by [`Phaser::register`]. +/// +/// All participants are already registered, so even those not yet yielded hold back phase +/// advancement. Dropping this iterator withdraws the remaining participants in one state +/// transition. Yielded participants are independent and retain their registrations. +/// +/// Closure does not prevent iteration over this existing batch, but arrival and waiting on the +/// yielded participants return [`Closed`]. This iterator is not cloneable. +#[must_use = "dropping the iterator withdraws participants that have not been yielded"] +#[derive(Debug)] +pub struct PhaserParticipants { + phaser: Phaser, + remaining: usize, +} + +impl Iterator for PhaserParticipants { + type Item = PhaserParticipant; + + fn next(&mut self) -> Option { + if self.remaining == 0 { + return None; + } + let participant = PhaserParticipant::new(self.phaser.clone()); + self.remaining -= 1; + Some(participant) + } + + fn size_hint(&self) -> (usize, Option) { + (self.remaining, Some(self.remaining)) + } +} + +impl ExactSizeIterator for PhaserParticipants {} + +impl FusedIterator for PhaserParticipants {} + +impl Drop for PhaserParticipants { + fn drop(&mut self) { + if self.remaining == 0 { + return; + } + let wakers = { + let mut state = self.phaser.state.lock(); + // Unyielded participants have never arrived and prevent their phase from advancing. + state.registered -= self.remaining; + state.unarrived -= self.remaining; + self.remaining = 0; + state.advance_if_ready() + }; + wake_all(wakers.into_iter().flatten()); + } +} + /// One participant's arrival obligation in every phase until it deregisters or is dropped. /// /// This handle is not cloneable. Dropping it withdraws the participant, including any outstanding @@ -413,10 +456,10 @@ impl PhaserParticipant { /// twice. This always removes the registration, including after closure. The pending /// observation is abandoned. Dropping a participant has the same membership effect. pub fn deregister(mut self) -> Result { - self.deregister_inner() + self.do_deregister() } - fn deregister_inner(&mut self) -> Result { + fn do_deregister(&mut self) -> Result { let (result, wakers) = { let mut state = self.phaser.state.lock(); self.registered = false; @@ -439,7 +482,7 @@ impl PhaserParticipant { impl Drop for PhaserParticipant { fn drop(&mut self) { if self.registered { - let _ = self.deregister_inner(); + let _ = self.do_deregister(); } } } diff --git a/asyncband/src/phaser/tests.rs b/asyncband/src/phaser/tests.rs index d1a27bac..774f307e 100644 --- a/asyncband/src/phaser/tests.rs +++ b/asyncband/src/phaser/tests.rs @@ -46,13 +46,94 @@ impl Wake for PanicWake { } #[test] -fn register_many_joins_one_observed_phase() { +fn batch_registration_joins_one_observed_phase() { let phaser = Phaser::new(); - let participants = phaser.register_many(3).unwrap(); + let mut participants = phaser.register(3).unwrap(); assert_eq!(participants.len(), 3); assert_eq!(phaser.registered_parties(), 3); assert_eq!(phaser.unarrived_parties(), 3); + + let mut first = participants.next().unwrap(); + let observed = first.arrive().unwrap(); + let mut second = participants.next().unwrap(); + second.arrive().unwrap(); + assert_eq!(participants.len(), 1); + assert_eq!(phaser.phase(), observed); + + let counter = Arc::new(CountWake(AtomicUsize::new(0))); + let waker = Waker::from(counter.clone()); + let mut wait = Box::pin(first.wait()); + assert!( + wait.as_mut() + .poll(&mut Context::from_waker(&waker)) + .is_pending() + ); + drop(participants); + assert_eq!(counter.0.load(Ordering::Relaxed), 1); + assert_eq!(poll_once(wait.as_mut()), Poll::Ready(Ok(phaser.phase()))); + assert_ne!(phaser.phase(), observed); + assert_eq!(phaser.registered_parties(), 2); + assert_eq!(phaser.unarrived_parties(), 2); +} + +#[test] +fn collecting_a_batch_can_unwind_without_leaking_registrations() { + let phaser = Phaser::new(); + let mut coordinator = phaser.register_one().unwrap(); + let observed = phaser.phase(); + + assert!( + panic::catch_unwind(|| { + let _: Vec<_> = phaser + .register(3) + .unwrap() + .enumerate() + .map(|(index, participant)| { + assert_ne!(index, 1, "task setup failed"); + participant + }) + .collect(); + }) + .is_err() + ); + assert_eq!(phaser.phase(), observed); + assert_eq!(phaser.registered_parties(), 1); + assert_eq!(phaser.unarrived_parties(), 1); + coordinator.arrive().unwrap(); + assert_ne!(phaser.phase(), observed); +} + +#[test] +fn exhausted_batch_does_not_advance_a_dormant_phaser() { + let phaser = Phaser::new(); + let mut participants = phaser.register(1).unwrap(); + drop(participants.next().unwrap()); + let completed = phaser.phase(); + + assert_eq!(participants.len(), 0); + assert!(participants.next().is_none()); + drop(participants); + assert_eq!(phaser.phase(), completed); + assert_eq!(phaser.registered_parties(), 0); +} + +#[test] +fn an_existing_batch_can_be_iterated_and_withdrawn_after_close() { + let phaser = Phaser::new(); + let mut participants = phaser.register(3).unwrap(); + let observed = phaser.phase(); + phaser.close(); + + let mut participant = participants.next().unwrap(); + drop(participants); + assert_eq!(phaser.registered_parties(), 1); + assert_eq!(phaser.unarrived_parties(), 1); + assert_eq!(participant.arrive(), Err(Closed)); + drop(participant); + assert_eq!(phaser.phase(), observed); + assert_eq!(phaser.registered_parties(), 0); + assert_eq!(phaser.unarrived_parties(), 0); } #[test] @@ -60,7 +141,7 @@ fn registering_zero_parties_is_a_noop() { let phaser = Phaser::new(); let phase = phaser.phase(); - assert!(phaser.register_many(0).unwrap().is_empty()); + assert_eq!(phaser.register(0).unwrap().len(), 0); assert_eq!(phaser.phase(), phase); assert_eq!(phaser.registered_parties(), 0); assert_eq!(phaser.unarrived_parties(), 0); @@ -70,8 +151,8 @@ fn registering_zero_parties_is_a_noop() { fn participants_advance_across_repeated_phases() { let phaser = Phaser::new(); let phase0 = phaser.phase(); - let mut first = phaser.register().unwrap(); - let mut second = phaser.register().unwrap(); + let mut first = phaser.register_one().unwrap(); + let mut second = phaser.register_one().unwrap(); assert_eq!(first.arrive().unwrap(), phase0); assert_eq!(phaser.arrived_parties(), 1); @@ -88,7 +169,7 @@ fn participants_advance_across_repeated_phases() { #[test] fn unpolled_wait_future_does_not_arrive() { let phaser = Phaser::new(); - let mut participant = phaser.register().unwrap(); + let mut participant = phaser.register_one().unwrap(); let wait = participant.wait(); @@ -101,8 +182,8 @@ fn unpolled_wait_future_does_not_arrive() { fn cancelled_wait_retry_waits_for_original_phase_after_advance() { let phaser = Phaser::new(); let phase0 = phaser.phase(); - let mut first = phaser.register().unwrap(); - let mut second = phaser.register().unwrap(); + let mut first = phaser.register_one().unwrap(); + let mut second = phaser.register_one().unwrap(); { let mut cancelled = Box::pin(first.wait()); @@ -123,8 +204,8 @@ fn cancelled_wait_retry_waits_for_original_phase_after_advance() { #[test] fn cancelled_wait_retry_before_advance_does_not_arrive_twice() { let phaser = Phaser::new(); - let mut first = phaser.register().unwrap(); - let mut second = phaser.register().unwrap(); + let mut first = phaser.register_one().unwrap(); + let mut second = phaser.register_one().unwrap(); { let mut cancelled = Box::pin(first.wait()); @@ -143,7 +224,7 @@ fn cancelled_wait_retry_before_advance_does_not_arrive_twice() { fn dropping_last_participant_advances_once_and_dormant_phaser_can_be_reused() { let phaser = Phaser::new(); let phase0 = phaser.phase(); - let participant = phaser.register().unwrap(); + let participant = phaser.register_one().unwrap(); drop(participant); let phase1 = phaser.phase(); @@ -151,7 +232,7 @@ fn dropping_last_participant_advances_once_and_dormant_phaser_can_be_reused() { assert_eq!(phaser.registered_parties(), 0); assert_eq!(phaser.arrived_parties(), 0); - let mut participant = phaser.register().unwrap(); + let mut participant = phaser.register_one().unwrap(); assert_eq!(participant.arrive().unwrap(), phase1); assert_ne!(phaser.phase(), phase1); } @@ -160,8 +241,8 @@ fn dropping_last_participant_advances_once_and_dormant_phaser_can_be_reused() { fn dropping_an_arrived_participant_only_removes_its_next_phase_registration() { let phaser = Phaser::new(); let phase0 = phaser.phase(); - let mut first = phaser.register().unwrap(); - let mut second = phaser.register().unwrap(); + let mut first = phaser.register_one().unwrap(); + let mut second = phaser.register_one().unwrap(); first.arrive().unwrap(); drop(first); @@ -177,11 +258,11 @@ fn dropping_an_arrived_participant_only_removes_its_next_phase_registration() { fn registration_before_last_arrival_joins_and_delays_current_phase() { let phaser = Phaser::new(); let phase = phaser.phase(); - let mut first = phaser.register().unwrap(); - let mut second = phaser.register().unwrap(); + let mut first = phaser.register_one().unwrap(); + let mut second = phaser.register_one().unwrap(); first.arrive().unwrap(); - let mut joining = phaser.register().unwrap(); + let mut joining = phaser.register_one().unwrap(); second.arrive().unwrap(); assert_eq!(phaser.phase(), phase); @@ -194,13 +275,13 @@ fn registration_before_last_arrival_joins_and_delays_current_phase() { fn registration_after_last_arrival_joins_the_advanced_phase() { let phaser = Phaser::new(); let phase0 = phaser.phase(); - let mut first = phaser.register().unwrap(); + let mut first = phaser.register_one().unwrap(); first.arrive().unwrap(); let phase1 = phaser.phase(); assert_ne!(phase1, phase0); - let mut joining = phaser.register().unwrap(); + let mut joining = phaser.register_one().unwrap(); assert_eq!(phaser.registered_parties(), 2); assert_eq!(phaser.unarrived_parties(), 2); assert_eq!(joining.arrive().unwrap(), phase1); @@ -211,8 +292,8 @@ fn registration_after_last_arrival_joins_the_advanced_phase() { fn registration_before_last_participant_drop_joins_the_current_phase() { let phaser = Phaser::new(); let phase0 = phaser.phase(); - let participant = phaser.register().unwrap(); - let joining = phaser.register().unwrap(); + let participant = phaser.register_one().unwrap(); + let joining = phaser.register_one().unwrap(); drop(participant); @@ -227,11 +308,11 @@ fn registration_before_last_participant_drop_joins_the_current_phase() { fn registration_after_last_participant_drop_joins_the_advanced_phase() { let phaser = Phaser::new(); let phase0 = phaser.phase(); - let participant = phaser.register().unwrap(); + let participant = phaser.register_one().unwrap(); drop(participant); let phase1 = phaser.phase(); - let joining = phaser.register().unwrap(); + let joining = phaser.register_one().unwrap(); assert_ne!(phase1, phase0); assert_eq!(phaser.registered_parties(), 1); @@ -255,7 +336,7 @@ fn wait_for_advance_is_a_cancel_safe_non_participant_observer() { } assert_eq!(phaser.registered_parties(), 0); - let participant = phaser.register().unwrap(); + let participant = phaser.register_one().unwrap(); drop(participant); assert_eq!(counter.0.load(Ordering::Relaxed), 0); } @@ -264,7 +345,7 @@ fn wait_for_advance_is_a_cancel_safe_non_participant_observer() { fn advancing_a_phase_wakes_every_registered_waiter_once() { let phaser = Phaser::new(); let observed = phaser.phase(); - let participant = phaser.register().unwrap(); + let participant = phaser.register_one().unwrap(); 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)); @@ -300,7 +381,7 @@ fn advancing_a_phase_wakes_every_registered_waiter_once() { fn cancelling_a_woken_waiter_does_not_unregister_a_next_phase_waiter() { let phaser = Phaser::new(); let phase0 = phaser.phase(); - let participant = phaser.register().unwrap(); + let participant = phaser.register_one().unwrap(); 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); @@ -315,7 +396,7 @@ fn cancelling_a_woken_waiter_does_not_unregister_a_next_phase_waiter() { assert_ne!(phase1, phase0); assert_eq!(stale_counter.0.load(Ordering::Relaxed), 1); - let participant = phaser.register().unwrap(); + let participant = phaser.register_one().unwrap(); 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); @@ -338,8 +419,8 @@ fn cancelling_a_woken_waiter_does_not_unregister_a_next_phase_waiter() { fn panicking_waker_does_not_lose_a_pending_phase() { let phaser = Phaser::new(); let phase0 = phaser.phase(); - let mut first = phaser.register().unwrap(); - let mut second = phaser.register().unwrap(); + let mut first = phaser.register_one().unwrap(); + let mut second = phaser.register_one().unwrap(); 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)); @@ -374,7 +455,7 @@ fn panicking_waker_does_not_lose_a_pending_phase() { fn a_late_waiter_for_a_completed_phase_is_immediately_ready() { let phaser = Phaser::new(); let observed = phaser.phase(); - let participant = phaser.register().unwrap(); + let participant = phaser.register_one().unwrap(); drop(participant); let mut wait = Box::pin(phaser.wait_for_advance(observed)); @@ -386,7 +467,7 @@ fn phase_identity_wraps_without_an_ordering_contract() { let phaser = Phaser::new(); phaser.state.lock().phase = u64::MAX; let observed = phaser.phase(); - let mut participant = phaser.register().unwrap(); + let mut participant = phaser.register_one().unwrap(); assert_eq!(participant.arrive().unwrap(), observed); assert_eq!(phaser.phase(), 0); @@ -396,22 +477,22 @@ fn phase_identity_wraps_without_an_ordering_contract() { #[test] fn registration_overflow_panics_without_partially_updating_state() { let phaser = Phaser::new(); - { - let mut state = phaser.state.lock(); - state.registered = u32::MAX; - state.unarrived = u32::MAX; - } + let participants = phaser.register(usize::MAX).unwrap(); - assert!(panic::catch_unwind(|| phaser.register().unwrap()).is_err()); - assert_eq!(phaser.registered_parties(), u32::MAX); - assert_eq!(phaser.unarrived_parties(), u32::MAX); + assert!(panic::catch_unwind(|| phaser.register_one().unwrap()).is_err()); + assert!(panic::catch_unwind(|| phaser.register(2).unwrap()).is_err()); + assert_eq!(phaser.registered_parties(), usize::MAX); + assert_eq!(phaser.unarrived_parties(), usize::MAX); + drop(participants); + assert_eq!(phaser.registered_parties(), 0); + assert_eq!(phaser.unarrived_parties(), 0); } #[test] fn explicit_arrival_and_wait_observe_the_same_completed_phase() { let phaser = Phaser::new(); - let mut first = phaser.register().unwrap(); - let mut second = phaser.register().unwrap(); + let mut first = phaser.register_one().unwrap(); + let mut second = phaser.register_one().unwrap(); let observed = first.arrive().unwrap(); second.arrive().unwrap(); let next = phaser.phase(); @@ -436,8 +517,8 @@ fn explicit_arrival_and_wait_observe_the_same_completed_phase() { #[test] fn explicit_arrival_replaces_a_cancelled_pending_observation() { let phaser = Phaser::new(); - let mut first = phaser.register().unwrap(); - let mut second = phaser.register().unwrap(); + let mut first = phaser.register_one().unwrap(); + let mut second = phaser.register_one().unwrap(); assert!(poll_once(Box::pin(first.wait()).as_mut()).is_pending()); second.arrive().unwrap(); let next = first.arrive().unwrap(); @@ -453,7 +534,7 @@ fn explicit_arrival_replaces_a_cancelled_pending_observation() { fn cloned_handles_observe_without_registering_and_participants_own_the_state() { let phaser = Phaser::new(); let observer = phaser.clone(); - let mut participant = phaser.register().unwrap(); + let mut participant = phaser.register_one().unwrap(); drop(phaser); assert_eq!(observer.registered_parties(), 1); let observed = observer.phase(); @@ -467,8 +548,8 @@ fn cloned_handles_observe_without_registering_and_participants_own_the_state() { #[test] fn closing_wakes_all_waiters_once_and_rejects_new_obligations() { let phaser = Phaser::new(); - let mut first = phaser.register().unwrap(); - let second = phaser.register().unwrap(); + let mut first = phaser.register_one().unwrap(); + let second = phaser.register_one().unwrap(); let observed = first.arrive().unwrap(); let counter = Arc::new(CountWake(AtomicUsize::new(0))); let waker = Waker::from(counter.clone()); @@ -486,9 +567,9 @@ fn closing_wakes_all_waiters_once_and_rejects_new_obligations() { assert_eq!(poll_once(wait.as_mut()), Poll::Ready(Err(Closed))); drop(wait); assert_eq!(first.arrive(), Err(Closed)); - assert!(matches!(phaser.register(), Err(Closed))); - assert!(matches!(phaser.register_many(2), Err(Closed))); - assert!(matches!(phaser.register_many(0), Err(Closed))); + assert!(matches!(phaser.register_one(), Err(Closed))); + assert!(matches!(phaser.register(2), Err(Closed))); + assert!(matches!(phaser.register(0), Err(Closed))); assert_eq!(first.deregister(), Err(Closed)); drop(second); assert_eq!(phaser.registered_parties(), 0); @@ -499,8 +580,8 @@ fn closing_wakes_all_waiters_once_and_rejects_new_obligations() { #[test] fn completed_arrival_remains_successful_after_close_but_cannot_start_another_round() { let phaser = Phaser::new(); - let mut first = phaser.register().unwrap(); - let mut second = phaser.register().unwrap(); + let mut first = phaser.register_one().unwrap(); + let mut second = phaser.register_one().unwrap(); let observed = first.arrive().unwrap(); let mut observer = Box::pin(phaser.wait_for_advance(observed)); assert!(poll_once(observer.as_mut()).is_pending()); @@ -555,7 +636,7 @@ fn close_survives_a_panicking_waker_and_notifies_other_waiters() { fn a_late_waiter_observes_completion_across_counter_wraparound() { let phaser = Phaser::new(); phaser.state.lock().phase = u64::MAX; - let mut participant = phaser.register().unwrap(); + let mut participant = phaser.register_one().unwrap(); participant.arrive().unwrap(); phaser.close(); assert_eq!( diff --git a/examples/src/phaser_completion.rs b/examples/src/phaser_completion.rs index 9989b484..0ac583fd 100644 --- a/examples/src/phaser_completion.rs +++ b/examples/src/phaser_completion.rs @@ -17,9 +17,9 @@ //! Finalize each round before releasing workers, and close the group on failure or cancellation. //! -//! Java mapping: onAdvance aggregation and convergence become an application coordinator between -//! two rendezvous points. The coordinator may await I/O. Merely running code after one wait, even -//! in a barrier leader, would not stop other workers from starting their next round. +//! An application coordinator aggregates results and checks convergence between two rendezvous +//! points. The coordinator may await I/O. Merely running code after one wait, even in a barrier +//! leader, would not stop other workers from starting their next round. //! //! Membership is fixed within this protocol; changes must update both groups at a common round //! boundary. Each phaser has its own counter, distinct from the application's iteration number. @@ -56,8 +56,8 @@ impl Member { fn register(ready: &Phaser, resume: &Phaser) -> Result { Ok(Self { _close: CloseOnDrop([ready.clone(), resume.clone()]), - ready: ready.register()?, - resume: resume.register()?, + ready: ready.register_one()?, + resume: resume.register_one()?, }) } } diff --git a/examples/src/phaser_groups.rs b/examples/src/phaser_groups.rs index 7d171bc1..f8c0a0c7 100644 --- a/examples/src/phaser_groups.rs +++ b/examples/src/phaser_groups.rs @@ -17,9 +17,9 @@ //! Group local arrivals before one global rendezvous, then release the local workers. //! -//! Java mapping: a group representative contributes one root participant. Unlike a native child -//! Phaser, this composition runs an explicit driver task and uses separate local counters. A -//! local ready phase must never authorize the next round until the root has also completed. +//! A group representative contributes one root participant. Each group runs an explicit driver +//! task and uses separate local counters. A local ready phase must never authorize the next round +//! until the root has also completed. //! The example uses a fixed cohort for three rounds; automatic parent registration and arbitrary //! concurrent changes to a hierarchical participant set are not provided by this composition. //! No performance advantage over a flat Phaser is claimed without workload-specific measurement. @@ -67,8 +67,8 @@ impl LocalMember { phasers: [root.clone(), ready.clone(), resume.clone()], armed: true, }, - ready: ready.register()?, - resume: resume.register()?, + ready: ready.register_one()?, + resume: resume.register_one()?, }) } } @@ -132,7 +132,7 @@ async fn main() -> Result<(), Closed> { async fn run_groups(fail_one_worker: bool) -> Result<(), Closed> { let root = Phaser::new(); - let mut coordinator = root.register()?; + let mut coordinator = root.register_one()?; // Created after the participant so cancellation closes the root before withdrawing it. let _close_root = CloseRootOnDrop(root.clone()); let values = Arc::new( @@ -145,7 +145,7 @@ async fn run_groups(fail_one_worker: bool) -> Result<(), Closed> { let ready = Phaser::new(); let resume = Phaser::new(); let driver = LocalMember::register(&root, &ready, &resume)?; - let representative = root.register()?; + let representative = root.register_one()?; for worker_id in 0..WORKERS_PER_GROUP { let member = LocalMember::register(&root, &ready, &resume)?; let id = group * WORKERS_PER_GROUP + worker_id; @@ -161,7 +161,7 @@ async fn run_groups(fail_one_worker: bool) -> Result<(), Closed> { root: representative, }))); } - assert_eq!(root.registered_parties(), GROUPS as u32 + 1); + assert_eq!(root.registered_parties(), GROUPS + 1); for round in 1..=ROUNDS { if let Err(error) = coordinator.wait().await { diff --git a/examples/src/phaser_rounds.rs b/examples/src/phaser_rounds.rs index bcec1a89..e5cad91e 100644 --- a/examples/src/phaser_rounds.rs +++ b/examples/src/phaser_rounds.rs @@ -17,10 +17,8 @@ //! A start gate, changing membership, split arrival/wait, observers, and cancellation retry. //! -//! Java mappings: register/bulkRegister become owned participant handles; arriveAndAwaitAdvance -//! becomes participant.wait; arrive/awaitAdvance become arrive/wait or an independent observer. //! The setup participant prevents early workers from completing the initial phase before the -//! whole batch is registered. Unlike Java's default policy, an empty phaser stays reusable. +//! whole batch is registered. An empty phaser stays reusable after all participants leave. //! //! Run: cargo run -p examples --example phaser_rounds @@ -38,9 +36,9 @@ async fn main() -> Result<(), Closed> { async fn start_gate() -> Result<(), Closed> { let phaser = Phaser::new(); - let setup = phaser.register()?; + let setup = phaser.register_one()?; let mut tasks = Vec::new(); - for mut participant in phaser.register_many(3)? { + for mut participant in phaser.register(3)? { tasks.push(tokio::spawn(async move { participant.wait().await?; // Initialization is complete; real work may now start. @@ -71,8 +69,8 @@ async fn work(mut participant: PhaserParticipant, rounds: usize) -> Result<(), C async fn changing_membership() -> Result<(), Closed> { let phaser = Phaser::new(); - let mut coordinator = phaser.register()?; - let worker = tokio::spawn(work(phaser.register()?, 3)); + let mut coordinator = phaser.register_one()?; + let worker = tokio::spawn(work(phaser.register_one()?, 3)); let progress = phaser.clone(); let observer = tokio::spawn(async move { @@ -88,7 +86,7 @@ async fn changing_membership() -> Result<(), Closed> { coordinator.wait().await?; // The coordinator has not arrived in the next phase, so this registration joins that phase. - let joining_worker = tokio::spawn(work(phaser.register()?, 2)); + let joining_worker = tokio::spawn(work(phaser.register_one()?, 2)); assert_eq!(phaser.registered_parties(), 3); coordinator.wait().await?; coordinator.wait().await?; @@ -104,8 +102,8 @@ async fn changing_membership() -> Result<(), Closed> { } /// A caller-side numeric threshold, for a run known not to cross counter wraparound. -/// Java's awaitPhase example uses the same loop over observed advances. This observer does not -/// register, drive the computation, or guarantee one notification for every intermediate phase. +/// This observer does not register, drive the computation, or guarantee one notification for +/// every intermediate phase. async fn wait_until(phaser: &Phaser, target: u64) -> Result { let mut observed = phaser.phase(); while observed < target { @@ -116,8 +114,8 @@ async fn wait_until(phaser: &Phaser, target: u64) -> Result { async fn cancellation_retry() -> Result<(), Closed> { let phaser = Phaser::new(); - let mut participant = phaser.register()?; - let mut peer = phaser.register()?; + let mut participant = phaser.register_one()?; + let mut peer = phaser.register_one()?; let observed = phaser.phase(); tokio::select! { diff --git a/tests-integration/tests/phaser_test.rs b/tests-integration/tests/phaser_test.rs index eea7e8ac..5391891f 100644 --- a/tests-integration/tests/phaser_test.rs +++ b/tests-integration/tests/phaser_test.rs @@ -20,8 +20,8 @@ use asyncband::phaser::Phaser; #[tokio::test] async fn participant_can_wait_from_a_spawned_task() { let phaser = Phaser::new(); - let mut first = phaser.register().unwrap(); - let mut second = phaser.register().unwrap(); + let mut first = phaser.register_one().unwrap(); + let mut second = phaser.register_one().unwrap(); let first_wait = tokio::spawn(async move { first.wait().await }); @@ -35,8 +35,8 @@ async fn participant_can_wait_from_a_spawned_task() { async fn observer_waits_without_becoming_a_party() { let phaser = Phaser::new(); let observed = phaser.phase(); - let mut first = phaser.register().unwrap(); - let second = phaser.register().unwrap(); + let mut first = phaser.register_one().unwrap(); + let second = phaser.register_one().unwrap(); let observer_phaser = phaser.clone(); let observer = tokio::spawn(async move { observer_phaser.wait_for_advance(observed).await }); @@ -56,9 +56,9 @@ fn arrivals_publish_each_workers_writes_across_threads() { let phaser = Phaser::new(); let values = std::array::from_fn::<_, 4, _>(|_| AtomicUsize::new(0)); - let participants = phaser.register_many(4).unwrap(); + let participants = phaser.register(values.len()).unwrap(); std::thread::scope(|scope| { - for (id, mut participant) in participants.into_iter().enumerate() { + for (id, mut participant) in participants.enumerate() { let values = &values; scope.spawn(move || { pollster::block_on(async { @@ -84,8 +84,8 @@ async fn a_failed_task_can_close_the_group_without_reporting_phase_completion() use asyncband::phaser::Closed; let phaser = Phaser::new(); - let mut worker = phaser.register().unwrap(); - let failing = phaser.register().unwrap(); + let mut worker = phaser.register_one().unwrap(); + let failing = phaser.register_one().unwrap(); let observed = phaser.phase(); let (arrived, arrival) = tokio::sync::oneshot::channel(); let task = tokio::spawn(async move { diff --git a/tests-integration/tests/traits_test.rs b/tests-integration/tests/traits_test.rs index a9b2efbd..2ddcad88 100644 --- a/tests-integration/tests/traits_test.rs +++ b/tests-integration/tests/traits_test.rs @@ -37,6 +37,7 @@ use asyncband::oneshot; use asyncband::phaser::Closed; use asyncband::phaser::Phaser; use asyncband::phaser::PhaserParticipant; +use asyncband::phaser::PhaserParticipants; use asyncband::pool; use asyncband::pool::ManageObject; use asyncband::pool::ObjectStatus; @@ -109,6 +110,7 @@ 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::>(); @@ -181,6 +183,7 @@ fn public_types_are_unpin() { assert_unpin::(); assert_unpin::(); assert_unpin::(); + assert_unpin::(); assert_unpin::>(); assert_unpin::>(); assert_unpin::>(); From 3080a89a4351caeeb6639801c5774395a652d5dc Mon Sep 17 00:00:00 2001 From: tison Date: Fri, 11 Sep 2026 11:43:49 +0800 Subject: [PATCH 09/12] refactor(phaser): restrict construction of Closed errors --- asyncband/src/phaser/mod.rs | 18 +++++++------- asyncband/src/phaser/tests.rs | 33 +++++++++++++------------- examples/src/phaser_completion.rs | 6 ++--- examples/src/phaser_groups.rs | 23 +++++++++++------- tests-integration/tests/phaser_test.rs | 4 +--- 5 files changed, 45 insertions(+), 39 deletions(-) diff --git a/asyncband/src/phaser/mod.rs b/asyncband/src/phaser/mod.rs index ae9d509e..02d25ef3 100644 --- a/asyncband/src/phaser/mod.rs +++ b/asyncband/src/phaser/mod.rs @@ -107,12 +107,14 @@ use crate::internal::wakerset::WakerToken; mod tests; /// The phaser was closed before this operation could complete. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct Closed; +/// +/// This error is returned by phaser operations and cannot be constructed directly by callers. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Closed(()); impl fmt::Display for Closed { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("phaser is closed") + f.write_str("Phaser is closed") } } @@ -150,7 +152,7 @@ impl State { if self.phase != observed { Poll::Ready(Ok(self.phase)) } else if self.closed { - Poll::Ready(Err(Closed)) + Poll::Ready(Err(Closed(()))) } else { Poll::Pending } @@ -285,7 +287,7 @@ impl Phaser { fn do_register(&self, parties: usize) -> Result<(), Closed> { let mut state = self.state.lock(); if state.closed { - return Err(Closed); + return Err(Closed(())); } let registered = state .registered @@ -380,7 +382,7 @@ impl Drop for PhaserParticipants { /// /// This handle is not cloneable. Dropping it withdraws the participant, including any outstanding /// current arrival. It does not report successful work or close the other participants. -#[must_use = "dropping a participant withdraws it from the phaser"] +#[must_use = "dropping a participant withdraws it from the Phaser"] #[derive(Debug)] pub struct PhaserParticipant { phaser: Phaser, @@ -415,7 +417,7 @@ impl PhaserParticipant { let (phase, wakers) = { let mut state = self.phaser.state.lock(); if state.closed { - return Err(Closed); + return Err(Closed(())); } let phase = state.phase; if self.arrived != Some(phase) { @@ -468,7 +470,7 @@ impl PhaserParticipant { state.unarrived -= 1; } let result = if state.closed { - Err(Closed) + Err(Closed(())) } else { Ok(state.phase) }; diff --git a/asyncband/src/phaser/tests.rs b/asyncband/src/phaser/tests.rs index 774f307e..a7698a0b 100644 --- a/asyncband/src/phaser/tests.rs +++ b/asyncband/src/phaser/tests.rs @@ -25,7 +25,6 @@ use std::task::Poll; use std::task::Wake; use std::task::Waker; -use super::Closed; use super::Phaser; use crate::test_support::poll_once; @@ -129,7 +128,7 @@ fn an_existing_batch_can_be_iterated_and_withdrawn_after_close() { drop(participants); assert_eq!(phaser.registered_parties(), 1); assert_eq!(phaser.unarrived_parties(), 1); - assert_eq!(participant.arrive(), Err(Closed)); + assert!(participant.arrive().is_err()); drop(participant); assert_eq!(phaser.phase(), observed); assert_eq!(phaser.registered_parties(), 0); @@ -563,14 +562,14 @@ fn closing_wakes_all_waiters_once_and_rejects_new_obligations() { phaser.close(); assert!(phaser.is_closed()); assert_eq!(counter.0.load(Ordering::Relaxed), 2); - assert_eq!(poll_once(observer.as_mut()), Poll::Ready(Err(Closed))); - assert_eq!(poll_once(wait.as_mut()), Poll::Ready(Err(Closed))); + assert!(matches!(poll_once(observer.as_mut()), Poll::Ready(Err(_)))); + assert!(matches!(poll_once(wait.as_mut()), Poll::Ready(Err(_)))); drop(wait); - assert_eq!(first.arrive(), Err(Closed)); - assert!(matches!(phaser.register_one(), Err(Closed))); - assert!(matches!(phaser.register(2), Err(Closed))); - assert!(matches!(phaser.register(0), Err(Closed))); - assert_eq!(first.deregister(), Err(Closed)); + assert!(first.arrive().is_err()); + assert!(phaser.register_one().is_err()); + assert!(phaser.register(2).is_err()); + assert!(phaser.register(0).is_err()); + assert!(first.deregister().is_err()); drop(second); assert_eq!(phaser.registered_parties(), 0); assert_eq!(phaser.unarrived_parties(), 0); @@ -594,10 +593,10 @@ fn completed_arrival_remains_successful_after_close_but_cannot_start_another_rou poll_once(Box::pin(first.wait()).as_mut()), Poll::Ready(Ok(completed)) ); - assert_eq!( + assert!(matches!( poll_once(Box::pin(first.wait()).as_mut()), - Poll::Ready(Err(Closed)) - ); + Poll::Ready(Err(_)) + )); drop(first); drop(second); assert_eq!(phaser.phase(), completed); @@ -628,8 +627,8 @@ fn close_survives_a_panicking_waker_and_notifies_other_waiters() { assert!(panic::catch_unwind(|| phaser.close()).is_err()); assert!(phaser.is_closed()); assert_eq!(counter.0.load(Ordering::Relaxed), 1); - assert_eq!(poll_once(first.as_mut()), Poll::Ready(Err(Closed))); - assert_eq!(poll_once(second.as_mut()), Poll::Ready(Err(Closed))); + assert!(matches!(poll_once(first.as_mut()), Poll::Ready(Err(_)))); + assert!(matches!(poll_once(second.as_mut()), Poll::Ready(Err(_)))); } #[test] @@ -670,9 +669,9 @@ fn closing_during_waker_clone_does_not_register_after_close() { // SAFETY: The vtable maintains Arc ownership and every callback is thread-safe. let waker = unsafe { Waker::from_raw(RawWaker::new(data, &VTABLE)) }; let mut wait = Box::pin(phaser.wait_for_advance(phaser.phase())); - assert_eq!( + assert!(matches!( wait.as_mut().poll(&mut Context::from_waker(&waker)), - Poll::Ready(Err(Closed)) - ); + Poll::Ready(Err(_)) + )); assert!(phaser.is_closed()); } diff --git a/examples/src/phaser_completion.rs b/examples/src/phaser_completion.rs index 0ac583fd..527d1b35 100644 --- a/examples/src/phaser_completion.rs +++ b/examples/src/phaser_completion.rs @@ -122,7 +122,7 @@ async fn finalize_until_converged() -> Result<(), Closed> { coordinator.resume.wait().await?; } for task in tasks { - assert_eq!(task.await.expect("worker panicked"), Err(Closed)); + assert!(task.await.expect("worker panicked").is_err()); } assert_eq!(published.load(Ordering::Relaxed), 18); println!("convergence: all workers stopped after the third aggregate"); @@ -146,7 +146,7 @@ async fn failure_closes_the_group() -> Result<(), Closed> { let healthy = Member::register(&ready, &resume)?; let failing = Member::register(&ready, &resume)?; let (peer, failure) = tokio::join!(wait_once(healthy), fail(failing)); - assert_eq!(peer, Err(Closed)); + assert!(peer.is_err()); assert_eq!(failure, Err("input validation failed")); assert_eq!(ready.phase(), 0); assert_eq!(resume.phase(), 0); @@ -160,7 +160,7 @@ async fn cancelling_an_unpolled_task_closes_the_group() -> Result<(), Closed> { let peer = Member::register(&ready, &resume)?; let cancelled = wait_once(Member::register(&ready, &resume)?); drop(cancelled); - assert_eq!(wait_once(peer).await, Err(Closed)); + assert!(wait_once(peer).await.is_err()); assert_eq!(ready.phase(), 0); println!("cancellation: dropping an unpolled task closed both gates"); Ok(()) diff --git a/examples/src/phaser_groups.rs b/examples/src/phaser_groups.rs index f8c0a0c7..4ea6986e 100644 --- a/examples/src/phaser_groups.rs +++ b/examples/src/phaser_groups.rs @@ -26,6 +26,7 @@ //! //! Run: cargo run -p examples --example phaser_groups +use std::error::Error; use std::sync::Arc; use std::sync::atomic::AtomicU64; use std::sync::atomic::Ordering; @@ -78,11 +79,11 @@ async fn worker( id: usize, values: Arc>, fail: bool, -) -> Result<(), Closed> { +) -> Result<(), Box> { for round in 1..=ROUNDS { if fail && round == 2 { // The abort guard closes the root before any participant is withdrawn. - return Err(Closed); + return Err("input validation failed".into()); } values[id].store(round, Ordering::Relaxed); member.ready.wait().await?; @@ -104,7 +105,7 @@ struct GroupDriver { root: PhaserParticipant, } -async fn drive_group(mut driver: GroupDriver) -> Result<(), Closed> { +async fn drive_group(mut driver: GroupDriver) -> Result<(), Box> { for _ in 0..ROUNDS { driver.local.ready.wait().await?; driver.root.wait().await?; @@ -123,14 +124,14 @@ impl Drop for CloseRootOnDrop { } #[tokio::main(flavor = "current_thread")] -async fn main() -> Result<(), Closed> { +async fn main() -> Result<(), Box> { run_groups(false).await?; - assert_eq!(run_groups(true).await, Err(Closed)); + assert!(run_groups(true).await.unwrap_err().is::()); println!("group failure: root closure propagated to every local group"); Ok(()) } -async fn run_groups(fail_one_worker: bool) -> Result<(), Closed> { +async fn run_groups(fail_one_worker: bool) -> Result<(), Box> { let root = Phaser::new(); let mut coordinator = root.register_one()?; // Created after the participant so cancellation closes the root before withdrawing it. @@ -167,11 +168,17 @@ async fn run_groups(fail_one_worker: bool) -> Result<(), Closed> { if let Err(error) = coordinator.wait().await { // Root closure propagates through the group drivers to their local waiters. root.close(); + let mut failures = 0; for task in tasks { - let _ = task.await.expect("group task panicked"); + let error = task.await.expect("group task panicked").unwrap_err(); + if !error.is::() { + assert_eq!(error.to_string(), "input validation failed"); + failures += 1; + } } + assert_eq!(failures, 1); assert_eq!(root.phase(), 1); - return Err(error); + return Err(error.into()); } println!("root: all {GROUPS} groups completed round {round}"); } diff --git a/tests-integration/tests/phaser_test.rs b/tests-integration/tests/phaser_test.rs index 5391891f..3afb6f3d 100644 --- a/tests-integration/tests/phaser_test.rs +++ b/tests-integration/tests/phaser_test.rs @@ -81,8 +81,6 @@ fn arrivals_publish_each_workers_writes_across_threads() { #[tokio::test] async fn a_failed_task_can_close_the_group_without_reporting_phase_completion() { - use asyncband::phaser::Closed; - let phaser = Phaser::new(); let mut worker = phaser.register_one().unwrap(); let failing = phaser.register_one().unwrap(); @@ -96,7 +94,7 @@ async fn a_failed_task_can_close_the_group_without_reporting_phase_completion() arrival.await.unwrap(); failing.phaser().close(); drop(failing); - assert_eq!(task.await.unwrap(), Err(Closed)); + assert!(task.await.unwrap().is_err()); assert_eq!(phaser.phase(), observed); assert_eq!(phaser.registered_parties(), 0); } From 197d450793fe9dbde1a2bddd7316fbc7df01ebcb Mon Sep 17 00:00:00 2001 From: tison Date: Fri, 11 Sep 2026 12:05:52 +0800 Subject: [PATCH 10/12] fixup Signed-off-by: tison --- CHANGELOG.md | 2 +- asyncband/src/mutex/mod.rs | 6 +- asyncband/src/phaser/mod.rs | 108 +++++++++++++++---------- asyncband/src/phaser/tests.rs | 28 +++---- examples/src/phaser_completion.rs | 23 +++--- examples/src/phaser_groups.rs | 24 +++--- examples/src/phaser_rounds.rs | 15 ++-- tests-integration/tests/phaser_test.rs | 2 +- 8 files changed, 121 insertions(+), 87 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c1b21ae..0fc85334 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ All notable changes to this project will be documented in this file. ### New features -* Add an opt-in runtime-agnostic `Phaser` with shared observer handles, dynamic RAII participants registered individually or in batches through an owning iterator, `u64` phase numbers, split arrival/wait with cancellation-resilient retries, and explicit closure that releases unfinished waits with `Closed`. +* Add an opt-in runtime-agnostic `Phaser` with shared observer handles, dynamic RAII participants registered individually or in batches through an owning iterator, `u64` phase numbers, split arrival/wait with cancellation-resilient retries, and a `close` operation that releases unfinished waits with `Closed`. * Add bounded MPSC `reserve` and `try_reserve` methods returning a `Permit`, allowing callers to wait for capacity before constructing a message; pending sends and reservations receive capacity in wait-queue order, and unused permits release capacity without claiming message order. ### Bug fixes diff --git a/asyncband/src/mutex/mod.rs b/asyncband/src/mutex/mod.rs index 97772eea..ed049641 100644 --- a/asyncband/src/mutex/mod.rs +++ b/asyncband/src/mutex/mod.rs @@ -19,9 +19,9 @@ // Copyright (c) Tokio Contributors // The Tokio-derived portions remain licensed under the MIT License. // Asyncband independently built the mutex on its own semaphore and substantially changed the -// incorporated guard implementation: the try-lock error type and semaphore closure are absent, -// projected guards use NonNull pointers with explicit invariance, and projected guards can be -// mapped repeatedly in both borrowed and owned forms. +// incorporated guard implementation: the try-lock error type and support for closing the semaphore +// are absent, projected guards use NonNull pointers with explicit invariance, and projected guards +// can be mapped repeatedly in both borrowed and owned forms. // Upstream source: // https://github.com/tokio-rs/tokio/blob/01e04daaa162ce6122bb894fdda0b6803dd32093/tokio/src/sync/mutex.rs diff --git a/asyncband/src/phaser/mod.rs b/asyncband/src/phaser/mod.rs index 02d25ef3..3f9f821a 100644 --- a/asyncband/src/phaser/mod.rs +++ b/asyncband/src/phaser/mod.rs @@ -22,41 +22,73 @@ //! Register participants before starting their tasks, or keep a coordinator participant registered //! while setting up a group so that the first workers cannot finish the phase prematurely. //! -//! # Arriving and waiting +//! # Example: build a shared dictionary before encoding documents //! -//! [`PhaserParticipant::wait`] arrives and waits for the other participants. To overlap independent -//! work with that wait, call [`arrive`](PhaserParticipant::arrive) first. The subsequent `wait` -//! observes that arrival's phase even if it has already completed. Explicitly arriving again -//! replaces the pending observation with the current phase; repeated arrivals within one phase -//! do not count twice. +//! An indexing job needs the same numeric ID for a word in every document. Workers first collect +//! vocabulary in parallel. Once all documents have contributed their words, the coordinator assigns +//! IDs in sorted order. A second rendezvous keeps workers from encoding documents before that +//! shared dictionary is ready. The same participants coordinate both steps. //! //! ``` +//! use std::collections::BTreeMap; +//! use std::sync::Arc; +//! use std::sync::Mutex; +//! +//! use asyncband::phaser::Closed; //! use asyncband::phaser::Phaser; //! //! # #[tokio::main(flavor = "current_thread")] -//! # async fn main() -> Result<(), asyncband::phaser::Closed> { +//! # async fn main() -> Result<(), Closed> { +//! let documents = ["rust async rust", "async tasks"]; +//! let dictionary = Arc::new(Mutex::new(BTreeMap::new())); //! let phaser = Phaser::new(); -//! let mut worker = phaser.register_one()?; //! let mut coordinator = phaser.register_one()?; +//! let participants = phaser.register(documents.len())?; +//! let mut tasks = Vec::new(); +//! +//! for (document, mut participant) in documents.into_iter().zip(participants) { +//! let dictionary = dictionary.clone(); +//! tasks.push(tokio::spawn(async move { +//! let words: Vec<_> = document.split_whitespace().collect(); +//! { +//! let mut dictionary = dictionary.lock().unwrap(); +//! for &word in &words { +//! dictionary.entry(word).or_insert(0); +//! } +//! } +//! participant.wait().await?; // All vocabulary has been collected. +//! participant.wait().await?; // The coordinator has assigned the IDs. //! -//! let task = tokio::spawn(async move { -//! for _ in 0..3 { -//! // Finish this round's work before arriving. -//! let completed = worker.arrive()?; -//! // Independent work can run here without delaying the other participants. -//! assert_ne!(worker.wait().await?, completed); -//! } -//! Ok::<_, asyncband::phaser::Closed>(()) -//! }); +//! let dictionary = dictionary.lock().unwrap(); +//! let encoded: Vec<_> = words.iter().map(|word| dictionary[word]).collect(); +//! Ok::<_, Closed>(encoded) +//! })); +//! } +//! +//! coordinator.wait().await?; +//! for (id, value) in dictionary.lock().unwrap().values_mut().enumerate() { +//! *value = id; +//! } +//! coordinator.wait().await?; //! -//! for _ in 0..3 { -//! coordinator.wait().await?; +//! let mut encoded_documents = Vec::new(); +//! for task in tasks { +//! encoded_documents.push(task.await.unwrap()?); //! } -//! task.await.unwrap()?; +//! // Every document uses the same dictionary: async = 0, rust = 1, tasks = 2. +//! assert_eq!(encoded_documents, [vec![1, 0, 1], vec![0, 2]]); //! # Ok(()) //! # } //! ``` //! +//! # Arriving and waiting +//! +//! [`PhaserParticipant::wait`] arrives and waits for the other participants. To overlap independent +//! work with that wait, call [`arrive`](PhaserParticipant::arrive) first. The subsequent `wait` +//! observes that arrival's phase even if it has already completed. Explicitly arriving again +//! replaces the pending observation with the current phase; repeated arrivals within one phase +//! do not count twice. +//! //! # Membership and cancellation //! //! Registration joins the phase current at the registration's synchronization point. In @@ -71,11 +103,11 @@ //! participant itself withdraws it from the group. Withdrawal does not certify successful work; //! applications that require all workers to succeed should close the phaser on failure. //! -//! [`Phaser::wait_for_advance`] is an independent, cancel-safe observation. It never registers a +//! [`Phaser::wait`] is an independent, cancel-safe observation. It never registers a //! participant or records an arrival. Observers may miss intermediate phases; this is not an //! event stream with one notification per phase. //! -//! # Closure and synchronization +//! # Closing and synchronization //! //! [`Phaser::close`] permanently freezes the current phase, rejects registration and arrival, //! and releases waits for the unfinished phase with [`Closed`]. A previously completed phase @@ -86,7 +118,7 @@ //! provided by a wait that returns `Closed`. //! //! Phase numbers start at zero and wrap from `u64::MAX` to zero. Pass a value previously obtained -//! from the same phaser to `wait_for_advance`; it tests for a different phase, not a target number +//! from the same phaser to `wait`; it tests for a different phase, not a target number //! or numeric threshold. An observation must not be retained across a full counter cycle. use std::fmt; @@ -191,7 +223,7 @@ impl Phaser { } } - /// Returns the current phase number, which remains fixed after closure. + /// Returns the current phase number, which remains fixed once the phaser is closed. pub fn phase(&self) -> u64 { self.state.lock().phase } @@ -203,12 +235,12 @@ impl Phaser { /// Closes this phaser and wakes all pending observers without completing the current phase. /// - /// Closure is idempotent and affects every handle and participant. Existing participants may - /// still deregister or be dropped; their removal no longer advances the phase. + /// This operation is idempotent and affects every handle and participant. Existing participants + /// may still deregister or be dropped; their removal no longer advances the phase. /// /// # Panics /// - /// If a waker panics, closure remains committed and notification is attempted for the other + /// If a waker panics, the phaser remains closed and notification is attempted for the other /// waiters before the panic resumes. pub fn close(&self) { let wakers = { @@ -264,14 +296,6 @@ impl 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. /// - /// ``` - /// use asyncband::phaser::Phaser; - /// - /// let phaser = Phaser::new(); - /// let participants: Vec<_> = phaser.register(3)?.collect(); - /// # Ok::<(), asyncband::phaser::Closed>(()) - /// ``` - /// /// # Panics /// /// Panics if the registered count would exceed `usize::MAX`. @@ -309,13 +333,13 @@ impl Phaser { /// It neither registers a participant nor records an arrival. /// /// Returns [`Closed`] if the observed phase is still current when the phaser closes. A phase - /// completed before closure 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 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. /// /// # Cancel safety /// /// Cancelling only unregisters this wait's waker. The same observation can be retried. - pub async fn wait_for_advance(&self, observed: u64) -> Result { + pub async fn wait(&self, observed: u64) -> Result { PhaserWait { phaser: self, observed, @@ -331,8 +355,8 @@ impl Phaser { /// advancement. Dropping this iterator withdraws the remaining participants in one state /// transition. Yielded participants are independent and retain their registrations. /// -/// Closure does not prevent iteration over this existing batch, but arrival and waiting on the -/// yielded participants return [`Closed`]. This iterator is not cloneable. +/// Closing the phaser does not prevent iteration over this existing batch, but arrival and waiting +/// on the yielded participants return [`Closed`]. This iterator is not cloneable. #[must_use = "dropping the iterator withdraws participants that have not been yielded"] #[derive(Debug)] pub struct PhaserParticipants { @@ -447,7 +471,7 @@ impl PhaserParticipant { Some(phase) => phase, None => self.arrive()?, }; - let next = self.phaser.wait_for_advance(observed).await?; + let next = self.phaser.wait(observed).await?; self.pending = None; Ok(next) } @@ -455,7 +479,7 @@ impl PhaserParticipant { /// Withdraws this participant and returns the phase from which it withdrew, or [`Closed`]. /// /// Any outstanding arrival is discharged without counting an already-arrived participant - /// twice. This always removes the registration, including after closure. The pending + /// 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. pub fn deregister(mut self) -> Result { self.do_deregister() diff --git a/asyncband/src/phaser/tests.rs b/asyncband/src/phaser/tests.rs index a7698a0b..0dc8b66b 100644 --- a/asyncband/src/phaser/tests.rs +++ b/asyncband/src/phaser/tests.rs @@ -321,7 +321,7 @@ fn registration_after_last_participant_drop_joins_the_advanced_phase() { } #[test] -fn wait_for_advance_is_a_cancel_safe_non_participant_observer() { +fn observer_wait_is_cancel_safe_and_does_not_participate() { let phaser = Phaser::new(); let phase = phaser.phase(); let counter = Arc::new(CountWake(AtomicUsize::new(0))); @@ -329,7 +329,7 @@ fn wait_for_advance_is_a_cancel_safe_non_participant_observer() { let mut context = Context::from_waker(&waker); { - let mut wait = Box::pin(phaser.wait_for_advance(phase)); + let mut wait = Box::pin(phaser.wait(phase)); assert_eq!(Future::poll(wait.as_mut(), &mut context), Poll::Pending); assert_eq!(phaser.registered_parties(), 0); } @@ -351,8 +351,8 @@ fn advancing_a_phase_wakes_every_registered_waiter_once() { 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)); + let mut first_wait = Box::pin(phaser.wait(observed)); + let mut second_wait = Box::pin(phaser.wait(observed)); assert_eq!( Future::poll(first_wait.as_mut(), &mut first_context), @@ -384,7 +384,7 @@ fn cancelling_a_woken_waiter_does_not_unregister_a_next_phase_waiter() { 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)); + let mut stale_wait = Box::pin(phaser.wait(phase0)); assert_eq!( Future::poll(stale_wait.as_mut(), &mut stale_context), @@ -399,7 +399,7 @@ fn cancelling_a_woken_waiter_does_not_unregister_a_next_phase_waiter() { 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)); + let mut current_wait = Box::pin(phaser.wait(phase1)); assert_eq!( Future::poll(current_wait.as_mut(), &mut current_context), Poll::Pending @@ -422,7 +422,7 @@ fn panicking_waker_does_not_lose_a_pending_phase() { let mut second = phaser.register_one().unwrap(); 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)); + let mut observer = Box::pin(phaser.wait(phase0)); assert_eq!( Future::poll(observer.as_mut(), &mut panic_context), @@ -457,7 +457,7 @@ fn a_late_waiter_for_a_completed_phase_is_immediately_ready() { let participant = phaser.register_one().unwrap(); drop(participant); - let mut wait = Box::pin(phaser.wait_for_advance(observed)); + let mut wait = Box::pin(phaser.wait(observed)); assert_eq!(poll_once(wait.as_mut()), Poll::Ready(Ok(phaser.phase()))); } @@ -539,7 +539,7 @@ fn cloned_handles_observe_without_registering_and_participants_own_the_state() { let observed = observer.phase(); participant.arrive().unwrap(); assert_eq!( - poll_once(Box::pin(observer.wait_for_advance(observed)).as_mut()), + poll_once(Box::pin(observer.wait(observed)).as_mut()), Poll::Ready(Ok(observer.phase())) ); } @@ -553,7 +553,7 @@ fn closing_wakes_all_waiters_once_and_rejects_new_obligations() { let counter = Arc::new(CountWake(AtomicUsize::new(0))); let waker = Waker::from(counter.clone()); let mut context = Context::from_waker(&waker); - let mut observer = Box::pin(phaser.wait_for_advance(observed)); + let mut observer = Box::pin(phaser.wait(observed)); let mut wait = Box::pin(first.wait()); assert!(observer.as_mut().poll(&mut context).is_pending()); assert!(wait.as_mut().poll(&mut context).is_pending()); @@ -582,7 +582,7 @@ fn completed_arrival_remains_successful_after_close_but_cannot_start_another_rou let mut first = phaser.register_one().unwrap(); let mut second = phaser.register_one().unwrap(); let observed = first.arrive().unwrap(); - let mut observer = Box::pin(phaser.wait_for_advance(observed)); + let mut observer = Box::pin(phaser.wait(observed)); assert!(poll_once(observer.as_mut()).is_pending()); second.arrive().unwrap(); let completed = phaser.phase(); @@ -609,8 +609,8 @@ fn close_survives_a_panicking_waker_and_notifies_other_waiters() { let panic_waker = Waker::from(Arc::new(PanicWake)); let counter = Arc::new(CountWake(AtomicUsize::new(0))); let count_waker = Waker::from(counter.clone()); - let mut first = Box::pin(phaser.wait_for_advance(observed)); - let mut second = Box::pin(phaser.wait_for_advance(observed)); + let mut first = Box::pin(phaser.wait(observed)); + let mut second = Box::pin(phaser.wait(observed)); assert!( first .as_mut() @@ -668,7 +668,7 @@ fn closing_during_waker_clone_does_not_register_after_close() { let data = Arc::into_raw(Arc::new(phaser.clone())).cast(); // SAFETY: The vtable maintains Arc ownership and every callback is thread-safe. let waker = unsafe { Waker::from_raw(RawWaker::new(data, &VTABLE)) }; - let mut wait = Box::pin(phaser.wait_for_advance(phaser.phase())); + let mut wait = Box::pin(phaser.wait(phaser.phase())); assert!(matches!( wait.as_mut().poll(&mut Context::from_waker(&waker)), Poll::Ready(Err(_)) diff --git a/examples/src/phaser_completion.rs b/examples/src/phaser_completion.rs index 527d1b35..cef5d205 100644 --- a/examples/src/phaser_completion.rs +++ b/examples/src/phaser_completion.rs @@ -15,16 +15,19 @@ // specific language governing permissions and limitations // under the License. -//! Finalize each round before releasing workers, and close the group on failure or cancellation. +//! Publish consistent progress snapshots from parallel import workers. //! -//! An application coordinator aggregates results and checks convergence between two rendezvous -//! points. The coordinator may await I/O. Merely running code after one wait, even in a barrier -//! leader, would not stop other workers from starting their next round. +//! Each worker contributes its cumulative record count after a batch. The coordinator sums those +//! counts and publishes a snapshot before workers process the next batch. Separate ready/resume +//! phasers prevent a fast worker from updating its count while the snapshot is being prepared. +//! The coordinator can also await an asynchronous checkpoint before releasing the workers. //! -//! Membership is fixed within this protocol; changes must update both groups at a common round -//! boundary. Each phaser has its own counter, distinct from the application's iteration number. +//! The first scenario stops once the total reaches a target. The other two show how a failed or +//! cancelled worker stops its peers, including a task cancelled before its first poll. Import work +//! is represented by counters, and checkpoint I/O by a yield. //! -//! Run: cargo run -p examples --example phaser_completion +//! Membership is fixed within this protocol; changes must update both groups at a common batch +//! boundary. Each phaser has its own counter, distinct from the application's batch number. use std::sync::Arc; use std::sync::atomic::AtomicU64; @@ -64,7 +67,7 @@ impl Member { #[tokio::main(flavor = "current_thread")] async fn main() -> Result<(), Closed> { - finalize_until_converged().await?; + publish_until_target().await?; failure_closes_the_group().await?; cancelling_an_unpolled_task_closes_the_group().await?; Ok(()) @@ -86,7 +89,7 @@ async fn compute( unreachable!() } -async fn finalize_until_converged() -> Result<(), Closed> { +async fn publish_until_target() -> Result<(), Closed> { let ready = Phaser::new(); let resume = Phaser::new(); let mut coordinator = Member::register(&ready, &resume)?; @@ -125,7 +128,7 @@ async fn finalize_until_converged() -> Result<(), Closed> { assert!(task.await.expect("worker panicked").is_err()); } assert_eq!(published.load(Ordering::Relaxed), 18); - println!("convergence: all workers stopped after the third aggregate"); + println!("target reached: all workers stopped after 18 imported records"); Ok(()) } diff --git a/examples/src/phaser_groups.rs b/examples/src/phaser_groups.rs index 4ea6986e..9db8c35f 100644 --- a/examples/src/phaser_groups.rs +++ b/examples/src/phaser_groups.rs @@ -15,16 +15,20 @@ // specific language governing permissions and limitations // under the License. -//! Group local arrivals before one global rendezvous, then release the local workers. +//! Synchronize the time steps of a simulation whose workers are grouped by region. //! -//! A group representative contributes one root participant. Each group runs an explicit driver -//! task and uses separate local counters. A local ready phase must never authorize the next round -//! until the root has also completed. -//! The example uses a fixed cohort for three rounds; automatic parent registration and arbitrary -//! concurrent changes to a hierarchical participant set are not provided by this composition. -//! No performance advantage over a flat Phaser is claimed without workload-specific measurement. +//! This example models progress tracking for two regions with two workers each. Workers publish +//! their completed time step, then wait for their region's driver. Each driver represents its +//! region at a root phaser and releases local workers only after every region is ready. No region +//! can start the next step while another is still processing the current one. //! -//! Run: cargo run -p examples --example phaser_groups +//! The first run completes three steps. The second fails a worker during step two and verifies +//! that all groups stop with only step one completed globally. The simulation's domain calculation +//! is omitted; the shared counters record each worker's completed step. +//! +//! The groups and drivers are explicit application code, with independent local phase counters +//! and fixed membership. This does not provide automatic parent registration or arbitrary +//! concurrent membership changes. Performance relative to a flat phaser depends on the workload. use std::error::Error; use std::sync::Arc; @@ -127,7 +131,7 @@ impl Drop for CloseRootOnDrop { async fn main() -> Result<(), Box> { run_groups(false).await?; assert!(run_groups(true).await.unwrap_err().is::()); - println!("group failure: root closure propagated to every local group"); + println!("group failure: every local group stopped after the root was closed"); Ok(()) } @@ -166,7 +170,7 @@ async fn run_groups(fail_one_worker: bool) -> Result<(), Box Result<(), Closed> { let progress = phaser.clone(); let observer = tokio::spawn(async move { let mut observed = progress.phase(); - while let Ok(next) = progress.wait_for_advance(observed).await { + while let Ok(next) = progress.wait(observed).await { println!("observer: phase {observed} -> {next}"); // A slow observer may skip phases; it never delays workers. observed = next; @@ -107,7 +110,7 @@ async fn changing_membership() -> Result<(), Closed> { async fn wait_until(phaser: &Phaser, target: u64) -> Result { let mut observed = phaser.phase(); while observed < target { - observed = phaser.wait_for_advance(observed).await?; + observed = phaser.wait(observed).await?; } Ok(observed) } diff --git a/tests-integration/tests/phaser_test.rs b/tests-integration/tests/phaser_test.rs index 3afb6f3d..1b827b63 100644 --- a/tests-integration/tests/phaser_test.rs +++ b/tests-integration/tests/phaser_test.rs @@ -38,7 +38,7 @@ async fn observer_waits_without_becoming_a_party() { let mut first = phaser.register_one().unwrap(); let second = phaser.register_one().unwrap(); let observer_phaser = phaser.clone(); - let observer = tokio::spawn(async move { observer_phaser.wait_for_advance(observed).await }); + let observer = tokio::spawn(async move { observer_phaser.wait(observed).await }); tokio::task::yield_now().await; assert_eq!(phaser.registered_parties(), 2); From 9df53664983d1a99f8b23a9d3f0da4cb3dadb1f2 Mon Sep 17 00:00:00 2001 From: tison Date: Fri, 11 Sep 2026 12:26:44 +0800 Subject: [PATCH 11/12] perf(phaser): simplify participant state and reuse task wakers --- asyncband/src/internal/wakerset.rs | 11 + asyncband/src/phaser/mod.rs | 26 +- asyncband/src/phaser/tests.rs | 631 ----------------------- benchmarks/Cargo.toml | 1 + benchmarks/asyncband/main.rs | 1 + benchmarks/asyncband/phaser/mod.rs | 119 +++++ tests-integration/tests/phaser_test.rs | 664 +++++++++++++++++++++++++ xtask/src/main.rs | 4 + 8 files changed, 816 insertions(+), 641 deletions(-) create mode 100644 benchmarks/asyncband/phaser/mod.rs diff --git a/asyncband/src/internal/wakerset.rs b/asyncband/src/internal/wakerset.rs index 7c9393bb..3d89107d 100644 --- a/asyncband/src/internal/wakerset.rs +++ b/asyncband/src/internal/wakerset.rs @@ -102,6 +102,17 @@ impl WakerSet { None } + /// Returns whether a live registration already wakes the given task, without cloning a waker. + /// + /// The caller must check completion before querying a potentially stale token. + #[inline] + pub fn will_wake(&self, token: &WakerToken, waker: &Waker) -> bool { + self.wakers + .get(token.0) + .expect("waker token must refer to an occupied slot") + .will_wake(waker) + } + /// Registers or replaces a waker cloned before taking the owner's state lock. /// /// Returns the previous waker so its destructor can run after releasing that lock. diff --git a/asyncband/src/phaser/mod.rs b/asyncband/src/phaser/mod.rs index 3f9f821a..b2994f17 100644 --- a/asyncband/src/phaser/mod.rs +++ b/asyncband/src/phaser/mod.rs @@ -156,6 +156,8 @@ impl std::error::Error for Closed {} /// /// Cloning this handle does not register a participant. Use [`register_one`](Self::register_one) to /// create a participant that can be moved into an independently spawned task. +/// +/// See the [module level documentation](self) for usage examples and synchronization semantics. #[derive(Clone)] pub struct Phaser { state: Arc>, @@ -317,12 +319,9 @@ impl Phaser { .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; + // unarrived <= registered, so the registered-count check also covers this addition. + state.unarrived += parties; Ok(()) } @@ -406,11 +405,13 @@ impl Drop for PhaserParticipants { /// /// This handle is not cloneable. Dropping it withdraws the participant, including any outstanding /// current arrival. It does not report successful work or close the other participants. +/// +/// See the [module level documentation](self) for usage examples and cancellation semantics. #[must_use = "dropping a participant withdraws it from the Phaser"] #[derive(Debug)] pub struct PhaserParticipant { phaser: Phaser, - arrived: Option, + // Cleared only after advancement, so a pending current phase also records arrival. pending: Option, registered: bool, } @@ -419,7 +420,6 @@ impl PhaserParticipant { fn new(phaser: Phaser) -> Self { Self { phaser, - arrived: None, pending: None, registered: true, } @@ -444,9 +444,8 @@ impl PhaserParticipant { return Err(Closed(())); } let phase = state.phase; - if self.arrived != Some(phase) { + if self.pending != Some(phase) { state.unarrived -= 1; - self.arrived = Some(phase); } self.pending = Some(phase); (phase, state.advance_if_ready()) @@ -490,7 +489,7 @@ impl PhaserParticipant { let mut state = self.phaser.state.lock(); self.registered = false; state.registered -= 1; - if self.arrived != Some(state.phase) { + if self.pending != Some(state.phase) { state.unarrived -= 1; } let result = if state.closed { @@ -531,6 +530,13 @@ impl Future for PhaserWait<'_> { this.token = None; return ready; } + if this + .token + .as_ref() + .is_some_and(|token| state.waiters.will_wake(token, cx.waker())) + { + return Poll::Pending; + } } // Waker cloning may reenter or close this phaser. Recheck completion before registering. diff --git a/asyncband/src/phaser/tests.rs b/asyncband/src/phaser/tests.rs index 0dc8b66b..48cffa89 100644 --- a/asyncband/src/phaser/tests.rs +++ b/asyncband/src/phaser/tests.rs @@ -15,452 +15,11 @@ // 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 super::Phaser; use crate::test_support::poll_once; -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 batch_registration_joins_one_observed_phase() { - let phaser = Phaser::new(); - let mut participants = phaser.register(3).unwrap(); - - assert_eq!(participants.len(), 3); - assert_eq!(phaser.registered_parties(), 3); - assert_eq!(phaser.unarrived_parties(), 3); - - let mut first = participants.next().unwrap(); - let observed = first.arrive().unwrap(); - let mut second = participants.next().unwrap(); - second.arrive().unwrap(); - assert_eq!(participants.len(), 1); - assert_eq!(phaser.phase(), observed); - - let counter = Arc::new(CountWake(AtomicUsize::new(0))); - let waker = Waker::from(counter.clone()); - let mut wait = Box::pin(first.wait()); - assert!( - wait.as_mut() - .poll(&mut Context::from_waker(&waker)) - .is_pending() - ); - drop(participants); - assert_eq!(counter.0.load(Ordering::Relaxed), 1); - assert_eq!(poll_once(wait.as_mut()), Poll::Ready(Ok(phaser.phase()))); - assert_ne!(phaser.phase(), observed); - assert_eq!(phaser.registered_parties(), 2); - assert_eq!(phaser.unarrived_parties(), 2); -} - -#[test] -fn collecting_a_batch_can_unwind_without_leaking_registrations() { - let phaser = Phaser::new(); - let mut coordinator = phaser.register_one().unwrap(); - let observed = phaser.phase(); - - assert!( - panic::catch_unwind(|| { - let _: Vec<_> = phaser - .register(3) - .unwrap() - .enumerate() - .map(|(index, participant)| { - assert_ne!(index, 1, "task setup failed"); - participant - }) - .collect(); - }) - .is_err() - ); - assert_eq!(phaser.phase(), observed); - assert_eq!(phaser.registered_parties(), 1); - assert_eq!(phaser.unarrived_parties(), 1); - coordinator.arrive().unwrap(); - assert_ne!(phaser.phase(), observed); -} - -#[test] -fn exhausted_batch_does_not_advance_a_dormant_phaser() { - let phaser = Phaser::new(); - let mut participants = phaser.register(1).unwrap(); - drop(participants.next().unwrap()); - let completed = phaser.phase(); - - assert_eq!(participants.len(), 0); - assert!(participants.next().is_none()); - drop(participants); - assert_eq!(phaser.phase(), completed); - assert_eq!(phaser.registered_parties(), 0); -} - -#[test] -fn an_existing_batch_can_be_iterated_and_withdrawn_after_close() { - let phaser = Phaser::new(); - let mut participants = phaser.register(3).unwrap(); - let observed = phaser.phase(); - phaser.close(); - - let mut participant = participants.next().unwrap(); - drop(participants); - assert_eq!(phaser.registered_parties(), 1); - assert_eq!(phaser.unarrived_parties(), 1); - assert!(participant.arrive().is_err()); - drop(participant); - assert_eq!(phaser.phase(), observed); - assert_eq!(phaser.registered_parties(), 0); - assert_eq!(phaser.unarrived_parties(), 0); -} - -#[test] -fn registering_zero_parties_is_a_noop() { - let phaser = Phaser::new(); - let phase = phaser.phase(); - - assert_eq!(phaser.register(0).unwrap().len(), 0); - 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 = Phaser::new(); - let phase0 = phaser.phase(); - let mut first = phaser.register_one().unwrap(); - let mut second = phaser.register_one().unwrap(); - - assert_eq!(first.arrive().unwrap(), phase0); - assert_eq!(phaser.arrived_parties(), 1); - assert_eq!(second.arrive().unwrap(), phase0); - let phase1 = phaser.phase(); - assert_ne!(phase1, phase0); - assert_eq!(phaser.arrived_parties(), 0); - - assert_eq!(first.arrive().unwrap(), phase1); - assert_eq!(second.arrive().unwrap(), phase1); - assert_ne!(phaser.phase(), phase1); -} - -#[test] -fn unpolled_wait_future_does_not_arrive() { - let phaser = Phaser::new(); - let mut participant = phaser.register_one().unwrap(); - - let wait = participant.wait(); - - assert_eq!(phaser.arrived_parties(), 0); - drop(wait); - assert_eq!(phaser.arrived_parties(), 0); -} - -#[test] -fn cancelled_wait_retry_waits_for_original_phase_after_advance() { - let phaser = Phaser::new(); - let phase0 = phaser.phase(); - let mut first = phaser.register_one().unwrap(); - let mut second = phaser.register_one().unwrap(); - - { - let mut cancelled = Box::pin(first.wait()); - assert!(poll_once(cancelled.as_mut()).is_pending()); - } - - assert_eq!(phaser.arrived_parties(), 1); - assert_eq!(second.arrive().unwrap(), phase0); - let phase1 = phaser.phase(); - assert_ne!(phase1, phase0); - assert_eq!(phaser.arrived_parties(), 0); - - let mut retry = Box::pin(first.wait()); - assert_eq!(poll_once(retry.as_mut()), Poll::Ready(Ok(phase1))); - assert_eq!(phaser.arrived_parties(), 0); -} - -#[test] -fn cancelled_wait_retry_before_advance_does_not_arrive_twice() { - let phaser = Phaser::new(); - let mut first = phaser.register_one().unwrap(); - let mut second = phaser.register_one().unwrap(); - - { - let mut cancelled = Box::pin(first.wait()); - assert!(poll_once(cancelled.as_mut()).is_pending()); - } - - let mut retry = Box::pin(first.wait()); - assert!(poll_once(retry.as_mut()).is_pending()); - assert_eq!(phaser.arrived_parties(), 1); - - second.arrive().unwrap(); - assert!(poll_once(retry.as_mut()).is_ready()); -} - -#[test] -fn dropping_last_participant_advances_once_and_dormant_phaser_can_be_reused() { - let phaser = Phaser::new(); - let phase0 = phaser.phase(); - let participant = phaser.register_one().unwrap(); - - 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_one().unwrap(); - assert_eq!(participant.arrive().unwrap(), phase1); - assert_ne!(phaser.phase(), phase1); -} - -#[test] -fn dropping_an_arrived_participant_only_removes_its_next_phase_registration() { - let phaser = Phaser::new(); - let phase0 = phaser.phase(); - let mut first = phaser.register_one().unwrap(); - let mut second = phaser.register_one().unwrap(); - - first.arrive().unwrap(); - drop(first); - assert_eq!(phaser.phase(), phase0); - assert_eq!(phaser.registered_parties(), 1); - assert_eq!(phaser.unarrived_parties(), 1); - - second.arrive().unwrap(); - assert_ne!(phaser.phase(), phase0); -} - -#[test] -fn registration_before_last_arrival_joins_and_delays_current_phase() { - let phaser = Phaser::new(); - let phase = phaser.phase(); - let mut first = phaser.register_one().unwrap(); - let mut second = phaser.register_one().unwrap(); - - first.arrive().unwrap(); - let mut joining = phaser.register_one().unwrap(); - second.arrive().unwrap(); - - assert_eq!(phaser.phase(), phase); - assert_eq!(phaser.unarrived_parties(), 1); - joining.arrive().unwrap(); - assert_ne!(phaser.phase(), phase); -} - -#[test] -fn registration_after_last_arrival_joins_the_advanced_phase() { - let phaser = Phaser::new(); - let phase0 = phaser.phase(); - let mut first = phaser.register_one().unwrap(); - - first.arrive().unwrap(); - let phase1 = phaser.phase(); - assert_ne!(phase1, phase0); - - let mut joining = phaser.register_one().unwrap(); - assert_eq!(phaser.registered_parties(), 2); - assert_eq!(phaser.unarrived_parties(), 2); - assert_eq!(joining.arrive().unwrap(), phase1); - assert_eq!(phaser.phase(), phase1); -} - -#[test] -fn registration_before_last_participant_drop_joins_the_current_phase() { - let phaser = Phaser::new(); - let phase0 = phaser.phase(); - let participant = phaser.register_one().unwrap(); - let joining = phaser.register_one().unwrap(); - - drop(participant); - - assert_eq!(phaser.phase(), phase0); - assert_eq!(phaser.registered_parties(), 1); - assert_eq!(phaser.unarrived_parties(), 1); - assert_eq!(joining.deregister().unwrap(), phase0); - assert_ne!(phaser.phase(), phase0); -} - -#[test] -fn registration_after_last_participant_drop_joins_the_advanced_phase() { - let phaser = Phaser::new(); - let phase0 = phaser.phase(); - let participant = phaser.register_one().unwrap(); - - drop(participant); - let phase1 = phaser.phase(); - let joining = phaser.register_one().unwrap(); - - assert_ne!(phase1, phase0); - assert_eq!(phaser.registered_parties(), 1); - assert_eq!(phaser.unarrived_parties(), 1); - assert_eq!(joining.deregister().unwrap(), phase1); - assert_ne!(phaser.phase(), phase1); -} - -#[test] -fn observer_wait_is_cancel_safe_and_does_not_participate() { - let phaser = 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(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_one().unwrap(); - drop(participant); - assert_eq!(counter.0.load(Ordering::Relaxed), 0); -} - -#[test] -fn advancing_a_phase_wakes_every_registered_waiter_once() { - let phaser = Phaser::new(); - let observed = phaser.phase(); - let participant = phaser.register_one().unwrap(); - 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(observed)); - let mut second_wait = Box::pin(phaser.wait(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 = Phaser::new(); - let phase0 = phaser.phase(); - let participant = phaser.register_one().unwrap(); - 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(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_one().unwrap(); - 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(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_a_pending_phase() { - let phaser = Phaser::new(); - let phase0 = phaser.phase(); - let mut first = phaser.register_one().unwrap(); - let mut second = phaser.register_one().unwrap(); - let panic_waker = Waker::from(Arc::new(PanicWake)); - let mut panic_context = Context::from_waker(&panic_waker); - let mut observer = Box::pin(phaser.wait(phase0)); - - assert_eq!( - Future::poll(observer.as_mut(), &mut panic_context), - Poll::Pending - ); - assert_eq!(first.arrive().unwrap(), 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.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 = Box::pin(second.wait()); - assert_eq!(poll_once(retry.as_mut()), Poll::Ready(Ok(phase1))); - assert_eq!(phaser.arrived_parties(), 0); -} - -#[test] -fn a_late_waiter_for_a_completed_phase_is_immediately_ready() { - let phaser = Phaser::new(); - let observed = phaser.phase(); - let participant = phaser.register_one().unwrap(); - drop(participant); - - let mut wait = Box::pin(phaser.wait(observed)); - assert_eq!(poll_once(wait.as_mut()), Poll::Ready(Ok(phaser.phase()))); -} - #[test] fn phase_identity_wraps_without_an_ordering_contract() { let phaser = Phaser::new(); @@ -473,164 +32,6 @@ fn phase_identity_wraps_without_an_ordering_contract() { assert_ne!(phaser.phase(), observed); } -#[test] -fn registration_overflow_panics_without_partially_updating_state() { - let phaser = Phaser::new(); - let participants = phaser.register(usize::MAX).unwrap(); - - assert!(panic::catch_unwind(|| phaser.register_one().unwrap()).is_err()); - assert!(panic::catch_unwind(|| phaser.register(2).unwrap()).is_err()); - assert_eq!(phaser.registered_parties(), usize::MAX); - assert_eq!(phaser.unarrived_parties(), usize::MAX); - drop(participants); - assert_eq!(phaser.registered_parties(), 0); - assert_eq!(phaser.unarrived_parties(), 0); -} - -#[test] -fn explicit_arrival_and_wait_observe_the_same_completed_phase() { - let phaser = Phaser::new(); - let mut first = phaser.register_one().unwrap(); - let mut second = phaser.register_one().unwrap(); - let observed = first.arrive().unwrap(); - second.arrive().unwrap(); - let next = phaser.phase(); - - assert_ne!(observed, next); - assert_eq!( - poll_once(Box::pin(first.wait()).as_mut()), - Poll::Ready(Ok(next)) - ); - assert_eq!( - poll_once(Box::pin(second.wait()).as_mut()), - Poll::Ready(Ok(next)) - ); - assert_eq!(phaser.arrived_parties(), 0); - - let mut wait = Box::pin(first.wait()); - assert!(poll_once(wait.as_mut()).is_pending()); - second.arrive().unwrap(); - assert_eq!(poll_once(wait.as_mut()), Poll::Ready(Ok(phaser.phase()))); -} - -#[test] -fn explicit_arrival_replaces_a_cancelled_pending_observation() { - let phaser = Phaser::new(); - let mut first = phaser.register_one().unwrap(); - let mut second = phaser.register_one().unwrap(); - assert!(poll_once(Box::pin(first.wait()).as_mut()).is_pending()); - second.arrive().unwrap(); - let next = first.arrive().unwrap(); - assert_eq!(next, phaser.phase()); - - let mut wait = Box::pin(first.wait()); - assert!(poll_once(wait.as_mut()).is_pending()); - second.arrive().unwrap(); - assert_eq!(poll_once(wait.as_mut()), Poll::Ready(Ok(phaser.phase()))); -} - -#[test] -fn cloned_handles_observe_without_registering_and_participants_own_the_state() { - let phaser = Phaser::new(); - let observer = phaser.clone(); - let mut participant = phaser.register_one().unwrap(); - drop(phaser); - assert_eq!(observer.registered_parties(), 1); - let observed = observer.phase(); - participant.arrive().unwrap(); - assert_eq!( - poll_once(Box::pin(observer.wait(observed)).as_mut()), - Poll::Ready(Ok(observer.phase())) - ); -} - -#[test] -fn closing_wakes_all_waiters_once_and_rejects_new_obligations() { - let phaser = Phaser::new(); - let mut first = phaser.register_one().unwrap(); - let second = phaser.register_one().unwrap(); - let observed = first.arrive().unwrap(); - let counter = Arc::new(CountWake(AtomicUsize::new(0))); - let waker = Waker::from(counter.clone()); - let mut context = Context::from_waker(&waker); - let mut observer = Box::pin(phaser.wait(observed)); - let mut wait = Box::pin(first.wait()); - assert!(observer.as_mut().poll(&mut context).is_pending()); - assert!(wait.as_mut().poll(&mut context).is_pending()); - - phaser.clone().close(); - phaser.close(); - assert!(phaser.is_closed()); - assert_eq!(counter.0.load(Ordering::Relaxed), 2); - assert!(matches!(poll_once(observer.as_mut()), Poll::Ready(Err(_)))); - assert!(matches!(poll_once(wait.as_mut()), Poll::Ready(Err(_)))); - drop(wait); - assert!(first.arrive().is_err()); - assert!(phaser.register_one().is_err()); - assert!(phaser.register(2).is_err()); - assert!(phaser.register(0).is_err()); - assert!(first.deregister().is_err()); - drop(second); - assert_eq!(phaser.registered_parties(), 0); - assert_eq!(phaser.unarrived_parties(), 0); - assert_eq!(phaser.phase(), observed); -} - -#[test] -fn completed_arrival_remains_successful_after_close_but_cannot_start_another_round() { - let phaser = Phaser::new(); - let mut first = phaser.register_one().unwrap(); - let mut second = phaser.register_one().unwrap(); - let observed = first.arrive().unwrap(); - let mut observer = Box::pin(phaser.wait(observed)); - assert!(poll_once(observer.as_mut()).is_pending()); - second.arrive().unwrap(); - let completed = phaser.phase(); - phaser.close(); - - assert_eq!(poll_once(observer.as_mut()), Poll::Ready(Ok(completed))); - assert_eq!( - poll_once(Box::pin(first.wait()).as_mut()), - Poll::Ready(Ok(completed)) - ); - assert!(matches!( - poll_once(Box::pin(first.wait()).as_mut()), - Poll::Ready(Err(_)) - )); - drop(first); - drop(second); - assert_eq!(phaser.phase(), completed); -} - -#[test] -fn close_survives_a_panicking_waker_and_notifies_other_waiters() { - let phaser = Phaser::new(); - let observed = phaser.phase(); - let panic_waker = Waker::from(Arc::new(PanicWake)); - let counter = Arc::new(CountWake(AtomicUsize::new(0))); - let count_waker = Waker::from(counter.clone()); - let mut first = Box::pin(phaser.wait(observed)); - let mut second = Box::pin(phaser.wait(observed)); - assert!( - first - .as_mut() - .poll(&mut Context::from_waker(&panic_waker)) - .is_pending() - ); - assert!( - second - .as_mut() - .poll(&mut Context::from_waker(&count_waker)) - .is_pending() - ); - - assert!(panic::catch_unwind(|| phaser.close()).is_err()); - assert!(phaser.is_closed()); - assert_eq!(counter.0.load(Ordering::Relaxed), 1); - assert!(matches!(poll_once(first.as_mut()), Poll::Ready(Err(_)))); - assert!(matches!(poll_once(second.as_mut()), Poll::Ready(Err(_)))); -} - #[test] fn a_late_waiter_observes_completion_across_counter_wraparound() { let phaser = Phaser::new(); @@ -643,35 +44,3 @@ fn a_late_waiter_observes_completion_across_counter_wraparound() { Poll::Ready(Ok(0)) ); } - -#[test] -fn closing_during_waker_clone_does_not_register_after_close() { - use std::mem::ManuallyDrop; - use std::task::RawWaker; - use std::task::RawWakerVTable; - - unsafe fn clone_waker(data: *const ()) -> RawWaker { - // SAFETY: Each raw waker owns an Arc; ManuallyDrop preserves this one's reference. - let phaser = ManuallyDrop::new(unsafe { Arc::::from_raw(data.cast()) }); - phaser.close(); - RawWaker::new(Arc::into_raw(Arc::clone(&phaser)).cast(), &VTABLE) - } - unsafe fn drop_waker(data: *const ()) { - // SAFETY: Consuming a raw waker releases exactly its one owned Arc reference. - drop(unsafe { Arc::::from_raw(data.cast()) }); - } - unsafe fn wake_by_ref(_: *const ()) {} - static VTABLE: RawWakerVTable = - RawWakerVTable::new(clone_waker, drop_waker, wake_by_ref, drop_waker); - - let phaser = Phaser::new(); - let data = Arc::into_raw(Arc::new(phaser.clone())).cast(); - // SAFETY: The vtable maintains Arc ownership and every callback is thread-safe. - let waker = unsafe { Waker::from_raw(RawWaker::new(data, &VTABLE)) }; - let mut wait = Box::pin(phaser.wait(phaser.phase())); - assert!(matches!( - wait.as_mut().poll(&mut Context::from_waker(&waker)), - Poll::Ready(Err(_)) - )); - assert!(phaser.is_closed()); -} diff --git a/benchmarks/Cargo.toml b/benchmarks/Cargo.toml index 19e5ac33..4e98b791 100644 --- a/benchmarks/Cargo.toml +++ b/benchmarks/Cargo.toml @@ -39,6 +39,7 @@ asyncband = { workspace = true, features = [ "once-cell", "once-map", "oneshot", + "phaser", "pool", "rwlock", "semaphore", diff --git a/benchmarks/asyncband/main.rs b/benchmarks/asyncband/main.rs index 907a07ef..8d0c8e08 100644 --- a/benchmarks/asyncband/main.rs +++ b/benchmarks/asyncband/main.rs @@ -27,6 +27,7 @@ mod mutex; mod once; mod once_map; mod oneshot; +mod phaser; mod pool; mod rwlock; mod semaphore; diff --git a/benchmarks/asyncband/phaser/mod.rs b/benchmarks/asyncband/phaser/mod.rs new file mode 100644 index 00000000..00b12391 --- /dev/null +++ b/benchmarks/asyncband/phaser/mod.rs @@ -0,0 +1,119 @@ +// 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::pin::pin; + +use asyncband::phaser::Phaser; +use divan::Bencher; +use divan::black_box; + +use crate::support::bench_context; +use crate::support::poll_pending; +use crate::support::poll_pinned_ready; +use crate::support::poll_ready; + +const PARTICIPANT_COUNTS: &[usize] = &[2, 8, 32]; + +#[divan::bench] +fn ready_wait(bencher: Bencher) { + let mut context = bench_context(); + let phaser = Phaser::new(); + let observed = phaser.phase(); + phaser.register_one().unwrap().arrive().unwrap(); + + bencher.bench_local(|| black_box(poll_ready(phaser.wait(observed), &mut context).unwrap())); +} + +#[divan::bench] +fn repoll_pending(bencher: Bencher) { + let mut context = bench_context(); + let phaser = Phaser::new(); + let mut wait = pin!(phaser.wait(phaser.phase())); + poll_pending(wait.as_mut(), &mut context); + + bencher.bench_local(|| poll_pending(wait.as_mut(), &mut context)); +} + +#[divan::bench] +fn cancel_pending(bencher: Bencher) { + let mut context = bench_context(); + let phaser = Phaser::new(); + let observed = phaser.phase(); + + bencher.bench_local(|| { + let mut wait = pin!(phaser.wait(observed)); + poll_pending(wait.as_mut(), &mut context); + }); + black_box(phaser); +} + +#[divan::bench(args = PARTICIPANT_COUNTS)] +fn register_batch(bencher: Bencher, parties: usize) { + let phaser = Phaser::new(); + + bencher.bench_local(|| { + let participants: Vec<_> = phaser.register(black_box(parties)).unwrap().collect(); + drop(black_box(participants)); + }); +} + +#[divan::bench(args = PARTICIPANT_COUNTS)] +fn register_individually(bencher: Bencher, parties: usize) { + let phaser = Phaser::new(); + + bencher.bench_local(|| { + let participants: Vec<_> = (0..black_box(parties)) + .map(|_| phaser.register_one().unwrap()) + .collect(); + drop(black_box(participants)); + }); +} + +#[divan::bench(args = PARTICIPANT_COUNTS)] +fn arrive_then_wait(bencher: Bencher, parties: usize) { + let mut context = bench_context(); + let phaser = Phaser::new(); + let mut participants: Vec<_> = phaser.register(parties).unwrap().collect(); + + bencher.bench_local(|| { + for participant in &mut participants { + black_box(participant.arrive().unwrap()); + } + for participant in &mut participants { + black_box(poll_ready(participant.wait(), &mut context).unwrap()); + } + }); +} + +#[divan::bench(args = PARTICIPANT_COUNTS)] +fn notify_pending_fanout(bencher: Bencher, parties: usize) { + let mut context = bench_context(); + let phaser = Phaser::new(); + let mut participants: Vec<_> = phaser.register(parties).unwrap().collect(); + let (last, others) = participants.split_last_mut().unwrap(); + + bencher.bench_local(|| { + let mut waiters: Vec<_> = others.iter_mut().map(|p| Box::pin(p.wait())).collect(); + for waiter in &mut waiters { + poll_pending(waiter.as_mut(), &mut context); + } + black_box(poll_ready(last.wait(), &mut context).unwrap()); + for waiter in &mut waiters { + black_box(poll_pinned_ready(waiter.as_mut(), &mut context).unwrap()); + } + }); +} diff --git a/tests-integration/tests/phaser_test.rs b/tests-integration/tests/phaser_test.rs index 1b827b63..e38f5331 100644 --- a/tests-integration/tests/phaser_test.rs +++ b/tests-integration/tests/phaser_test.rs @@ -15,9 +15,671 @@ // 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 asyncband::phaser::Phaser; +fn poll_once(future: std::pin::Pin<&mut F>) -> Poll { + future.poll(&mut Context::from_waker(Waker::noop())) +} + +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 batch_registration_joins_one_observed_phase() { + let phaser = Phaser::new(); + let mut participants = phaser.register(3).unwrap(); + + assert_eq!(participants.len(), 3); + assert_eq!(phaser.registered_parties(), 3); + assert_eq!(phaser.unarrived_parties(), 3); + + let mut first = participants.next().unwrap(); + let observed = first.arrive().unwrap(); + let mut second = participants.next().unwrap(); + second.arrive().unwrap(); + assert_eq!(participants.len(), 1); + assert_eq!(phaser.phase(), observed); + + let counter = Arc::new(CountWake(AtomicUsize::new(0))); + let waker = Waker::from(counter.clone()); + let mut wait = Box::pin(first.wait()); + assert!( + wait.as_mut() + .poll(&mut Context::from_waker(&waker)) + .is_pending() + ); + drop(participants); + assert_eq!(counter.0.load(Ordering::Relaxed), 1); + assert_eq!(poll_once(wait.as_mut()), Poll::Ready(Ok(phaser.phase()))); + assert_ne!(phaser.phase(), observed); + assert_eq!(phaser.registered_parties(), 2); + assert_eq!(phaser.unarrived_parties(), 2); +} + +#[test] +fn collecting_a_batch_can_unwind_without_leaking_registrations() { + let phaser = Phaser::new(); + let mut coordinator = phaser.register_one().unwrap(); + let observed = phaser.phase(); + + assert!( + panic::catch_unwind(|| { + let _: Vec<_> = phaser + .register(3) + .unwrap() + .enumerate() + .map(|(index, participant)| { + assert_ne!(index, 1, "task setup failed"); + participant + }) + .collect(); + }) + .is_err() + ); + assert_eq!(phaser.phase(), observed); + assert_eq!(phaser.registered_parties(), 1); + assert_eq!(phaser.unarrived_parties(), 1); + coordinator.arrive().unwrap(); + assert_ne!(phaser.phase(), observed); +} + +#[test] +fn exhausted_batch_does_not_advance_a_dormant_phaser() { + let phaser = Phaser::new(); + let mut participants = phaser.register(1).unwrap(); + drop(participants.next().unwrap()); + let completed = phaser.phase(); + + assert_eq!(participants.len(), 0); + assert!(participants.next().is_none()); + drop(participants); + assert_eq!(phaser.phase(), completed); + assert_eq!(phaser.registered_parties(), 0); +} + +#[test] +fn an_existing_batch_can_be_iterated_and_withdrawn_after_close() { + let phaser = Phaser::new(); + let mut participants = phaser.register(3).unwrap(); + let observed = phaser.phase(); + phaser.close(); + + let mut participant = participants.next().unwrap(); + drop(participants); + assert_eq!(phaser.registered_parties(), 1); + assert_eq!(phaser.unarrived_parties(), 1); + assert!(participant.arrive().is_err()); + drop(participant); + assert_eq!(phaser.phase(), observed); + assert_eq!(phaser.registered_parties(), 0); + assert_eq!(phaser.unarrived_parties(), 0); +} + +#[test] +fn registering_zero_parties_is_a_noop() { + let phaser = Phaser::new(); + let phase = phaser.phase(); + + assert_eq!(phaser.register(0).unwrap().len(), 0); + 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 = Phaser::new(); + let phase0 = phaser.phase(); + let mut first = phaser.register_one().unwrap(); + let mut second = phaser.register_one().unwrap(); + + assert_eq!(first.arrive().unwrap(), phase0); + assert_eq!(phaser.arrived_parties(), 1); + assert_eq!(second.arrive().unwrap(), phase0); + let phase1 = phaser.phase(); + assert_ne!(phase1, phase0); + assert_eq!(phaser.arrived_parties(), 0); + + assert_eq!(first.arrive().unwrap(), phase1); + assert_eq!(second.arrive().unwrap(), phase1); + assert_ne!(phaser.phase(), phase1); +} + +#[test] +fn unpolled_wait_future_does_not_arrive() { + let phaser = Phaser::new(); + let mut participant = phaser.register_one().unwrap(); + + let wait = participant.wait(); + + assert_eq!(phaser.arrived_parties(), 0); + drop(wait); + assert_eq!(phaser.arrived_parties(), 0); +} + +#[test] +fn cancelled_wait_retry_waits_for_original_phase_after_advance() { + let phaser = Phaser::new(); + let phase0 = phaser.phase(); + let mut first = phaser.register_one().unwrap(); + let mut second = phaser.register_one().unwrap(); + + { + let mut cancelled = Box::pin(first.wait()); + assert!(poll_once(cancelled.as_mut()).is_pending()); + } + + assert_eq!(phaser.arrived_parties(), 1); + assert_eq!(second.arrive().unwrap(), phase0); + let phase1 = phaser.phase(); + assert_ne!(phase1, phase0); + assert_eq!(phaser.arrived_parties(), 0); + + let mut retry = Box::pin(first.wait()); + assert_eq!(poll_once(retry.as_mut()), Poll::Ready(Ok(phase1))); + assert_eq!(phaser.arrived_parties(), 0); +} + +#[test] +fn cancelled_wait_retry_before_advance_does_not_arrive_twice() { + let phaser = Phaser::new(); + let mut first = phaser.register_one().unwrap(); + let mut second = phaser.register_one().unwrap(); + + { + let mut cancelled = Box::pin(first.wait()); + assert!(poll_once(cancelled.as_mut()).is_pending()); + } + + let mut retry = Box::pin(first.wait()); + assert!(poll_once(retry.as_mut()).is_pending()); + assert_eq!(phaser.arrived_parties(), 1); + + second.arrive().unwrap(); + assert!(poll_once(retry.as_mut()).is_ready()); +} + +#[test] +fn dropping_last_participant_advances_once_and_dormant_phaser_can_be_reused() { + let phaser = Phaser::new(); + let phase0 = phaser.phase(); + let participant = phaser.register_one().unwrap(); + + 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_one().unwrap(); + assert_eq!(participant.arrive().unwrap(), phase1); + assert_ne!(phaser.phase(), phase1); +} + +#[test] +fn dropping_an_arrived_participant_only_removes_its_next_phase_registration() { + let phaser = Phaser::new(); + let phase0 = phaser.phase(); + let mut first = phaser.register_one().unwrap(); + let mut second = phaser.register_one().unwrap(); + + first.arrive().unwrap(); + drop(first); + assert_eq!(phaser.phase(), phase0); + assert_eq!(phaser.registered_parties(), 1); + assert_eq!(phaser.unarrived_parties(), 1); + + second.arrive().unwrap(); + assert_ne!(phaser.phase(), phase0); +} + +#[test] +fn registration_before_last_arrival_joins_and_delays_current_phase() { + let phaser = Phaser::new(); + let phase = phaser.phase(); + let mut first = phaser.register_one().unwrap(); + let mut second = phaser.register_one().unwrap(); + + first.arrive().unwrap(); + let mut joining = phaser.register_one().unwrap(); + second.arrive().unwrap(); + + assert_eq!(phaser.phase(), phase); + assert_eq!(phaser.unarrived_parties(), 1); + joining.arrive().unwrap(); + assert_ne!(phaser.phase(), phase); +} + +#[test] +fn registration_after_last_arrival_joins_the_advanced_phase() { + let phaser = Phaser::new(); + let phase0 = phaser.phase(); + let mut first = phaser.register_one().unwrap(); + + first.arrive().unwrap(); + let phase1 = phaser.phase(); + assert_ne!(phase1, phase0); + + let mut joining = phaser.register_one().unwrap(); + assert_eq!(phaser.registered_parties(), 2); + assert_eq!(phaser.unarrived_parties(), 2); + assert_eq!(joining.arrive().unwrap(), phase1); + assert_eq!(phaser.phase(), phase1); +} + +#[test] +fn registration_before_last_participant_drop_joins_the_current_phase() { + let phaser = Phaser::new(); + let phase0 = phaser.phase(); + let participant = phaser.register_one().unwrap(); + let joining = phaser.register_one().unwrap(); + + drop(participant); + + assert_eq!(phaser.phase(), phase0); + assert_eq!(phaser.registered_parties(), 1); + assert_eq!(phaser.unarrived_parties(), 1); + assert_eq!(joining.deregister().unwrap(), phase0); + assert_ne!(phaser.phase(), phase0); +} + +#[test] +fn registration_after_last_participant_drop_joins_the_advanced_phase() { + let phaser = Phaser::new(); + let phase0 = phaser.phase(); + let participant = phaser.register_one().unwrap(); + + drop(participant); + let phase1 = phaser.phase(); + let joining = phaser.register_one().unwrap(); + + assert_ne!(phase1, phase0); + assert_eq!(phaser.registered_parties(), 1); + assert_eq!(phaser.unarrived_parties(), 1); + assert_eq!(joining.deregister().unwrap(), phase1); + assert_ne!(phaser.phase(), phase1); +} + +#[test] +fn observer_wait_is_cancel_safe_and_does_not_participate() { + let phaser = 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(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_one().unwrap(); + drop(participant); + assert_eq!(counter.0.load(Ordering::Relaxed), 0); +} + +#[test] +fn advancing_a_phase_wakes_every_registered_waiter_once() { + let phaser = Phaser::new(); + let observed = phaser.phase(); + let participant = phaser.register_one().unwrap(); + 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(observed)); + let mut second_wait = Box::pin(phaser.wait(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 repolling_updates_the_task_that_will_be_notified() { + let phaser = Phaser::new(); + let participant = phaser.register_one().unwrap(); + let first = Arc::new(CountWake(AtomicUsize::new(0))); + let second = Arc::new(CountWake(AtomicUsize::new(0))); + let first_waker = Waker::from(first.clone()); + let second_waker = Waker::from(second.clone()); + let mut wait = Box::pin(phaser.wait(phaser.phase())); + + for waker in [&first_waker, &first_waker, &second_waker, &second_waker] { + assert!( + wait.as_mut() + .poll(&mut Context::from_waker(waker)) + .is_pending() + ); + } + drop(participant); + + assert_eq!(first.0.load(Ordering::Relaxed), 0); + assert_eq!(second.0.load(Ordering::Relaxed), 1); + assert_eq!(poll_once(wait.as_mut()), Poll::Ready(Ok(phaser.phase()))); +} + +#[test] +fn cancelling_a_woken_waiter_does_not_unregister_a_next_phase_waiter() { + let phaser = Phaser::new(); + let phase0 = phaser.phase(); + let participant = phaser.register_one().unwrap(); + 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(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_one().unwrap(); + 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(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_a_pending_phase() { + let phaser = Phaser::new(); + let phase0 = phaser.phase(); + let mut first = phaser.register_one().unwrap(); + let mut second = phaser.register_one().unwrap(); + let panic_waker = Waker::from(Arc::new(PanicWake)); + let mut panic_context = Context::from_waker(&panic_waker); + let mut observer = Box::pin(phaser.wait(phase0)); + + assert_eq!( + Future::poll(observer.as_mut(), &mut panic_context), + Poll::Pending + ); + assert_eq!(first.arrive().unwrap(), 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.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 = Box::pin(second.wait()); + assert_eq!(poll_once(retry.as_mut()), Poll::Ready(Ok(phase1))); + assert_eq!(phaser.arrived_parties(), 0); +} + +#[test] +fn a_late_waiter_for_a_completed_phase_is_immediately_ready() { + let phaser = Phaser::new(); + let observed = phaser.phase(); + let participant = phaser.register_one().unwrap(); + drop(participant); + + let mut wait = Box::pin(phaser.wait(observed)); + assert_eq!(poll_once(wait.as_mut()), Poll::Ready(Ok(phaser.phase()))); +} + +#[test] +fn registration_overflow_panics_without_partially_updating_state() { + let phaser = Phaser::new(); + let participants = phaser.register(usize::MAX).unwrap(); + + assert!(panic::catch_unwind(|| phaser.register_one().unwrap()).is_err()); + assert!(panic::catch_unwind(|| phaser.register(2).unwrap()).is_err()); + assert_eq!(phaser.registered_parties(), usize::MAX); + assert_eq!(phaser.unarrived_parties(), usize::MAX); + drop(participants); + assert_eq!(phaser.registered_parties(), 0); + assert_eq!(phaser.unarrived_parties(), 0); +} + +#[test] +fn explicit_arrival_and_wait_observe_the_same_completed_phase() { + let phaser = Phaser::new(); + let mut first = phaser.register_one().unwrap(); + let mut second = phaser.register_one().unwrap(); + let observed = first.arrive().unwrap(); + second.arrive().unwrap(); + let next = phaser.phase(); + + assert_ne!(observed, next); + assert_eq!( + poll_once(Box::pin(first.wait()).as_mut()), + Poll::Ready(Ok(next)) + ); + assert_eq!( + poll_once(Box::pin(second.wait()).as_mut()), + Poll::Ready(Ok(next)) + ); + assert_eq!(phaser.arrived_parties(), 0); + + let mut wait = Box::pin(first.wait()); + assert!(poll_once(wait.as_mut()).is_pending()); + second.arrive().unwrap(); + assert_eq!(poll_once(wait.as_mut()), Poll::Ready(Ok(phaser.phase()))); +} + +#[test] +fn explicit_arrival_replaces_a_cancelled_pending_observation() { + let phaser = Phaser::new(); + let mut first = phaser.register_one().unwrap(); + let mut second = phaser.register_one().unwrap(); + assert!(poll_once(Box::pin(first.wait()).as_mut()).is_pending()); + second.arrive().unwrap(); + let next = first.arrive().unwrap(); + assert_eq!(next, phaser.phase()); + + let mut wait = Box::pin(first.wait()); + assert!(poll_once(wait.as_mut()).is_pending()); + second.arrive().unwrap(); + assert_eq!(poll_once(wait.as_mut()), Poll::Ready(Ok(phaser.phase()))); +} + +#[test] +fn cloned_handles_observe_without_registering_and_participants_own_the_state() { + let phaser = Phaser::new(); + let observer = phaser.clone(); + let mut participant = phaser.register_one().unwrap(); + drop(phaser); + assert_eq!(observer.registered_parties(), 1); + let observed = observer.phase(); + participant.arrive().unwrap(); + assert_eq!( + poll_once(Box::pin(observer.wait(observed)).as_mut()), + Poll::Ready(Ok(observer.phase())) + ); +} + +#[test] +fn closing_wakes_all_waiters_once_and_rejects_new_obligations() { + let phaser = Phaser::new(); + let mut first = phaser.register_one().unwrap(); + let second = phaser.register_one().unwrap(); + let observed = first.arrive().unwrap(); + let counter = Arc::new(CountWake(AtomicUsize::new(0))); + let waker = Waker::from(counter.clone()); + let mut context = Context::from_waker(&waker); + let mut observer = Box::pin(phaser.wait(observed)); + let mut wait = Box::pin(first.wait()); + assert!(observer.as_mut().poll(&mut context).is_pending()); + assert!(wait.as_mut().poll(&mut context).is_pending()); + + phaser.clone().close(); + phaser.close(); + assert!(phaser.is_closed()); + assert_eq!(counter.0.load(Ordering::Relaxed), 2); + assert!(matches!(poll_once(observer.as_mut()), Poll::Ready(Err(_)))); + assert!(matches!(poll_once(wait.as_mut()), Poll::Ready(Err(_)))); + drop(wait); + assert!(first.arrive().is_err()); + assert!(phaser.register_one().is_err()); + assert!(phaser.register(2).is_err()); + assert!(phaser.register(0).is_err()); + assert!(first.deregister().is_err()); + drop(second); + assert_eq!(phaser.registered_parties(), 0); + assert_eq!(phaser.unarrived_parties(), 0); + assert_eq!(phaser.phase(), observed); +} + +#[test] +fn completed_arrival_remains_successful_after_close_but_cannot_start_another_round() { + let phaser = Phaser::new(); + let mut first = phaser.register_one().unwrap(); + let mut second = phaser.register_one().unwrap(); + let observed = first.arrive().unwrap(); + let mut observer = Box::pin(phaser.wait(observed)); + assert!(poll_once(observer.as_mut()).is_pending()); + second.arrive().unwrap(); + let completed = phaser.phase(); + phaser.close(); + + assert_eq!(poll_once(observer.as_mut()), Poll::Ready(Ok(completed))); + assert_eq!( + poll_once(Box::pin(first.wait()).as_mut()), + Poll::Ready(Ok(completed)) + ); + assert!(matches!( + poll_once(Box::pin(first.wait()).as_mut()), + Poll::Ready(Err(_)) + )); + drop(first); + drop(second); + assert_eq!(phaser.phase(), completed); +} + +#[test] +fn close_survives_a_panicking_waker_and_notifies_other_waiters() { + let phaser = Phaser::new(); + let observed = phaser.phase(); + let panic_waker = Waker::from(Arc::new(PanicWake)); + let counter = Arc::new(CountWake(AtomicUsize::new(0))); + let count_waker = Waker::from(counter.clone()); + let mut first = Box::pin(phaser.wait(observed)); + let mut second = Box::pin(phaser.wait(observed)); + assert!( + first + .as_mut() + .poll(&mut Context::from_waker(&panic_waker)) + .is_pending() + ); + assert!( + second + .as_mut() + .poll(&mut Context::from_waker(&count_waker)) + .is_pending() + ); + + assert!(panic::catch_unwind(|| phaser.close()).is_err()); + assert!(phaser.is_closed()); + assert_eq!(counter.0.load(Ordering::Relaxed), 1); + assert!(matches!(poll_once(first.as_mut()), Poll::Ready(Err(_)))); + assert!(matches!(poll_once(second.as_mut()), Poll::Ready(Err(_)))); +} + +#[test] +fn closing_during_waker_clone_does_not_register_after_close() { + use std::mem::ManuallyDrop; + use std::task::RawWaker; + use std::task::RawWakerVTable; + + unsafe fn clone_waker(data: *const ()) -> RawWaker { + // SAFETY: Each raw waker owns an Arc; ManuallyDrop preserves this one's reference. + let phaser = ManuallyDrop::new(unsafe { Arc::::from_raw(data.cast()) }); + phaser.close(); + RawWaker::new(Arc::into_raw(Arc::clone(&phaser)).cast(), &VTABLE) + } + unsafe fn drop_waker(data: *const ()) { + // SAFETY: Consuming a raw waker releases exactly its one owned Arc reference. + drop(unsafe { Arc::::from_raw(data.cast()) }); + } + unsafe fn wake_by_ref(_: *const ()) {} + static VTABLE: RawWakerVTable = + RawWakerVTable::new(clone_waker, drop_waker, wake_by_ref, drop_waker); + + let phaser = Phaser::new(); + let data = Arc::into_raw(Arc::new(phaser.clone())).cast(); + // SAFETY: The vtable maintains Arc ownership and every callback is thread-safe. + let waker = unsafe { Waker::from_raw(RawWaker::new(data, &VTABLE)) }; + let mut wait = Box::pin(phaser.wait(phaser.phase())); + assert!(matches!( + wait.as_mut().poll(&mut Context::from_waker(&waker)), + Poll::Ready(Err(_)) + )); + assert!(phaser.is_closed()); +} + #[tokio::test] +#[cfg_attr(miri, ignore = "requires an OS-backed Tokio runtime")] async fn participant_can_wait_from_a_spawned_task() { let phaser = Phaser::new(); let mut first = phaser.register_one().unwrap(); @@ -32,6 +694,7 @@ async fn participant_can_wait_from_a_spawned_task() { } #[tokio::test] +#[cfg_attr(miri, ignore = "requires an OS-backed Tokio runtime")] async fn observer_waits_without_becoming_a_party() { let phaser = Phaser::new(); let observed = phaser.phase(); @@ -80,6 +743,7 @@ fn arrivals_publish_each_workers_writes_across_threads() { } #[tokio::test] +#[cfg_attr(miri, ignore = "requires an OS-backed Tokio runtime")] async fn a_failed_task_can_close_the_group_without_reporting_phase_completion() { let phaser = Phaser::new(); let mut worker = phaser.register_one().unwrap(); diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 41c5a877..51fb9866 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -121,6 +121,10 @@ impl CommandMiri { &["--test", "unsafe_paths_test"], )); run_command(make_miri_cmd("tests-integration", &["--test", "mpsc_test"])); + run_command(make_miri_cmd( + "tests-integration", + &["--test", "phaser_test"], + )); } } From 78855a46850afc68ea78539536ff976c92404078 Mon Sep 17 00:00:00 2001 From: tison Date: Fri, 11 Sep 2026 13:13:34 +0800 Subject: [PATCH 12/12] refactor(phaser): reuse borrowed waker registration Register under the existing state lock and defer replaced waker destruction until after unlocking, following the contract from #257. Remove the separate probe and owned registration APIs, and replace clone-reentrancy coverage with replacement and cancellation drop-reentrancy tests. --- asyncband/src/internal/wakerset.rs | 33 ------------ asyncband/src/phaser/mod.rs | 21 +------- tests-integration/tests/phaser_test.rs | 74 +++++++++++++++++--------- 3 files changed, 50 insertions(+), 78 deletions(-) diff --git a/asyncband/src/internal/wakerset.rs b/asyncband/src/internal/wakerset.rs index 3d89107d..c49d49fd 100644 --- a/asyncband/src/internal/wakerset.rs +++ b/asyncband/src/internal/wakerset.rs @@ -102,39 +102,6 @@ impl WakerSet { None } - /// Returns whether a live registration already wakes the given task, without cloning a waker. - /// - /// The caller must check completion before querying a potentially stale token. - #[inline] - pub fn will_wake(&self, token: &WakerToken, waker: &Waker) -> bool { - self.wakers - .get(token.0) - .expect("waker token must refer to an occupied slot") - .will_wake(waker) - } - - /// Registers or replaces a waker cloned before taking the owner's state lock. - /// - /// Returns the previous waker so its destructor can run after releasing that lock. - #[inline] - #[must_use = "drop the returned waker after releasing the state lock"] - pub fn register_owned( - &mut self, - token: &mut Option, - waker: Waker, - ) -> Option { - if let Some(token) = token { - let current = self - .wakers - .get_mut(token.0) - .expect("waker token must refer to an occupied slot"); - return Some(mem::replace(current, waker)); - } - - *token = Some(WakerToken(self.wakers.insert(waker))); - None - } - /// Removes the waker identified by `token`. /// /// The owner must clear stale tokens without calling this method after detaching the set. The diff --git a/asyncband/src/phaser/mod.rs b/asyncband/src/phaser/mod.rs index b2994f17..ae7fbe12 100644 --- a/asyncband/src/phaser/mod.rs +++ b/asyncband/src/phaser/mod.rs @@ -524,31 +524,12 @@ impl Future for PhaserWait<'_> { fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.get_mut(); - { - let state = this.phaser.state.lock(); - if let ready @ Poll::Ready(_) = state.completion(this.observed) { - this.token = None; - return ready; - } - if this - .token - .as_ref() - .is_some_and(|token| state.waiters.will_wake(token, cx.waker())) - { - return Poll::Pending; - } - } - - // Waker cloning may reenter or close this phaser. Recheck completion before registering. - let waker = cx.waker().clone(); let mut state = this.phaser.state.lock(); if let ready @ Poll::Ready(_) = state.completion(this.observed) { this.token = None; - drop(state); - drop(waker); return ready; } - let retired = state.waiters.register_owned(&mut this.token, waker); + let retired = state.waiters.register(&mut this.token, cx.waker()); drop(state); drop(retired); Poll::Pending diff --git a/tests-integration/tests/phaser_test.rs b/tests-integration/tests/phaser_test.rs index e38f5331..9730c859 100644 --- a/tests-integration/tests/phaser_test.rs +++ b/tests-integration/tests/phaser_test.rs @@ -47,6 +47,20 @@ impl Wake for PanicWake { } } +struct CloseOnDrop(Phaser); + +impl Wake for CloseOnDrop { + fn wake(self: Arc) { + self.0.close(); + } +} + +impl Drop for CloseOnDrop { + fn drop(&mut self) { + self.0.close(); + } +} + #[test] fn batch_registration_joins_one_observed_phase() { let phaser = Phaser::new(); @@ -647,34 +661,44 @@ fn close_survives_a_panicking_waker_and_notifies_other_waiters() { } #[test] -fn closing_during_waker_clone_does_not_register_after_close() { - use std::mem::ManuallyDrop; - use std::task::RawWaker; - use std::task::RawWakerVTable; - - unsafe fn clone_waker(data: *const ()) -> RawWaker { - // SAFETY: Each raw waker owns an Arc; ManuallyDrop preserves this one's reference. - let phaser = ManuallyDrop::new(unsafe { Arc::::from_raw(data.cast()) }); - phaser.close(); - RawWaker::new(Arc::into_raw(Arc::clone(&phaser)).cast(), &VTABLE) - } - unsafe fn drop_waker(data: *const ()) { - // SAFETY: Consuming a raw waker releases exactly its one owned Arc reference. - drop(unsafe { Arc::::from_raw(data.cast()) }); - } - unsafe fn wake_by_ref(_: *const ()) {} - static VTABLE: RawWakerVTable = - RawWakerVTable::new(clone_waker, drop_waker, wake_by_ref, drop_waker); +fn replacing_a_waiter_waker_can_close_the_phaser_from_its_destructor() { + let phaser = Phaser::new(); + let waker = Waker::from(Arc::new(CloseOnDrop(phaser.clone()))); + let mut wait = Box::pin(phaser.wait(phaser.phase())); + assert!( + wait.as_mut() + .poll(&mut Context::from_waker(&waker)) + .is_pending() + ); + drop(waker); + assert!(!phaser.is_closed()); + + let counter = Arc::new(CountWake(AtomicUsize::new(0))); + let replacement = Waker::from(counter.clone()); + assert!( + wait.as_mut() + .poll(&mut Context::from_waker(&replacement)) + .is_pending() + ); + assert!(phaser.is_closed()); + assert_eq!(counter.0.load(Ordering::Relaxed), 1); + assert!(matches!(poll_once(wait.as_mut()), Poll::Ready(Err(_)))); +} +#[test] +fn cancelling_a_waiter_can_close_the_phaser_from_its_waker_destructor() { let phaser = Phaser::new(); - let data = Arc::into_raw(Arc::new(phaser.clone())).cast(); - // SAFETY: The vtable maintains Arc ownership and every callback is thread-safe. - let waker = unsafe { Waker::from_raw(RawWaker::new(data, &VTABLE)) }; + let waker = Waker::from(Arc::new(CloseOnDrop(phaser.clone()))); let mut wait = Box::pin(phaser.wait(phaser.phase())); - assert!(matches!( - wait.as_mut().poll(&mut Context::from_waker(&waker)), - Poll::Ready(Err(_)) - )); + assert!( + wait.as_mut() + .poll(&mut Context::from_waker(&waker)) + .is_pending() + ); + drop(waker); + assert!(!phaser.is_closed()); + + drop(wait); assert!(phaser.is_closed()); }