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;