Skip to content

feat(phaser): add reusable phase coordination - #250

Open
ButterBright wants to merge 4 commits into
apache:mainfrom
ButterBright:main
Open

feat(phaser): add reusable phase coordination#250
ButterBright wants to merge 4 commits into
apache:mainfrom
ButterBright:main

Conversation

@ButterBright

@ButterBright ButterBright commented Aug 30, 2026

Copy link
Copy Markdown
Member

Summary

  • Add an opt-in reusable Phaser synchronization primitive with dynamically registered RAII participants.
  • Define cancellation-safe arrival and waiting behavior across repeated phases.
  • Support non-participant phase observers, participant deregistration on drop, dormant empty phasers, and wrapping phase identities.
  • Add tests covering phase advancement, registration, deregistration, cancellation, stale wakers, and repeated reuse.

Design Notes

Registration returns a PhaserParticipant capability instead of exposing counter-oriented arrival methods directly. This associates each registration with a participant lifecycle and prevents duplicate arrival within one phase.

Dropping a participant arrives and deregisters it. Cancelling arrive_and_wait after its first poll does not retract the committed arrival, and retrying waits for the previously recorded phase instead of arriving again.

An empty phaser remains dormant and can accept new participants later. Waiters identify completion by phase identity, and phase values use wrapping equality without providing a total ordering across wraparound.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Phaser::poll_wait currently clones wakers unconditionally instead of following the repo’s established WaitSet::will_wake pattern, adding avoidable per-poll overhead.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds a new opt-in asyncband::phaser synchronization primitive for coordinating repeated phases with a dynamic participant set, including RAII participants, cancel-safe waiting semantics, and documentation/tests to integrate it into the crate’s public surface.

Changes:

  • Introduce asyncband::phaser::{Phase, Phaser, PhaserParticipant} behind a new phaser feature flag.
  • Add unit + integration tests covering registration/arrival/advance semantics and cross-task waiting.
  • Update crate docs (README + crate-level docs) and changelog to advertise the new primitive.
File summaries
File Description
tests-integration/tests/traits_test.rs Extends trait assertions (Send/Sync/Unpin) to cover new public phaser types.
tests-integration/tests/phaser_test.rs Adds integration tests for spawned-task waiting and observer semantics.
tests-integration/Cargo.toml Enables asyncband’s new phaser feature for integration tests.
README.md Documents the new Phaser feature in the public feature table.
CHANGELOG.md Records the addition of the new opt-in Phaser.
Cargo.lock Captures new dev/test dependency resolution (e.g., tokio-test).
asyncband/src/phaser/tests.rs Adds focused unit tests for phase advancement, cancellation, waker behavior, and wraparound.
asyncband/src/phaser/mod.rs Implements the new Phaser primitive, participants, waiting, and internal state transitions.
asyncband/src/lib.rs Wires the phaser module into the crate behind a feature flag and updates crate docs.
asyncband/src/internal/mod.rs Extends internal module feature gating to include phaser where needed.
asyncband/Cargo.toml Adds the phaser feature and includes tokio-test for module tests.
Review details
  • Files reviewed: 10/11 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +371 to +388
fn poll_wait(
&self,
token: &mut Option<WakerToken>,
observed: Phase,
cx: &mut Context<'_>,
) -> Poll<Phase> {
let waker = cx.waker().clone();
let _retired_waker = {
let mut state = self.state.lock();
if state.phase != observed {
let phase = state.phase;
*token = None;
return Poll::Ready(phase);
}
state.waiters.register(token, waker)
};
Poll::Pending
}
@tisonkun tisonkun mentioned this pull request Sep 4, 2026
33 tasks

@orthur2 orthur2 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you rebase this onto current main branch first? Since the branch was last updated, #273 replaced WaitSet with WakerSet, and the recent API-table changes now conflict with this branch.

I'd also recommend adding a short Summary to the PR description, in line with AGENTS.md.You can refer to the body of PRs that have already been merged.

I'd be happy to do a more thorough review once it's rebased.

@ButterBright

Copy link
Copy Markdown
Member Author

Updated.

@orthur2 orthur2 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the update!
I went through the implementation against #220, and it looks good to me. I left a couple of comments on the synchronization guarantee and using the API from a spawned task.

Comment thread CHANGELOG.md
* 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you move this entry to Unreleased list?

//!
//! Waiters compare phase identity instead of inferring transitions from party counts.
//!
//! # Examples

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add an async example showing a few phases, including separate arrival and waiting?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, a spawned task needs a separate Arc<Phaser> to call wait_for_advance() after participant.arrive(). The participant already holds that same Arc internally. I'd suggest exposing a phaser() accessor so callers can use the two operations separately without carrying another handle.

//!
//! [`Phaser::arrived_parties`] is the difference between the registered and unarrived counts.
//!
//! All state transitions and waiter registration share one synchronization point.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we spell out the memory visibility guarantee here?

The shared synchronization point explains how the internal transitions are ordered, but does not explicitly tell callers whether operations before each participant's arrival happen-before operations after a wait observes that phase's completion. The shared mutex already provides this guarantee, but it should be explicit in the public API docs.

I added a # Synchronization section for this in ManualResetEvent last week. Something similar would help here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants