Background
Asyncband has notification and coordination building blocks, but none represents a resettable, level-triggered boolean signal:
- Condvar does not retain notifications; callers protect and recheck an external predicate.
- Semaphore retains a count of permits and releases a corresponding number of acquirers.
- Latch retains a monotonic countdown state and cannot be reset.
- Channel primitives transfer or retain values rather than exposing a value-free readiness gate.
A manual-reset event retains one boolean state. It starts unset, set makes it ready and wakes all current waiters, new waiters remain ready until reset, and reset makes subsequent waits block again. Python's asyncio.Event and .NET's ManualResetEventSlim provide established versions of this contract.
Typical uses include a service-ready gate, configuration-loaded state, pause/resume coordination, and a reusable condition that does not need an associated mutex or payload.
Tracked by #218.
Proposed minimum contract
The initial primitive should expose behavior equivalent to:
- construct an unset event, with an optional explicitly set initial state;
- query whether it is currently set;
- set the event idempotently;
- reset the event idempotently;
- wait asynchronously until a set operation applies to the waiter;
- support an owned wait form when the event is held through Arc, if this is consistent with other Asyncband primitives.
set releases every waiter registered for the current unset generation. Repeated calls to set while already set are coalesced. A waiter created while the event is set completes immediately.
The type should be named ManualResetEvent, or otherwise use documentation that makes the reset mode explicit. A generic Event name would make it too easy to confuse this contract with an auto-reset event or a one-permit notification primitive.
Design considerations
Set/reset linearization
The most important race is a rapid set followed by reset while registered waiters have been woken but have not yet been polled again.
Two contracts are possible:
- a pure level check, where those waiters can observe the reset and block again; or
- a generation-aware contract, where a waiter registered before set remains committed to completion even if reset happens before it is next polled.
The second contract better matches the statement that set releases all current waiters and avoids scheduler-dependent lost notifications. It requires the implementation to track a generation or an equivalent committed-waiter state rather than only an atomic boolean.
Waiter registration race
Registering a waker and checking the event state must be coordinated so a concurrent set cannot occur between the check and registration and leave the waiter asleep. Tests should cover set-before-register, set-during-register, and reset-after-wake cases.
Cancellation
Dropping a pending wait future should remove or invalidate only that waiter. It must not reset the event, consume a signal, or affect other waiters. Stale wakers must be reclaimed without requiring another state transition.
State query semantics
is_set is an observational snapshot, not a synchronization guarantee for a later operation. Documentation should discourage check-then-act patterns that assume the state cannot change before use.
Reuse and generation overflow
If a generation counter is used to distinguish set/reset cycles, wraparound must not cause a live waiter to miss a transition. The implementation may use wrapping arithmetic as long as it never relies on a global total ordering across a wrap.
Key trade-offs
- A single atomic flag and smaller state versus generation tracking that gives deterministic set/reset behavior.
- A short Event name versus the semantically explicit ManualResetEvent name.
- Borrowed waits only versus parity with owned wait futures elsewhere in Asyncband.
- Reusing internal latch or wait-set machinery versus keeping resettable-event invariants isolated.
Non-goals
This issue does not propose:
- an auto-reset event;
- Tokio-style Notify semantics with at most one stored permit;
- counted permits, which remain the role of Semaphore;
- a mutex-associated predicate, which remains the role of Condvar;
- timeout or timer integration;
- carrying a value with the notification.
Those contracts should not be hidden behind modes of one public type.
Acceptance criteria
- The public documentation defines the set, reset, repeated-set, and waiter-registration linearization behavior.
- Waiters registered before a set are released according to a deterministic contract even when reset races with their next poll.
- New waiters complete while set and block after reset.
- Cancelling one waiter has no effect on the event state or other waiters.
- Concurrent set/reset/wait registration has targeted regression tests for lost wake-ups and stale wakers.
- No executor, timer, or runtime-specific dependency is introduced.
- Repository build, test, lint, and formatting workflows pass through cargo x.
Background
Asyncband has notification and coordination building blocks, but none represents a resettable, level-triggered boolean signal:
A manual-reset event retains one boolean state. It starts unset, set makes it ready and wakes all current waiters, new waiters remain ready until reset, and reset makes subsequent waits block again. Python's asyncio.Event and .NET's ManualResetEventSlim provide established versions of this contract.
Typical uses include a service-ready gate, configuration-loaded state, pause/resume coordination, and a reusable condition that does not need an associated mutex or payload.
Tracked by #218.
Proposed minimum contract
The initial primitive should expose behavior equivalent to:
set releases every waiter registered for the current unset generation. Repeated calls to set while already set are coalesced. A waiter created while the event is set completes immediately.
The type should be named ManualResetEvent, or otherwise use documentation that makes the reset mode explicit. A generic Event name would make it too easy to confuse this contract with an auto-reset event or a one-permit notification primitive.
Design considerations
Set/reset linearization
The most important race is a rapid set followed by reset while registered waiters have been woken but have not yet been polled again.
Two contracts are possible:
The second contract better matches the statement that set releases all current waiters and avoids scheduler-dependent lost notifications. It requires the implementation to track a generation or an equivalent committed-waiter state rather than only an atomic boolean.
Waiter registration race
Registering a waker and checking the event state must be coordinated so a concurrent set cannot occur between the check and registration and leave the waiter asleep. Tests should cover set-before-register, set-during-register, and reset-after-wake cases.
Cancellation
Dropping a pending wait future should remove or invalidate only that waiter. It must not reset the event, consume a signal, or affect other waiters. Stale wakers must be reclaimed without requiring another state transition.
State query semantics
is_set is an observational snapshot, not a synchronization guarantee for a later operation. Documentation should discourage check-then-act patterns that assume the state cannot change before use.
Reuse and generation overflow
If a generation counter is used to distinguish set/reset cycles, wraparound must not cause a live waiter to miss a transition. The implementation may use wrapping arithmetic as long as it never relies on a global total ordering across a wrap.
Key trade-offs
Non-goals
This issue does not propose:
Those contracts should not be hidden behind modes of one public type.
Acceptance criteria