Skip to main content

windows_ioring_sys/
event_delivery.rs

1// Copyright (c) 2026 Mike Grier
2//! Model A delivery: the ring's completion event wired to a thread-pool wait
3//! (M4).
4
5use std::io;
6use std::sync::{Arc, Mutex, MutexGuard};
7
8use windows_sys::Win32::Storage::FileSystem::IORING_OP_CODE;
9use windows_threadpool_sys::callback_env::CallbackEnviron;
10
11use crate::batch::Batch;
12use crate::capability::RingVersion;
13use crate::completion_wake::CompletionWake;
14use crate::ring::{Completion, IoRing, Op, RingInfo};
15/// What a delivery hands a caller for each completion: the completion, and
16/// whatever the ring was holding for it, exactly as [`IoRing::try_pop`]
17/// returns them.
18///
19/// The two `Option`s answer different questions, and a callback must not
20/// collapse them. The **outer** `None` means the push created no entry at all
21/// -- a `_raw` flush or cancel, or [`IoRing::push_raw`]. `Some((None, extra))`
22/// is an `_owned` push that never had a buffer, a flush or a cancellation, and
23/// its sidecar still arrives: discarding it on the inner `None` would lose
24/// which group of writes the flush belonged to. `try_pop`'s "What each `None`
25/// means" is the authoritative statement.
26type OnCompletion<T, X> = dyn Fn(Completion, Option<(Option<T>, X)>) + Send + Sync;
27
28/// Pop every completion currently available and hand each to `on_completion`.
29///
30/// Each pop is its own short lock: `on_completion` always runs with the
31/// mutex released, so a slow callback does not block a submitter, and a
32/// callback that calls [`EventDelivery::scope`] and submits cannot deadlock
33/// against this loop. That is contract, not just how this happens to work:
34/// [`EventDelivery::new`] states it, and [D-83](../DESIGN-NOTES.md#d-83)
35/// records it.
36fn drain<T, X>(ring: &Mutex<IoRing<T, X>>, on_completion: &OnCompletion<T, X>) {
37    loop {
38        let popped = {
39            let mut ring = ring
40                .lock()
41                .unwrap_or_else(std::sync::PoisonError::into_inner);
42            ring.try_pop()
43        };
44        match popped {
45            Ok(Some((completion, held))) => on_completion(completion, held),
46            Ok(None) => break,
47            Err(error) => {
48                debug_assert!(
49                    false,
50                    "IoRing::try_pop failed during event-driven drain: {error}"
51                );
52                break;
53            }
54        }
55    }
56}
57
58/// Delivers an [`IoRing`]'s completions on thread-pool callback threads via
59/// its completion event, rather than a caller polling [`IoRing::try_pop`]
60/// itself (Model A, D-3).
61///
62/// # Not a `CleanupGroup` member
63///
64/// There is no way to add an already-built `EventDelivery` to a
65/// [`CleanupGroup`](windows_threadpool_sys::cleanup_group::CleanupGroup), for
66/// the same reason `CleanupGroup` excludes
67/// [`ThreadpoolIo`](windows_threadpool_sys::io::ThreadpoolIo): the ring must
68/// run down its outstanding operations before closing, and a group's bulk
69/// `CloseThreadpoolCleanupGroupMembers` has no way to run that logic for a
70/// member it did not create itself. `EventDelivery` stays individually
71/// owned, where its own field-drop order (below) gives the same
72/// quiesce-then-close guarantee a group would otherwise provide.
73pub struct EventDelivery<T = (), X = ()> {
74    // Drop order matters and is why these fields are declared in this order:
75    // Rust drops struct fields top-to-bottom. `wake` must go first -- its own
76    // `Drop` disarms, suppresses re-arming, and drains any in-flight callback
77    // before releasing its captured `Arc<Mutex<IoRing>>` clone -- so that by
78    // the time `ring`'s last reference drops below and runs
79    // `IoRing::run_down` then `CloseIoRing`, no callback can still be
80    // touching it (M4.3).
81    #[expect(
82        dead_code,
83        reason = "held for its Drop, which stops the wait and drains it before the ring is released"
84    )]
85    wake: CompletionWake,
86    ring: Arc<Mutex<IoRing<T, X>>>,
87}
88
89impl<T: Send + 'static, X: Send + 'static> EventDelivery<T, X> {
90    /// Wire `ring`'s completion event to a thread-pool wait, delivering every
91    /// popped [`Completion`] to `on_completion` on a pool thread (M4.2).
92    ///
93    /// The wait is armed before this returns, so the calling thread never
94    /// waits for a completion itself (M4.4).
95    ///
96    /// # Completions already queued when `ring` is handed over
97    ///
98    /// They are delivered too -- but that is a guarantee this method has to
99    /// buy, not one it inherits, and saying so is the point of this section.
100    /// The ring's event is edge-triggered on the completion queue going empty
101    /// to non-empty ([`IoRing::completion_event`], D-19), so attaching to a
102    /// ring whose queue is *already* non-empty signals nothing, and no later
103    /// completion signals either, because the queue never returns to empty to
104    /// re-arm the edge. Such a backlog is stranded permanently.
105    ///
106    /// What closes that gap is a deliberate signal raised on the event once
107    /// it has been attached: the first callback then drains the backlog
108    /// exactly as it would any other wakeup.
109    ///
110    /// That signal is raised *after* the wait has been armed, which is the
111    /// order `SetThreadpoolWait` documents -- "you must re-register the event
112    /// with the wait object before signaling it each time to trigger the wait
113    /// callback". The ordering binds to that documented rule, not to any
114    /// observed tolerance for the other order. This method therefore attaches
115    /// the event unsignalled and raises the signal itself, rather than going
116    /// through [`IoRing::completion_event`], which raises the signal before it
117    /// returns and so leaves a caller no way to arm in between.
118    ///
119    /// This was false in the implementation, and asserted anyway in this
120    /// rustdoc, before M11.3 -- every test until then handed over a fresh
121    /// ring, so nothing contradicted it. A caller on an earlier version
122    /// cannot rely on the guarantee; `tests/event_delivery.rs` keeps the
123    /// repro that now holds it. The ordering above was wrong until M26.9, in
124    /// a way that stranded the backlog in roughly one run in a hundred. It
125    /// was wrong again until `M26.12`, which raised the signal only when this
126    /// method had itself attached the event: a caller that attached earlier
127    /// and consumed that signal lost its whole backlog, every time rather
128    /// than rarely.
129    ///
130    /// # What `on_completion` may rely on
131    ///
132    /// Two properties of the callback are part of this method's contract
133    /// ([D-83](../DESIGN-NOTES.md#d-83)), so a consumer may build on them:
134    ///
135    /// - **It runs with the ring unlocked.** Each completion is popped under
136    ///   the ring's lock and handed to `on_completion` after the lock is
137    ///   released, so the callback may call [`EventDelivery::scope`] and
138    ///   submit without deadlocking. This method hands the callback nothing
139    ///   to reach its `EventDelivery` by; the caller arranges that, with a
140    ///   `Weak` set after construction, for instance.
141    /// - **Invocations may overlap.** Two calls may run at once on different
142    ///   pool threads, and completions handed to concurrent calls carry no
143    ///   order relative to each other. A consumer that needs an order imposes
144    ///   it. The overlap comes from the delivery loop's shape -- drain, re-arm
145    ///   the wait, drain again -- since a completion arriving after the
146    ///   re-arm can start a second call while the first is still draining.
147    ///
148    /// Neither makes the ring a synchronization provider: its lock is its
149    /// own, and a callback holds it only for as long as a scope it opens.
150    ///
151    /// # Errors
152    ///
153    /// Returns [`io::ErrorKind::Unsupported`] if the running system does not
154    /// report `IORING_FEATURE_SET_COMPLETION_EVENT` (M4.1), which
155    /// [`IoRing::completion_event`] is what decides. This crate refuses to
156    /// silently substitute a thread-based polling loop instead -- a caller
157    /// who asked for event-driven delivery and got a spun-up thread has been
158    /// told something false. Also returns any other error from attaching the
159    /// ring's completion event, from `ThreadpoolWait::new`, or from raising
160    /// the setup signal.
161    pub fn new<F>(
162        ring: IoRing<T, X>,
163        on_completion: F,
164        env: Option<&mut CallbackEnviron<'_>>,
165    ) -> io::Result<Self>
166    where
167        F: Fn(Completion, Option<(Option<T>, X)>) + Send + Sync + 'static,
168    {
169        // The protocol -- attach the event unsignalled, arm the wait, raise the
170        // setup signal; drain, re-arm, drain -- is `CompletionWake`'s, written
171        // once there (D-84). This adds only the drain it calls, over a ring this
172        // delivery owns.
173        let on_completion: Arc<OnCompletion<T, X>> = Arc::new(on_completion);
174        let ring = Arc::new(Mutex::new(ring));
175        let ring_for_wake = Arc::clone(&ring);
176        // Built under the ring's lock, which the setup signal needs; a callback
177        // the signal starts waits for the lock until this returns.
178        let wake = CompletionWake::new(
179            &mut *ring
180                .lock()
181                .unwrap_or_else(std::sync::PoisonError::into_inner),
182            move || drain(&ring_for_wake, on_completion.as_ref()),
183            env,
184        )?;
185        Ok(Self { wake, ring })
186    }
187
188    /// Lock the wrapped ring and return a scope for submitting work to it.
189    ///
190    /// A caller submits by opening a [`RingScope::batch`], the same ring the
191    /// wait callback locks to pop completions. Mutex poisoning is absorbed
192    /// here rather than surfaced: a panic elsewhere does not invalidate a ring
193    /// handle, and the callback's own drain already takes the same view, so
194    /// making every caller write `unwrap_or_else(PoisonError::into_inner)` only
195    /// invited the inconsistency this method removes.
196    ///
197    /// # What the scope deliberately withholds
198    ///
199    /// The rule is: **every read-only part of [`IoRing`], plus batch
200    /// construction -- and nothing that can retarget the ring or steal the
201    /// pool's completions.** So there is no `try_pop` (D-21 makes the pool the
202    /// single drainer; a second one breaks the drain-to-empty that re-arms the
203    /// edge), no `completion_event` (it hands back a duplicate of the event
204    /// this `EventDelivery` already waits on, giving two waiters on one ring),
205    /// no `run_down`, and above all **no `&mut IoRing`**.
206    ///
207    /// That last one is the point, and it is why this returns a scope rather
208    /// than the `&Mutex<IoRing>` it used to. Any `&mut IoRing` permits
209    /// whole-value assignment, so safe code could replace the ring while the
210    /// pool's wait stayed armed on the *original* ring's event -- measured, and
211    /// delivery stopped silently ([D-43](../DESIGN-NOTES.md#d-43)). Note a
212    /// `Deref`/`DerefMut` newtype would not have closed that, since
213    /// `*scope = ...` works through `DerefMut` just as well; nor would handing
214    /// a `&mut IoRing` to a closure.
215    ///
216    /// Replacing the ring is therefore refused at compile time. The expected
217    /// error is pinned, though rustdoc enforces a pinned code only on a
218    /// nightly toolchain or under `RUSTC_BOOTSTRAP=1`, which CI's
219    /// `doctest-error-codes` job sets; on plain stable, the compiling twin
220    /// below is what keeps this example honest:
221    ///
222    /// ```compile_fail,E0614
223    /// # use windows_ioring_sys::{EventDelivery, IoRing};
224    /// let delivery =
225    ///     EventDelivery::new(IoRing::new(8, 8).unwrap(), |_, _| {}, None).unwrap();
226    /// let mut scope = delivery.scope();
227    /// // No `DerefMut`, so there is no `&mut IoRing` to assign through.
228    /// *scope = IoRing::new(8, 8).unwrap();
229    /// ```
230    ///
231    /// The same setup doing the legitimate thing must still compile. A
232    /// `compile_fail` example passes on *any* error, including a typo in its
233    /// own setup, so this pair is what keeps the one above honest:
234    ///
235    /// ```
236    /// # use windows_ioring_sys::{EventDelivery, IoRing};
237    /// let delivery =
238    ///     EventDelivery::new(IoRing::new(8, 8).unwrap(), |_, _| {}, None).unwrap();
239    /// let scope = delivery.scope();
240    /// assert_eq!(scope.outstanding(), 0);
241    /// ```
242    #[must_use]
243    pub fn scope(&self) -> RingScope<'_, T, X> {
244        RingScope {
245            ring: self
246                .ring
247                .lock()
248                .unwrap_or_else(std::sync::PoisonError::into_inner),
249        }
250    }
251}
252
253/// Exclusive access to an [`EventDelivery`]'s ring, narrowed to what a
254/// submitting caller needs (M18.6).
255///
256/// Held for as long as the value lives, so keep it to the shortest scope that
257/// covers a submission: the wait callback needs the same lock to deliver
258/// completions.
259///
260/// See [`EventDelivery::scope`] for what this deliberately does not expose,
261/// and why handing out anything that yields a `&mut IoRing` would reopen
262/// [D-43](../DESIGN-NOTES.md#d-43).
263pub struct RingScope<'delivery, T = (), X = ()> {
264    ring: MutexGuard<'delivery, IoRing<T, X>>,
265}
266
267impl<T, X> RingScope<'_, T, X> {
268    /// Open a [`Batch`] against the ring.
269    ///
270    /// The borrow is confined to the returned batch, so no `&mut IoRing`
271    /// escapes to the caller.
272    pub fn batch(&mut self) -> Batch<'_, T, X> {
273        Batch::new(&mut self.ring)
274    }
275
276    /// Operations submitted but not yet popped, as [`IoRing::outstanding`].
277    #[must_use]
278    pub fn outstanding(&self) -> usize {
279        self.ring.outstanding()
280    }
281
282    /// This ring's negotiated version, as [`IoRing::version`].
283    #[must_use]
284    pub fn version(&self) -> RingVersion {
285        self.ring.version()
286    }
287
288    /// Query the ring, as [`IoRing::info`].
289    ///
290    /// # Errors
291    ///
292    /// As [`IoRing::info`].
293    pub fn info(&self) -> io::Result<RingInfo> {
294        self.ring.info()
295    }
296
297    /// Whether this ring supports `op`, as [`IoRing::supports`].
298    #[must_use]
299    pub fn supports(&self, op: Op) -> bool {
300        self.ring.supports(op)
301    }
302
303    /// Whether this ring supports `op_code`, as [`IoRing::supports_raw`].
304    #[must_use]
305    pub fn supports_raw(&self, op_code: IORING_OP_CODE) -> bool {
306        self.ring.supports_raw(op_code)
307    }
308
309    /// Registered file count, as [`IoRing::registered_file_count`].
310    #[must_use]
311    pub fn registered_file_count(&self) -> u32 {
312        self.ring.registered_file_count()
313    }
314
315    /// Registered buffer count, as [`IoRing::registered_buffer_count`].
316    #[must_use]
317    pub fn registered_buffer_count(&self) -> u32 {
318        self.ring.registered_buffer_count()
319    }
320}
321
322#[cfg(test)]
323mod tests;