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    /// `env` is [`windows_threadpool_sys::callback_env::CallbackEnviron`], the
152    /// type of the `windows-threadpool-sys` this crate depends on. Another
153    /// version of that crate has another `CallbackEnviron`, so a caller builds
154    /// the environment with the version this crate resolves to.
155    ///
156    /// # Errors
157    ///
158    /// Returns [`io::ErrorKind::Unsupported`] if the running system does not
159    /// report `IORING_FEATURE_SET_COMPLETION_EVENT` (M4.1), which
160    /// [`IoRing::completion_event`] is what decides. This crate refuses to
161    /// silently substitute a thread-based polling loop instead -- a caller
162    /// who asked for event-driven delivery and got a spun-up thread has been
163    /// told something false. Also returns any other error from attaching the
164    /// ring's completion event, from `ThreadpoolWait::new`, or from raising
165    /// the setup signal.
166    pub fn new<F>(
167        ring: IoRing<T, X>,
168        on_completion: F,
169        env: Option<&mut CallbackEnviron<'_>>,
170    ) -> io::Result<Self>
171    where
172        F: Fn(Completion, Option<(Option<T>, X)>) + Send + Sync + 'static,
173    {
174        // The protocol -- attach the event unsignalled, arm the wait, raise the
175        // setup signal; drain, re-arm, drain -- is `CompletionWake`'s, written
176        // once there (D-84). This adds only the drain it calls, over a ring this
177        // delivery owns.
178        let on_completion: Arc<OnCompletion<T, X>> = Arc::new(on_completion);
179        let ring = Arc::new(Mutex::new(ring));
180        let ring_for_wake = Arc::clone(&ring);
181        // Built under the ring's lock, which the setup signal needs; a callback
182        // the signal starts waits for the lock until this returns.
183        let wake = CompletionWake::new(
184            &mut *ring
185                .lock()
186                .unwrap_or_else(std::sync::PoisonError::into_inner),
187            move || drain(&ring_for_wake, on_completion.as_ref()),
188            env,
189        )?;
190        Ok(Self { wake, ring })
191    }
192
193    /// Lock the wrapped ring and return a scope for submitting work to it.
194    ///
195    /// A caller submits by opening a [`RingScope::batch`], the same ring the
196    /// wait callback locks to pop completions. Mutex poisoning is absorbed
197    /// here rather than surfaced: a panic elsewhere does not invalidate a ring
198    /// handle, and the callback's own drain already takes the same view, so
199    /// making every caller write `unwrap_or_else(PoisonError::into_inner)` only
200    /// invited the inconsistency this method removes.
201    ///
202    /// # What the scope deliberately withholds
203    ///
204    /// The rule is: **every read-only part of [`IoRing`], plus batch
205    /// construction -- and nothing that can retarget the ring or steal the
206    /// pool's completions.** So there is no `try_pop` (D-21 makes the pool the
207    /// single drainer; a second one breaks the drain-to-empty that re-arms the
208    /// edge), no `completion_event` (it hands back a duplicate of the event
209    /// this `EventDelivery` already waits on, giving two waiters on one ring),
210    /// no `run_down`, and above all **no `&mut IoRing`**.
211    ///
212    /// That last one is the point, and it is why this returns a scope rather
213    /// than the `&Mutex<IoRing>` it used to. Any `&mut IoRing` permits
214    /// whole-value assignment, so safe code could replace the ring while the
215    /// pool's wait stayed armed on the *original* ring's event -- measured, and
216    /// delivery stopped silently ([D-43](../DESIGN-NOTES.md#d-43)). Note a
217    /// `Deref`/`DerefMut` newtype would not have closed that, since
218    /// `*scope = ...` works through `DerefMut` just as well; nor would handing
219    /// a `&mut IoRing` to a closure.
220    ///
221    /// Replacing the ring is therefore refused at compile time. The expected
222    /// error is pinned, though rustdoc enforces a pinned code only on a
223    /// nightly toolchain or under `RUSTC_BOOTSTRAP=1`, which CI's
224    /// `doctest-error-codes` job sets; on plain stable, the compiling twin
225    /// below is what keeps this example honest:
226    ///
227    /// ```compile_fail,E0614
228    /// # use windows_ioring_sys::{EventDelivery, IoRing};
229    /// let delivery =
230    ///     EventDelivery::new(IoRing::new(8, 8).unwrap(), |_, _| {}, None).unwrap();
231    /// let mut scope = delivery.scope();
232    /// // No `DerefMut`, so there is no `&mut IoRing` to assign through.
233    /// *scope = IoRing::new(8, 8).unwrap();
234    /// ```
235    ///
236    /// The same setup doing the legitimate thing must still compile. A
237    /// `compile_fail` example passes on *any* error, including a typo in its
238    /// own setup, so this pair is what keeps the one above honest:
239    ///
240    /// ```
241    /// # use windows_ioring_sys::{EventDelivery, IoRing};
242    /// let delivery =
243    ///     EventDelivery::new(IoRing::new(8, 8).unwrap(), |_, _| {}, None).unwrap();
244    /// let scope = delivery.scope();
245    /// assert_eq!(scope.outstanding(), 0);
246    /// ```
247    #[must_use]
248    pub fn scope(&self) -> RingScope<'_, T, X> {
249        RingScope {
250            ring: self
251                .ring
252                .lock()
253                .unwrap_or_else(std::sync::PoisonError::into_inner),
254        }
255    }
256}
257
258/// Exclusive access to an [`EventDelivery`]'s ring, narrowed to what a
259/// submitting caller needs (M18.6).
260///
261/// Held for as long as the value lives, so keep it to the shortest scope that
262/// covers a submission: the wait callback needs the same lock to deliver
263/// completions.
264///
265/// See [`EventDelivery::scope`] for what this deliberately does not expose,
266/// and why handing out anything that yields a `&mut IoRing` would reopen
267/// [D-43](../DESIGN-NOTES.md#d-43).
268pub struct RingScope<'delivery, T = (), X = ()> {
269    ring: MutexGuard<'delivery, IoRing<T, X>>,
270}
271
272impl<T, X> RingScope<'_, T, X> {
273    /// Open a [`Batch`] against the ring.
274    ///
275    /// The borrow is confined to the returned batch, so no `&mut IoRing`
276    /// escapes to the caller.
277    pub fn batch(&mut self) -> Batch<'_, T, X> {
278        Batch::new(&mut self.ring)
279    }
280
281    /// Operations submitted but not yet popped, as [`IoRing::outstanding`].
282    #[must_use]
283    pub fn outstanding(&self) -> usize {
284        self.ring.outstanding()
285    }
286
287    /// This ring's negotiated version, as [`IoRing::version`].
288    #[must_use]
289    pub fn version(&self) -> RingVersion {
290        self.ring.version()
291    }
292
293    /// Query the ring, as [`IoRing::info`].
294    ///
295    /// # Errors
296    ///
297    /// As [`IoRing::info`].
298    pub fn info(&self) -> io::Result<RingInfo> {
299        self.ring.info()
300    }
301
302    /// Whether this ring supports `op`, as [`IoRing::supports`].
303    #[must_use]
304    pub fn supports(&self, op: Op) -> bool {
305        self.ring.supports(op)
306    }
307
308    /// Whether this ring supports `op_code`, as [`IoRing::supports_raw`].
309    #[must_use]
310    pub fn supports_raw(&self, op_code: IORING_OP_CODE) -> bool {
311        self.ring.supports_raw(op_code)
312    }
313
314    /// Registered file count, as [`IoRing::registered_file_count`].
315    #[must_use]
316    pub fn registered_file_count(&self) -> u32 {
317        self.ring.registered_file_count()
318    }
319
320    /// Registered buffer count, as [`IoRing::registered_buffer_count`].
321    #[must_use]
322    pub fn registered_buffer_count(&self) -> u32 {
323        self.ring.registered_buffer_count()
324    }
325}
326
327#[cfg(test)]
328mod tests;