pub struct EventDelivery<T = (), X = ()> { /* private fields */ }Expand description
Delivers an IoRing’s completions on thread-pool callback threads via
its completion event, rather than a caller polling IoRing::try_pop
itself (Model A, D-3).
§Not a CleanupGroup member
There is no way to add an already-built EventDelivery to a
CleanupGroup, for
the same reason CleanupGroup excludes
ThreadpoolIo: the ring must
run down its outstanding operations before closing, and a group’s bulk
CloseThreadpoolCleanupGroupMembers has no way to run that logic for a
member it did not create itself. EventDelivery stays individually
owned, where its own field-drop order (below) gives the same
quiesce-then-close guarantee a group would otherwise provide.
Implementations§
Source§impl<T: Send + 'static, X: Send + 'static> EventDelivery<T, X>
impl<T: Send + 'static, X: Send + 'static> EventDelivery<T, X>
Sourcepub fn new<F>(
ring: IoRing<T, X>,
on_completion: F,
env: Option<&mut CallbackEnviron<'_>>,
) -> Result<Self>
pub fn new<F>( ring: IoRing<T, X>, on_completion: F, env: Option<&mut CallbackEnviron<'_>>, ) -> Result<Self>
Wire ring’s completion event to a thread-pool wait, delivering every
popped Completion to on_completion on a pool thread (M4.2).
The wait is armed before this returns, so the calling thread never waits for a completion itself (M4.4).
§Completions already queued when ring is handed over
They are delivered too – but that is a guarantee this method has to
buy, not one it inherits, and saying so is the point of this section.
The ring’s event is edge-triggered on the completion queue going empty
to non-empty (IoRing::completion_event, D-19), so attaching to a
ring whose queue is already non-empty signals nothing, and no later
completion signals either, because the queue never returns to empty to
re-arm the edge. Such a backlog is stranded permanently.
What closes that gap is a deliberate signal raised on the event once it has been attached: the first callback then drains the backlog exactly as it would any other wakeup.
That signal is raised after the wait has been armed, which is the
order SetThreadpoolWait documents – “you must re-register the event
with the wait object before signaling it each time to trigger the wait
callback”. The ordering binds to that documented rule, not to any
observed tolerance for the other order. This method therefore attaches
the event unsignalled and raises the signal itself, rather than going
through IoRing::completion_event, which raises the signal before it
returns and so leaves a caller no way to arm in between.
This was false in the implementation, and asserted anyway in this
rustdoc, before M11.3 – every test until then handed over a fresh
ring, so nothing contradicted it. A caller on an earlier version
cannot rely on the guarantee; tests/event_delivery.rs keeps the
repro that now holds it. The ordering above was wrong until M26.9, in
a way that stranded the backlog in roughly one run in a hundred. It
was wrong again until M26.12, which raised the signal only when this
method had itself attached the event: a caller that attached earlier
and consumed that signal lost its whole backlog, every time rather
than rarely.
§What on_completion may rely on
Two properties of the callback are part of this method’s contract (D-83), so a consumer may build on them:
- It runs with the ring unlocked. Each completion is popped under
the ring’s lock and handed to
on_completionafter the lock is released, so the callback may callEventDelivery::scopeand submit without deadlocking. This method hands the callback nothing to reach itsEventDeliveryby; the caller arranges that, with aWeakset after construction, for instance. - Invocations may overlap. Two calls may run at once on different pool threads, and completions handed to concurrent calls carry no order relative to each other. A consumer that needs an order imposes it. The overlap comes from the delivery loop’s shape – drain, re-arm the wait, drain again – since a completion arriving after the re-arm can start a second call while the first is still draining.
Neither makes the ring a synchronization provider: its lock is its own, and a callback holds it only for as long as a scope it opens.
§Errors
Returns io::ErrorKind::Unsupported if the running system does not
report IORING_FEATURE_SET_COMPLETION_EVENT (M4.1), which
IoRing::completion_event is what decides. This crate refuses to
silently substitute a thread-based polling loop instead – a caller
who asked for event-driven delivery and got a spun-up thread has been
told something false. Also returns any other error from attaching the
ring’s completion event, from ThreadpoolWait::new, or from raising
the setup signal.
Sourcepub fn scope(&self) -> RingScope<'_, T, X>
pub fn scope(&self) -> RingScope<'_, T, X>
Lock the wrapped ring and return a scope for submitting work to it.
A caller submits by opening a RingScope::batch, the same ring the
wait callback locks to pop completions. Mutex poisoning is absorbed
here rather than surfaced: a panic elsewhere does not invalidate a ring
handle, and the callback’s own drain already takes the same view, so
making every caller write unwrap_or_else(PoisonError::into_inner) only
invited the inconsistency this method removes.
§What the scope deliberately withholds
The rule is: every read-only part of IoRing, plus batch
construction – and nothing that can retarget the ring or steal the
pool’s completions. So there is no try_pop (D-21 makes the pool the
single drainer; a second one breaks the drain-to-empty that re-arms the
edge), no completion_event (it hands back a duplicate of the event
this EventDelivery already waits on, giving two waiters on one ring),
no run_down, and above all no &mut IoRing.
That last one is the point, and it is why this returns a scope rather
than the &Mutex<IoRing> it used to. Any &mut IoRing permits
whole-value assignment, so safe code could replace the ring while the
pool’s wait stayed armed on the original ring’s event – measured, and
delivery stopped silently (D-43). Note a
Deref/DerefMut newtype would not have closed that, since
*scope = ... works through DerefMut just as well; nor would handing
a &mut IoRing to a closure.
Replacing the ring is therefore refused at compile time. The expected
error is pinned, though rustdoc enforces a pinned code only on a
nightly toolchain or under RUSTC_BOOTSTRAP=1, which CI’s
doctest-error-codes job sets; on plain stable, the compiling twin
below is what keeps this example honest:
let delivery =
EventDelivery::new(IoRing::new(8, 8).unwrap(), |_, _| {}, None).unwrap();
let mut scope = delivery.scope();
// No `DerefMut`, so there is no `&mut IoRing` to assign through.
*scope = IoRing::new(8, 8).unwrap();The same setup doing the legitimate thing must still compile. A
compile_fail example passes on any error, including a typo in its
own setup, so this pair is what keeps the one above honest:
let delivery =
EventDelivery::new(IoRing::new(8, 8).unwrap(), |_, _| {}, None).unwrap();
let scope = delivery.scope();
assert_eq!(scope.outstanding(), 0);