Skip to main content

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}