Skip to main content

EventDelivery

Struct EventDelivery 

Source
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>

Source

pub fn new<F>( ring: IoRing<T, X>, on_completion: F, env: Option<&mut CallbackEnviron<'_>>, ) -> Result<Self>
where F: Fn(Completion, Option<(Option<T>, X)>) + Send + Sync + 'static,

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_completion after the lock is released, so the callback may call EventDelivery::scope and submit without deadlocking. This method hands the callback nothing to reach its EventDelivery by; the caller arranges that, with a Weak set 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.

Source

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);

Auto Trait Implementations§

§

impl<T = (), X = ()> !RefUnwindSafe for EventDelivery<T, X>

§

impl<T = (), X = ()> !UnwindSafe for EventDelivery<T, X>

§

impl<T, X> Freeze for EventDelivery<T, X>
where Arc<Mutex<IoRing<T, X>>>: Freeze,

§

impl<T, X> Send for EventDelivery<T, X>
where Arc<Mutex<IoRing<T, X>>>: Send,

§

impl<T, X> Sync for EventDelivery<T, X>
where Arc<Mutex<IoRing<T, X>>>: Sync,

§

impl<T, X> Unpin for EventDelivery<T, X>
where Arc<Mutex<IoRing<T, X>>>: Unpin,

§

impl<T, X> UnsafeUnpin for EventDelivery<T, X>
where Arc<Mutex<IoRing<T, X>>>: UnsafeUnpin,

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.