Repository navigation
Provide a dynamically registered phaser #220
Description
Activity
- addedenhancementNew feature or requestNew feature or requestgood first issueGood for newcomersGood for newcomers
on Aug 26, 2026 - added a parent issue
on Aug 26, 2026 Hi, I'd like to contribute. Here is my proposed design.
Public API
Use a phase newtype instead of raw
u64so the public contract can say phases are identity tokens, not ordered timestamps across wraparound.pub struct Phaser; #[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] pub struct Phase(u64); impl Phase { pub const fn get(self) -> u64; } impl Phaser { pub const fn new() -> Self; pub fn phase(&self) -> Phase; pub fn registered_parties(&self) -> u32; pub fn arrived_parties(&self) -> u32; pub fn unarrived_parties(&self) -> u32; pub fn register(self: &std::sync::Arc<Self>) -> PhaserParticipant; pub fn register_many(self: &std::sync::Arc<Self>, parties: u32) -> Vec<PhaserParticipant>; pub async fn wait_for_advance(&self, phase: Phase) -> Phase; } pub struct PhaserParticipant; impl PhaserParticipant { pub fn arrive(&mut self) -> Phase; pub async fn arrive_and_wait(&mut self) -> Phase; pub fn arrive_and_deregister(self) -> Phase; }
State Model
Represent internal state as one lock-protected record:
phase: Phase,registered: u32,unarrived: u32, andwaiters: WaitSet.Registering
nparties under the state lock increments bothregisteredandunarrived, records the observed phase on each returned participant, and panics onu32overflow.Arriving a participant is idempotent per tracked phase: if it has not arrived in the tracked phase, decrement
unarrivedonce and mark the participant arrived; if already arrived, do not change counts.Completing a phase happens while holding the state lock when
registered > 0 && unarrived == 0; resetunarrived = registered, advancephase = phase.wrapping_add(1), clear each continuing participant's arrived flag when it next observes the new phase, take all waiters, release the lock, then callwake_all.Arrive-and-deregister under the lock removes the participant from
registered; if the participant had not arrived for the tracked phase, also remove its outstanding arrival requirement by decrementingunarrived; if it had arrived, only decrementregistered.Dropping a still-registered participant is exactly arrive-and-deregister for its current tracked phase, including possible phase advancement if it was the last outstanding party.
Zero registered parties are dormant and reusable: reaching zero does not self-advance repeatedly, but a wait on the just-completed phase can still be woken by the deregistration that reached zero, and later registration joins the current dormant phase.
Waiting for an observed phase never registers, never arrives, and on cancellation only unregisters its
WakerToken.Arrival in
arrive_and_waitcommits on first poll. The participant stores a separate pending-wait phase until the wait completes; cancelling after arrival does not retract it, and retrying with the same participant waits for that stored phase even if the phaser has already advanced, rather than arriving in the next phase.@ButterBright Thanks for your contribution! If possible, you can directly open a pull request where inline comments become easier.
Reacted by Huang YouliangThis should be resolved by #250 while some follow-ups can be continously applied for improvements.
Background
Asyncband currently provides several related coordination primitives, but none covers reusable phases with a participant set that changes over time:
Barrieris reusable but fixes the participant count at construction time.Latchis a one-way countdown.WaitGrouptracks a dynamic set of handles, but it represents one completion epoch rather than repeated rounds.A phaser fills the gap for iterative work whose participants may join or leave between rounds. Java's
Phaseris the main prior art: parties register dynamically, arrive separately from waiting, optionally deregister on arrival, and observe a monotonically advancing phase.This primitive fits Asyncband's runtime-independent scope because progress is driven only by explicit registration, arrival, drop, and polling. It does not need to spawn tasks, obtain an executor, or install timers.
Tracked by #218.
Proposed minimum contract
The first version should provide one non-hierarchical phaser with these capabilities:
Registration racing with phase advancement must have one documented linearization point: it joins either the current phase or the next phase, never an ambiguous mixture of both.
The primitive must remain runtime agnostic and use the standard
Future/Wakercontract.Design considerations
Participant representation
Java exposes integer counts and treats arriving without registration as a usage error. Rust can make this safer by returning a participant capability from registration. A participant handle could prevent double arrival in one phase and make deregistration explicit in the type system.
The main alternatives are:
Phaser, which are compact and match Java but permit more misuse; orA participant handle is the preferred initial direction, but the public shape should be settled before implementation.
Drop and deregistration
A registered participant that disappears without arriving can prevent a phase from advancing forever. Automatically deregistering on handle drop avoids that leak, but an implicit drop can also advance a phase at a surprising point.
The issue must decide whether drop means:
The first option is the most cancellation-resilient, but it needs deterministic tests for races with the last arrival.
Arrival versus waiting cancellation
Arrival and waiting are distinct operations. Once an arrival has been recorded, cancelling the subsequent wait must not retract that arrival. Retrying with the same participant must wait for the already-recorded phase rather than count a second arrival.
This differs from a future whose only effect happens at completion. The cancellation section of the public API must state the exact point at which arrival becomes committed.
Zero participants
Java terminates a phaser by default when deregistration reduces the participant count to zero. Asyncband could instead leave an empty phaser dormant so later registrations can start another phase. Dormancy is simpler and more reusable, while explicit termination can be added later if a concrete need appears.
Phase identity and wraparound
Waiters should compare phase identity rather than only an
arrived == registeredpredicate, otherwise a delayed waiter can confuse two rounds. The public phase type, wraparound behavior, and ordering guarantees need to be documented. A wrapping integer is acceptable if equality, rather than total ordering across the wrap, is the synchronization contract.Fairness and wake-up behavior
All waiters for the completed phase should become runnable. Registration and arrival operations themselves do not require scheduling fairness, but waiter storage must handle cancellation without retaining stale wakers or losing a phase transition.
Key trade-offs
arrive_and_waitfuture versus an explicit arrival token followed by waiting.Non-goals
The first version should not include:
These can be evaluated independently after the core contract is proven.
Acceptance criteria
cargo x.