pub(crate) struct ReleaseTimer {
inner: Arc<Inner>,
thread: Option<JoinHandle<()>>,
}Expand description
An owned release wheel: one OS thread, one heap, one parker.
The process-wide wheel is one of these in a OnceLock. Instances
created directly (this module’s tests today, per-socket owners later)
join their thread on drop — unless the drop is itself running on that
thread, which the Drop impl below explains.
Fields§
§inner: Arc<Inner>§thread: Option<JoinHandle<()>>Implementations§
Source§impl ReleaseTimer
impl ReleaseTimer
Sourcepub(crate) fn new() -> Self
pub(crate) fn new() -> Self
Resolve a backend and start the release thread.
Honours MOQTAP_RELEASE_TIMER (slice | condvar), read once,
here. Unset or unrecognised means slice. Cannot fail, and
that is a property of the design rather than an omission: there is
no OS object to fail to create, so there is no fallback ladder and
no error path to test.
Sourcepub(crate) fn arm(&self, at: Instant) -> Deadline
pub(crate) fn arm(&self, at: Instant) -> Deadline
Register at. at <= now yields an already-cancelled Deadline
without touching the heap or waking the thread.
Sourcepub(crate) fn backend(&self) -> TimerBackend
pub(crate) fn backend(&self) -> TimerBackend
Which primitive this wheel’s thread waits on.
Trait Implementations§
Source§impl Drop for ReleaseTimer
Sets stopping, wakes the thread, and joins it — except when the drop
is itself running on that thread, where it detaches instead. Pending
deadlines are abandoned, not fired: a dropped wheel has no listeners
left.
impl Drop for ReleaseTimer
Sets stopping, wakes the thread, and joins it — except when the drop
is itself running on that thread, where it detaches instead. Pending
deadlines are abandoned, not fired: a dropped wheel has no listeners
left.
§Why the join is conditional
The join is cheap only from another thread. Measured 0.097 ms with a
10 s deadline armed, because the thread never waits longer than one
SLICE and so reads stopping promptly — but that measurement was
taken with the wheel dropped from a thread other than its own release
thread, and it licenses nothing about the other case. It was the only
case anything could reach while the never-dropped process-wide wheel
was the sole non-test owner; a wheel owned per socket is droppable
from anywhere.
From the release thread the join is not a slow path, it is a permanent
deadlock. Step 3 of run cancels tokens outside the lock and on
the release thread, so every waker registered on a deadline — which is
what awaiting token().cancelled() installs — runs there too. A waker
that drops the last handle to the wheel enters this function on the
release thread, and join() then waits for the thread executing it.
stopping and notify_all do not rescue it: the thread is inside the
cancel loop and can never return to the top of run to read the flag.
Measured that way, the drop had not returned after 20 s against a
500 µs SLICE, on every run.
Detaching there is not a compromise, it is the only outcome that
terminates. stopping is set and notify_all sent before the branch,
so the detached thread finishes the cancel loop it is in, sees the flag
on its next iteration and returns on its own: a bounded, self-clearing
thread rather than an unbounded hang. The handle already carries the
id spawn gave the thread, so no separate field is needed to
recognise it.