Skip to content

feat(completion): add a shared one-shot completion primitive - #240

Open
QwQBiG wants to merge 2 commits into
apache:mainfrom
QwQBiG:feat/completion
Open

feat(completion): add a shared one-shot completion primitive#240
QwQBiG wants to merge 2 commits into
apache:mainfrom
QwQBiG:feat/completion

Conversation

@QwQBiG

@QwQBiG QwQBiG commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add an opt-in, runtime-agnostic asyncband::completion primitive with one non-cloneable Completer and cloneable Completion observers
  • publish one value exactly once, wake all current waiters, retain it for late observers, and report an explicit closed state when the completer is dropped before completion
  • return ownership of rejected values after duplicate completion or when no observers remain
  • add public documentation, a shared-result example, contract and trait tests, race and cancellation regressions, and self-benchmarks

Closes #222.

Design Notes

Completion::wait(&self) -> Result<&T, WaitError> returns a borrow tied to the observer instead of imposing T: Clone or always exposing Arc<T>. This keeps the primitive's ownership policy minimal: callers can borrow the stored value directly, clone it when appropriate, or choose T = Arc<U> when independently owned shared results are needed.

The primitive remains distinct from both nearby APIs. oneshot transfers one value to one receiver, while completion retains one result for multiple current and future observers. OnceCell drives initialization through the cell API, while completion separates a producer-only capability from wait-only observer capabilities and reports producer closure explicitly. The shared_completion example demonstrates one worker publishing a result to two independent consumers and a late observer; expressing the same lifecycle with oneshot would require one channel per consumer, manual fan-out, and separate storage for late consumers.

Internally, Completion observers own the strong Arc<Shared<T>>, while Completer holds a Weak<Shared<T>>. This prevents the completer from retaining the shared state or a completed value after the final observer is gone. complete() temporarily upgrades the weak reference, and the mutex-protected observer count remains the disconnection authority so completion and final-observer drop linearize cleanly. The completed value is stored in OnceLock<T>, while the same small private mutex protects the Pending | Completed | Closed status, observer count, and WaitSet. WaitError::Closed specifically means that the completer was dropped without producing a value; it remains distinct from any error carried by the user-provided payload itself, such as T = Result<V, E>. Each wait() owns an independent WakerToken, so cancellation unregisters only that call. Waker replacement, cancellation cleanup, wake callbacks, and rejected payload destruction occur after releasing the internal lock. The existing wake_all behavior ensures that every remaining waiter is attempted even if one waker panics.

The internal module #[cfg] gates for arena, mutex, and waitset now include feature = "completion": completion uses the internal mutex directly and reaches Arena through WaitSet. These additions only make existing private implementation modules available when the public completion feature is enabled; they do not alter any other public feature combination or public behavior.

The benchmarks cover ready waits, pending cancellation, complete-then-wait, pending notification, and 1/2/4/8/32-observer fanout. They are self-benchmarks because the existing primitives do not provide a semantically equivalent retained multi-observer result.

Validation included cargo x build --locked, the nightly feature-matrix cargo x check, full workspace tests on stable and Rust 1.86.0, cargo x bench, nightly Clippy and rustfmt with warnings denied, Taplo checks for every changed manifest, typos, Hawkeye license checks, and all-features rustdoc with warnings denied.

@tisonkun
tisonkun requested review from orthur2 and tisonkun August 29, 2026 16:23
@tisonkun

Copy link
Copy Markdown
Member

Thanks for your contribution! It's midnight now so I'd review this PR tomorrow or next week.

@QwQBiG

QwQBiG commented Aug 29, 2026

Copy link
Copy Markdown
Contributor Author

Thanks! No rush at all — have a good night :P

@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.

Nice work, and thanks for the contribution! I left one question and one small docs note inline.

///
/// The value is rejected and returned if another value has already completed the primitive or
/// if no observers remain.
pub fn complete(&self, value: T) -> Result<(), CompleteError<T>> {

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 document the panic behavior here? A waker can panic after the value has been committed, so complete() may panic even though completion succeeded.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch — I've added a # Panics section documenting that the value is committed before notification begins, so a waker panic can occur after completion succeeds. It also notes that complete() still attempts to wake the remaining registered wakers before resuming the panic.

Comment thread asyncband/src/completion/mod.rs Outdated
/// This type deliberately does not implement [`Clone`]. Dropping it before completion closes the
/// primitive and wakes all pending observers.
pub struct Completer<T> {
shared: Arc<Shared<T>>,

@orthur2 orthur2 Aug 30, 2026

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 Completer hold a Weak<Shared<T>> instead?

Once the last Completion is dropped, there is no way to create another observer, but the current strong Arc can still keep a completed value alive until the completer itself is dropped. Since the API already treats having no observers as disconnected, it seems more natural for the result to be released with the last Completion.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good point — I've switched Completer to Weak<Shared<T>>, while Completion observers retain the strong Arc. The mutex-protected observer count remains in place to linearize complete() against the final observer drop, and I added a regression test confirming that the completed payload is released with the last observer.

QwQBiG added 2 commits August 30, 2026 13:34
Signed-off-by: QwQBiG <baigaozi114514@gmail.com>
Signed-off-by: QwQBiG <baigaozi114514@gmail.com>
@QwQBiG

QwQBiG commented Aug 30, 2026

Copy link
Copy Markdown
Contributor Author

Nice work, and thanks for the contribution! I left one question and one small docs note inline.

Thanks again for the thoughtful review! The ownership-lifetime point was especially helpful and made the design cleaner. I've addressed both comments and would really appreciate another look when you have time. If you spot any other rough edges or opportunities to improve it, please don't hesitate to let me know ^w^

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.

Provide a shared one-shot completion primitive

3 participants