feat(completion): add a shared one-shot completion primitive - #240
feat(completion): add a shared one-shot completion primitive#240QwQBiG wants to merge 2 commits into
Conversation
|
Thanks for your contribution! It's midnight now so I'd review this PR tomorrow or next week. |
|
Thanks! No rush at all — have a good night :P |
orthur2
left a comment
There was a problem hiding this comment.
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>> { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| /// 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>>, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Signed-off-by: QwQBiG <baigaozi114514@gmail.com>
Signed-off-by: QwQBiG <baigaozi114514@gmail.com>
a7d2c3e to
4290d5e
Compare
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^ |
Summary
asyncband::completionprimitive with one non-cloneableCompleterand cloneableCompletionobserversCloses #222.
Design Notes
Completion::wait(&self) -> Result<&T, WaitError>returns a borrow tied to the observer instead of imposingT: Cloneor always exposingArc<T>. This keeps the primitive's ownership policy minimal: callers can borrow the stored value directly, clone it when appropriate, or chooseT = Arc<U>when independently owned shared results are needed.The primitive remains distinct from both nearby APIs.
oneshottransfers one value to one receiver, while completion retains one result for multiple current and future observers.OnceCelldrives initialization through the cell API, while completion separates a producer-only capability from wait-only observer capabilities and reports producer closure explicitly. Theshared_completionexample 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,
Completionobservers own the strongArc<Shared<T>>, whileCompleterholds aWeak<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 inOnceLock<T>, while the same small private mutex protects thePending | Completed | Closedstatus, observer count, andWaitSet.WaitError::Closedspecifically means that the completer was dropped without producing a value; it remains distinct from any error carried by the user-provided payload itself, such asT = Result<V, E>. Eachwait()owns an independentWakerToken, so cancellation unregisters only that call. Waker replacement, cancellation cleanup, wake callbacks, and rejected payload destruction occur after releasing the internal lock. The existingwake_allbehavior ensures that every remaining waiter is attempted even if one waker panics.The internal module
#[cfg]gates forarena,mutex, andwaitsetnow includefeature = "completion": completion uses the internal mutex directly and reachesArenathroughWaitSet. These additions only make existing private implementation modules available when the publiccompletionfeature 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-matrixcargo 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.