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;