Skip to content

Provide a dynamically registered phaser #220

Description

@tisonkun

Background

Asyncband currently provides several related coordination primitives, but none covers reusable phases with a participant set that changes over time:

  • Barrier is reusable but fixes the participant count at construction time.
  • Latch is a one-way countdown.
  • WaitGroup tracks 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 Phaser is 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:

  • observe the current phase;
  • register one participant and optionally register several participants at once;
  • record an arrival without waiting;
  • arrive and wait for the phase to advance;
  • arrive and deregister so the participant is excluded from later phases;
  • wait for a particular observed phase to advance without implicitly registering another participant;
  • wake every waiter for a completed phase exactly once.

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/Waker contract.

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:

  1. counter-oriented methods directly on Phaser, which are compact and match Java but permit more misuse; or
  2. RAII participant handles, which add types but can encode one registration and its lifecycle.

A 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:

  • arrive and deregister from the current phase;
  • deregister only from future phases while the current arrival remains required; or
  • a usage error requiring explicit deregistration.

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 == registered predicate, 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

  • RAII participant safety versus a smaller counter-oriented API.
  • Automatic drop cleanup versus surprising implicit phase advancement.
  • A dormant zero-party state versus Java-style automatic termination.
  • A combined arrive_and_wait future versus an explicit arrival token followed by waiting.
  • A compact shared state versus generation-aware waiter bookkeeping needed for cancellation safety.

Non-goals

The first version should not include:

  • parent/child or tiered phasers;
  • executor, task, or thread integration;
  • timeout APIs;
  • user-defined barrier actions;
  • forced termination or recovery policies;
  • priority scheduling.

These can be evaluated independently after the core contract is proven.

Acceptance criteria

  • The public contract explains registration, arrival, deregistration, phase advancement, drop, and cancellation behavior.
  • Dynamic join and leave behavior is covered across multiple phases.
  • A participant cannot accidentally contribute two arrivals to one phase.
  • Cancelling a waiter neither retracts a committed arrival nor leaks its waker.
  • Races between registration, the final arrival, deregistration, and waiter cancellation have deterministic regression tests.
  • No executor, timer, or runtime-specific dependency is introduced.
  • Repository build, test, lint, and formatting workflows pass through cargo x.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions