Skip to content

Provide an auto-reset event #292

Description

@tisonkun

Summary

Add a runtime-agnostic asyncband::event::AutoResetEvent under the existing event feature. It should retain at most one unassigned signal and release one waiter per delivered signal, consuming the signal automatically when a wait succeeds.

This fills the single-permit notification gap identified in the primitive survey. Related: #218. The manual-reset event issue #221 explicitly excluded auto-reset and Tokio-style single-permit notification semantics.

Motivation

A background worker or a serialized drain operation may need a payload-free "recheck the predicate" signal. Notifications must survive the interval between checking the external state and registering a wait, while repeated notifications without a consumer can coalesce.

The existing primitives represent different contracts:

  • Condvar does not retain notifications and coordinates waiting with a mutex-protected predicate.
  • ManualResetEvent releases all registered waits and remains set for future waits until explicitly reset.
  • Semaphore counts permits and normally returns an acquired permit when its guard is dropped.
  • watch<T> retains a value and tracks changes independently for each receiver.

An auto-reset event offers one consumable signal without a payload, version tracking, or a permit-return lifecycle. It is useful for simplifying notification plumbing; any performance benefit should be measured separately.

Proposed initial API

AutoResetEvent::new()
AutoResetEvent::with_state(bool)
event.set()
event.try_wait() -> bool
event.wait().await
event.wait_owned().await // through Arc<AutoResetEvent>

Keep AutoResetEvent and ManualResetEvent as separate public types. The initial API need not include reset() or is_set(): try_wait() provides an atomic consuming operation, while manual reset would require an additional contract for signals already assigned to waiters.

Proposed contract and design questions

  • new() starts unset. with_state(true) starts with one stored signal.
  • If eligible waiters are queued, set() assigns a signal to one waiter. Prefer FIFO selection by registration order and define whether try_wait() can bypass queued waits.
  • Otherwise, set() stores one signal. Further sets while that unassigned signal is stored coalesce rather than accumulating a count.
  • A successful wait or try_wait() consumes one signal; completion or dropping the completed future does not return it.
  • Distinguish a stored signal from a signal already assigned to a waiter. Specify the outcome of repeated sets when selected waiters have been woken but not polled again.
  • Define the waiter-registration point, including futures created but never polled. Coordinate state checks and registration so a concurrent set cannot leave a registered waiter asleep with an available signal.
  • Cancelling an unselected pending wait removes only that registration. If a selected waiter is cancelled before returning Ready, transfer its signal to another eligible waiter or restore the stored signal, coalescing with any signal already stored.
  • Define the memory-publication guarantee between signaling and successful consumption, including repeated/coalesced sets.

A mutex-protected state and WaitList are a reasonable baseline. The existing Condvar::notify_one cancellation handoff is relevant design experience. Waker clone, drop, and wake callbacks must remain outside internal state locks, including on cancellation paths.

Usage boundaries

For a single async observer, a predicate loop can use:

while !predicate() {
    changed.wait().await;
}

// On a path that may make the predicate true:
publish_state();
changed.set();

The predicate remains the source of truth, and every relevant state transition must signal. A stale signal may cause an extra predicate check.

This is not automatically a replacement for multi-observer watch or manual-reset events: one signal only releases one waiter. Examples involving drain generations must make the single-observer invariant explicit. Multiple consumers of external state may require an explicit registration protocol; document the supported pattern rather than promising that this loop works for every consumer topology.

Acceptance criteria

  • Document stored-signal coalescing, signal assignment and consumption, registration, fairness, memory publication, and cancellation.
  • Cover set-before-wait, repeated sets, competing waiters, registration races, and cancellation before and after signal assignment with deterministic regression tests.
  • Verify that cancellation neither loses an assigned signal nor retains stale wakers indefinitely.
  • Support borrowed and owned waits consistently with ManualResetEvent, without executor or timer dependencies.
  • Add public examples and update the API map and changelog when the feature is implemented; validate through the repository's cargo x workflows.

Prior art

  • .NET AutoResetEvent: a reusable signal consumed by one waiter.
  • Tokio Notify: single-permit notification, with useful documentation on coalescing and multi-consumer registration races. This proposal does not require its broadcast operations or the full Notify API.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions