windows_ioring_sys/completion_wake.rs
1// Copyright (c) 2026 Mike Grier
2//! Wake-only Model A delivery (M33.1, [D-84](../DESIGN-NOTES.md#d-84)): the ring's completion event
3//! wired to a thread-pool wait that tells the ring's owner to drain, for a caller that keeps its
4//! ring.
5//!
6//! [`EventDelivery`](crate::EventDelivery) is built on this, so the protocol below is written once.
7
8use std::io;
9
10use windows_threadpool_sys::callback_env::CallbackEnviron;
11use windows_threadpool_sys::wait::{ThreadpoolWait, WaitableHandle};
12
13use crate::ring::IoRing;
14
15/// Tells the owner of an [`IoRing`] to drain it, on a thread-pool thread, whenever the ring's
16/// completion queue may have gone from empty to non-empty (Model A, D-3; D-84).
17///
18/// The counterpart to [`EventDelivery`](crate::EventDelivery) for a caller that keeps its ring --
19/// behind its own lock, beside its own state -- and wants the thread pool only to say when to
20/// drain. `EventDelivery` is this, with a drain that hands each completion to a callback, over a
21/// ring it owns.
22///
23/// # What the owner owes
24///
25/// - **Drain to empty on every call.** The ring's event is an edge, raised when the completion
26/// queue goes from empty to non-empty ([D-19](../DESIGN-NOTES.md#d-19)), so a call that leaves a
27/// completion queued may never be followed by another.
28/// - **Treat a call with nothing to pop as normal.** There is one at setup, and each activation
29/// calls twice.
30/// - **One waiter per ring** ([D-21](../DESIGN-NOTES.md#d-21)): no second wake over the same ring,
31/// and no other wait on its event. Stated, not enforced, because the wake does not hold the ring.
32/// - **Never drop the wake from inside its own call.** Dropping it waits for a running call, which
33/// would be waiting for itself.
34///
35/// # What the owner may rely on
36///
37/// Both are contract, as they are for `EventDelivery` ([D-83](../DESIGN-NOTES.md#d-83)):
38///
39/// - **No lock and no ring is held while it calls.** The wake holds no reference to the ring, so
40/// the owner may take its own lock in the call, pop, and push and submit more work.
41/// - **Calls may overlap.** Two may run at once on different pool threads; the owner's own lock is
42/// what serializes its ring.
43pub struct CompletionWake {
44 wait: ThreadpoolWait,
45}
46
47impl CompletionWake {
48 /// Wire `ring`'s completion event to a thread-pool wait that calls `on_wake` on a pool thread
49 /// whenever the ring's completion queue may have gone from empty to non-empty, and once at
50 /// setup, so a backlog already queued is reached.
51 ///
52 /// The protocol is `EventDelivery`'s: the event is attached unsignalled, the wait armed, and
53 /// only then the setup signal raised -- the order `SetThreadpoolWait` documents ("you must
54 /// re-register the event with the wait object before signaling it each time to trigger the
55 /// wait callback"); and each activation calls `on_wake`, re-arms, and calls `on_wake` again,
56 /// the second call closing the gap a completion arriving before the re-arm would leave
57 /// (`M4.2`, `M26.9`, `M26.12`).
58 ///
59 /// `env` binds the wait's pool -- to a NUMA node's, for instance; `None` is the process's
60 /// default pool.
61 ///
62 /// # Errors
63 ///
64 /// [`io::ErrorKind::Unsupported`] if the running system does not report
65 /// `IORING_FEATURE_SET_COMPLETION_EVENT`; this crate does not substitute a polling thread.
66 /// Also any error from attaching the ring's completion event, from `ThreadpoolWait::new`, or
67 /// from raising the setup signal.
68 pub fn new<T, X, K, F>(
69 ring: &mut IoRing<T, X, K>,
70 on_wake: F,
71 env: Option<&mut CallbackEnviron<'_>>,
72 ) -> io::Result<Self>
73 where
74 F: Fn() + Send + Sync + 'static,
75 {
76 // The ring creates, owns, and attaches its own event and hands back a duplicate (D-20),
77 // which leaves exactly one `SetIoRingCompletionEvent` call site in this crate. It is taken
78 // *unsignalled*, and the setup signal raised only after `wait.arm` below.
79 let event = ring.attach_completion_event_unsignalled()?;
80 windows_threadpool_sys::trace_record!(
81 "delivery",
82 "event-attached",
83 std::os::windows::io::AsRawHandle::as_raw_handle(&event) as usize
84 );
85 // SAFETY: `attach_completion_event_unsignalled` returns a duplicate of an auto-reset
86 // event -- always a supported wait target, and never a mutex -- and that duplicate is
87 // exclusively ours. The ring keeps its own separate handle, so nothing else closes this
88 // one while a wait is pending on it, and the ring goes on signalling whichever copies
89 // survive.
90 let event = unsafe { WaitableHandle::assume_waitable(event) };
91 let wait = ThreadpoolWait::new(
92 event,
93 move |activation| {
94 // Call, re-arm, call again: the event auto-resets on wait and is set whenever a
95 // completion lands, so the only gap this leaves is a completion arriving between
96 // the owner's last pop and the re-arm -- and the second call closes exactly that
97 // gap (M4.2).
98 windows_threadpool_sys::trace_record!("delivery", "callback-entered");
99 on_wake();
100 activation.rearm(None);
101 on_wake();
102 windows_threadpool_sys::trace_record!("delivery", "callback-left");
103 },
104 env,
105 )?;
106 wait.arm(None);
107 windows_threadpool_sys::trace_record!("delivery", "armed");
108
109 // Only now, with the wait registered, is the setup signal raised -- the order
110 // `SetThreadpoolWait` documents. This is what makes a backlog reachable, so a failure to
111 // raise it is a failure to construct. Raised unconditionally, whoever attached the event
112 // first (`M26.12`).
113 ring.raise_setup_signal()?;
114
115 Ok(Self { wait })
116 }
117}
118
119impl Drop for CompletionWake {
120 fn drop(&mut self) {
121 // The wait is armed, so it owes a drain. Discharged here, before the wait is released:
122 // `windows-threadpool-sys` reports an undischarged one and, under its `fail-fast`
123 // feature, panics on it. On return no call is running and none will start.
124 //
125 // Leaving it to the field's own drop would change no behaviour -- `ThreadpoolWait`'s
126 // `Drop` disarms and drains too -- only the report, which is why the sabotage that does
127 // so is declared an equivalent mutant rather than a gap.
128 self.wait.stop_and_drain();
129 }
130}