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;
10use windows_threadpool_sys::wait::{ThreadpoolWait, WaitableHandle};
11
12use crate::batch::Batch;
13use crate::capability::RingVersion;
14use crate::ring::{Completion, IoRing, Op, RingInfo};
15/// Pop every completion currently available and hand each to `on_completion`.
16///
17/// Each pop is its own short lock: `on_completion` always runs with the
18/// mutex released, so a slow callback does not block a submitter, and a
19/// callback that calls [`EventDelivery::ring`] and locks it itself cannot
20/// deadlock against this loop.
21fn drain(ring: &Mutex<IoRing>, on_completion: &(dyn Fn(Completion) + Send + Sync)) {
22    loop {
23        let popped = {
24            let mut ring = ring
25                .lock()
26                .unwrap_or_else(std::sync::PoisonError::into_inner);
27            ring.try_pop()
28        };
29        match popped {
30            Ok(Some(completion)) => on_completion(completion),
31            Ok(None) => break,
32            Err(error) => {
33                debug_assert!(
34                    false,
35                    "IoRing::try_pop failed during event-driven drain: {error}"
36                );
37                break;
38            }
39        }
40    }
41}
42
43/// Delivers an [`IoRing`]'s completions on thread-pool callback threads via
44/// its completion event, rather than a caller polling [`IoRing::try_pop`]
45/// itself (Model A, D-3).
46///
47/// # Not a `CleanupGroup` member
48///
49/// There is no way to add an already-built `EventDelivery` to a
50/// [`CleanupGroup`](windows_threadpool_sys::cleanup_group::CleanupGroup), for
51/// the same reason `CleanupGroup` excludes
52/// [`ThreadpoolIo`](windows_threadpool_sys::io::ThreadpoolIo): the ring must
53/// run down its outstanding operations before closing, and a group's bulk
54/// `CloseThreadpoolCleanupGroupMembers` has no way to run that logic for a
55/// member it did not create itself. `EventDelivery` stays individually
56/// owned, where its own field-drop order (below) gives the same
57/// quiesce-then-close guarantee a group would otherwise provide.
58pub struct EventDelivery {
59    // Drop order matters and is why these fields are declared in this order:
60    // Rust drops struct fields top-to-bottom. `wait` must go first -- its own
61    // `Drop` disarms, suppresses re-arming, and drains any in-flight callback
62    // before releasing its captured `Arc<Mutex<IoRing>>` clone -- so that by
63    // the time `ring`'s last reference drops below and runs
64    // `IoRing::run_down` then `CloseIoRing`, no callback can still be
65    // touching it (M4.3).
66    #[allow(
67        dead_code,
68        reason = "held only for its Drop side effect and ordering relative to `ring`"
69    )]
70    wait: ThreadpoolWait,
71    ring: Arc<Mutex<IoRing>>,
72}
73
74impl EventDelivery {
75    /// Wire `ring`'s completion event to a thread-pool wait, delivering every
76    /// popped [`Completion`] to `on_completion` on a pool thread (M4.2).
77    ///
78    /// The wait is armed before this returns, so the calling thread never
79    /// waits for a completion itself (M4.4).
80    ///
81    /// # Completions already queued when `ring` is handed over
82    ///
83    /// They are delivered too -- but that is a guarantee this method has to
84    /// buy, not one it inherits, and saying so is the point of this section.
85    /// The ring's event is edge-triggered on the completion queue going empty
86    /// to non-empty ([`IoRing::completion_event`], D-19), so attaching to a
87    /// ring whose queue is *already* non-empty signals nothing, and no later
88    /// completion signals either, because the queue never returns to empty to
89    /// re-arm the edge. Such a backlog is stranded permanently.
90    ///
91    /// What closes that gap is the deliberate signal
92    /// [`IoRing::completion_event`] raises as it attaches: the first callback
93    /// then drains the backlog exactly as it would any other wakeup.
94    ///
95    /// This was false in the implementation, and asserted anyway in this
96    /// rustdoc, before M11.3 -- every test until then handed over a fresh
97    /// ring, so nothing contradicted it. A caller on an earlier version
98    /// cannot rely on the guarantee; `tests/event_delivery.rs` keeps the
99    /// repro that now holds it.
100    ///
101    /// # Errors
102    ///
103    /// Returns [`io::ErrorKind::Unsupported`] if the running system does not
104    /// report `IORING_FEATURE_SET_COMPLETION_EVENT` (M4.1), which
105    /// [`IoRing::completion_event`] is what decides. This crate refuses to
106    /// silently substitute a thread-based polling loop instead -- a caller
107    /// who asked for event-driven delivery and got a spun-up thread has been
108    /// told something false. Also returns any other error from
109    /// [`IoRing::completion_event`] or from `ThreadpoolWait::new`.
110    pub fn new<F>(
111        mut ring: IoRing,
112        on_completion: F,
113        env: Option<&mut CallbackEnviron<'_>>,
114    ) -> io::Result<Self>
115    where
116        F: Fn(Completion) + Send + Sync + 'static,
117    {
118        // The ring creates, owns, and attaches its own event and hands back a
119        // duplicate (D-20), which leaves exactly one
120        // `SetIoRingCompletionEvent` call site in this crate. Delegating also
121        // means the capability check, the `Unsupported` error, and the
122        // signal-once-on-attach that makes the backlog guarantee above true
123        // are each stated in one place rather than restated here.
124        let event = ring.completion_event()?;
125        // SAFETY: `completion_event` returns a duplicate of an auto-reset
126        // event -- always a supported wait target, and never a mutex -- and
127        // that duplicate is exclusively ours. The ring keeps its own separate
128        // handle, so nothing else closes this one while a wait is pending on
129        // it, and the ring goes on signalling whichever copies survive.
130        let event = unsafe { WaitableHandle::assume_waitable(event) };
131
132        let ring = Arc::new(Mutex::new(ring));
133        let ring_for_wait = Arc::clone(&ring);
134        let on_completion: Arc<dyn Fn(Completion) + Send + Sync> = Arc::new(on_completion);
135        let wait = ThreadpoolWait::new(
136            event,
137            move |activation| {
138                // Drain-to-empty, re-arm, drain-to-empty again: the event
139                // auto-resets on wait and is set whenever a completion
140                // lands, so the only gap this leaves is a completion that
141                // arrives between the last pop and the re-arm -- and the
142                // second drain closes exactly that gap (M4.2).
143                drain(&ring_for_wait, on_completion.as_ref());
144                activation.rearm(None);
145                drain(&ring_for_wait, on_completion.as_ref());
146            },
147            env,
148        )?;
149        wait.arm(None);
150
151        Ok(Self { wait, ring })
152    }
153
154    /// Lock the wrapped ring and return a scope for submitting work to it.
155    ///
156    /// A caller submits by opening a [`RingScope::batch`], the same ring the
157    /// wait callback locks to pop completions. Mutex poisoning is absorbed
158    /// here rather than surfaced: a panic elsewhere does not invalidate a ring
159    /// handle, and the callback's own drain already takes the same view, so
160    /// making every caller write `unwrap_or_else(PoisonError::into_inner)` only
161    /// invited the inconsistency this method removes.
162    ///
163    /// # What the scope deliberately withholds
164    ///
165    /// The rule is: **every read-only part of [`IoRing`], plus batch
166    /// construction -- and nothing that can retarget the ring or steal the
167    /// pool's completions.** So there is no `try_pop` (D-21 makes the pool the
168    /// single drainer; a second one breaks the drain-to-empty that re-arms the
169    /// edge), no `completion_event` (it hands back a duplicate of the event
170    /// this `EventDelivery` already waits on, giving two waiters on one ring),
171    /// no `run_down`, and above all **no `&mut IoRing`**.
172    ///
173    /// That last one is the point, and it is why this returns a scope rather
174    /// than the `&Mutex<IoRing>` it used to. Any `&mut IoRing` permits
175    /// whole-value assignment, so safe code could replace the ring while the
176    /// pool's wait stayed armed on the *original* ring's event -- measured, and
177    /// delivery stopped silently ([D-43](../DESIGN-NOTES.md#d-43)). Note a
178    /// `Deref`/`DerefMut` newtype would not have closed that, since
179    /// `*scope = ...` works through `DerefMut` just as well; nor would handing
180    /// a `&mut IoRing` to a closure.
181    ///
182    /// Replacing the ring is therefore refused at compile time:
183    ///
184    /// ```compile_fail
185    /// # use windows_ioring_sys::{EventDelivery, IoRing};
186    /// let delivery =
187    ///     EventDelivery::new(IoRing::new(8, 8).unwrap(), |_| {}, None).unwrap();
188    /// let mut scope = delivery.scope();
189    /// // No `DerefMut`, so there is no `&mut IoRing` to assign through.
190    /// *scope = IoRing::new(8, 8).unwrap();
191    /// ```
192    ///
193    /// The same setup doing the legitimate thing must still compile. A
194    /// `compile_fail` example passes on *any* error, including a typo in its
195    /// own setup, so this pair is what keeps the one above honest:
196    ///
197    /// ```
198    /// # use windows_ioring_sys::{EventDelivery, IoRing};
199    /// let delivery =
200    ///     EventDelivery::new(IoRing::new(8, 8).unwrap(), |_| {}, None).unwrap();
201    /// let scope = delivery.scope();
202    /// assert_eq!(scope.outstanding(), 0);
203    /// ```
204    #[must_use]
205    pub fn scope(&self) -> RingScope<'_> {
206        RingScope {
207            ring: self
208                .ring
209                .lock()
210                .unwrap_or_else(std::sync::PoisonError::into_inner),
211        }
212    }
213}
214
215/// Exclusive access to an [`EventDelivery`]'s ring, narrowed to what a
216/// submitting caller needs (M18.6).
217///
218/// Held for as long as the value lives, so keep it to the shortest scope that
219/// covers a submission: the wait callback needs the same lock to deliver
220/// completions.
221///
222/// See [`EventDelivery::scope`] for what this deliberately does not expose,
223/// and why handing out anything that yields a `&mut IoRing` would reopen
224/// [D-43](../DESIGN-NOTES.md#d-43).
225pub struct RingScope<'delivery> {
226    ring: MutexGuard<'delivery, IoRing>,
227}
228
229impl RingScope<'_> {
230    /// Open a [`Batch`] against the ring.
231    ///
232    /// The borrow is confined to the returned batch, so no `&mut IoRing`
233    /// escapes to the caller.
234    pub fn batch(&mut self) -> Batch<'_> {
235        Batch::new(&mut self.ring)
236    }
237
238    /// Operations submitted but not yet popped, as [`IoRing::outstanding`].
239    #[must_use]
240    pub fn outstanding(&self) -> usize {
241        self.ring.outstanding()
242    }
243
244    /// This ring's negotiated version, as [`IoRing::version`].
245    #[must_use]
246    pub fn version(&self) -> RingVersion {
247        self.ring.version()
248    }
249
250    /// Query the ring, as [`IoRing::info`].
251    ///
252    /// # Errors
253    ///
254    /// As [`IoRing::info`].
255    pub fn info(&self) -> io::Result<RingInfo> {
256        self.ring.info()
257    }
258
259    /// Whether this ring supports `op`, as [`IoRing::supports`].
260    #[must_use]
261    pub fn supports(&self, op: Op) -> bool {
262        self.ring.supports(op)
263    }
264
265    /// Whether this ring supports `op_code`, as [`IoRing::supports_raw`].
266    #[must_use]
267    pub fn supports_raw(&self, op_code: IORING_OP_CODE) -> bool {
268        self.ring.supports_raw(op_code)
269    }
270
271    /// Registered file count, as [`IoRing::registered_file_count`].
272    #[must_use]
273    pub fn registered_file_count(&self) -> u32 {
274        self.ring.registered_file_count()
275    }
276
277    /// Registered buffer count, as [`IoRing::registered_buffer_count`].
278    #[must_use]
279    pub fn registered_buffer_count(&self) -> u32 {
280        self.ring.registered_buffer_count()
281    }
282}
283
284#[cfg(test)]
285mod tests;