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;