windows_ioring_sys/contract.rs
1// Copyright (c) 2026 Mike Grier
2//! [`RingContract`](crate::contract::RingContract) -- this crate's conservation
3//! rules, made executable.
4//!
5//! # Why an oracle rather than more assertions
6//!
7//! Some of what this crate promises is a property of a *sequence*, not of any
8//! single value or call, and a per-value type cannot carry it. "Every SQE that
9//! successfully queues produces exactly one completion" is only observable by
10//! counting pushes against completions across a whole run, and so is "no
11//! registered buffer is still in use once everything has finished".
12//!
13//! Those rules are stated in `DESIGN-NOTES.md` and, before this module, were
14//! checked nowhere. That was not a hypothetical gap: two real defects in this
15//! repository were conservation failures. `Appender::claim` returned early on
16//! a failed write and permanently leaked the arena slot its token held, and a
17//! strategy harness shared one deferred-commit slot between two lanes so half
18//! its commits were never awaited. Both were found by review and by
19//! measurement respectively, and both would have fallen out of a quiescence
20//! check automatically. (The first was a token leak, and the rule that caught
21//! that kind of leak retired with the token API -- see below.)
22//!
23//! # Why it lives here
24//!
25//! The layer that owns an invariant owns the oracle for it. A copy written in
26//! a test harness is a second implementation of the rule rather than a check
27//! of it -- if the two disagree, the harness is what gets "fixed", and the
28//! disagreement is precisely the bug worth finding. Being public means a
29//! consumer can hold its own harness, its own test doubles, or a captured run
30//! to the same definition this crate's tests use.
31//!
32//! # What it checks
33//!
34//! | Rule | Where it comes from |
35//! |---|---|
36//! | Every queued SQE produces exactly one completion | [`DESIGN-NOTES.md`'s category-2 audit](../DESIGN-NOTES.md#one-sqe-one-completion) |
37//! | A push that failed synchronously produces none | the same section: it is un-counted, not merely uncompleted |
38//! | No completion arrives for an operation never pushed | corollary of the above |
39//! | Nothing is outstanding at quiescence | what `IoRing::run_down`'s termination depends on |
40//!
41//! Every rule here is about the **kernel**, not about a caller's bookkeeping.
42//! A fifth row used to sit above -- "every token is claimed, or deliberately
43//! leaked" -- and it retired with the token API: a push now hands back only an
44//! `OperationId`, which owns nothing, so there is nothing a caller could fail
45//! to claim. [D-74](../DESIGN-NOTES.md#d-74) records why that is a retirement
46//! rather than a narrowing.
47//!
48//! What replaced it is **not** `held() == outstanding()`. Those two are not
49//! equal in general and the ring does not compare them: a `_raw` flush or
50//! cancel is outstanding while deliberately stowing no inventory
51//! entry, so a perfectly valid push makes them differ. Reading equality as an
52//! invariant would report that valid push as a conservation failure, which is
53//! the over-constraining this oracle's next section exists to avoid.
54//!
55//! The owned-push invariant is narrower: **every push that stows an entry
56//! retires exactly that entry at the pop which observes its completion**, so
57//! what remains in the inventory is precisely the set of owned pushes whose
58//! completions have not yet been seen. [`IoRing::held`] counts that set, which
59//! is a subset of what [`IoRing::outstanding`] counts: `held() <= outstanding()`,
60//! and the gap is the raw operations in flight.
61//! Teardown checks the matching property rather than the equality: that rundown
62//! succeeded **and** no identity the ring minted is still in flight, which
63//! covers the inventory as well ([D-78](../DESIGN-NOTES.md#d-78)).
64//!
65//! # What it deliberately does **not** check
66//!
67//! Being explicit about this matters as much as the list above: over-
68//! constraining is the same defect as under-specifying, and an oracle that
69//! reports a violation which is not one trains its reader to ignore it.
70//!
71//! - **Completion order.** The ring makes no ordering promise between
72//! independent operations, so this counts them and never sequences them.
73//! There is one promised ordering -- a covering flush against the operations
74//! *preceding* it, and nothing about those following it
75//! ([D-24](../DESIGN-NOTES.md#d-24), as corrected by
76//! [D-47](../DESIGN-NOTES.md#d-47-detail)) -- and this oracle does not check
77//! it. Not because it is unobservable: tag the operations and it is entirely
78//! observable from the completion stream, which is exactly how
79//! [tests/flush_barrier.rs](../tests/flush_barrier.rs) tests it and how D-47
80//! measured the flag's one-sidedness over some 4,500 trials. It is that
81//! *this* oracle is a conservation checker -- it matches each completion
82//! against an outstanding claim and never relates that claim to the order
83//! anything was submitted in, so it holds nothing it could compare against.
84//!
85//! An observer that does want to check it needs tagged identities, so that a
86//! popped completion can be attributed to the operation it belongs to. Pop
87//! order itself is a faithful witness of the order the kernel *posted* those
88//! completions -- that is precisely the observable D-47 measured, and what
89//! this crate's contract is stated in terms of. What it does not witness is
90//! the order the operations *began executing*, which is invisible from user
91//! mode -- see [D-47](../DESIGN-NOTES.md#d-47-detail) for why the contract is
92//! stated in terms of completion order and stops there.
93//! - **Whether an operation succeeded.** A failed operation still completes
94//! exactly once; conservation and success are different questions.
95//! - **Anything about the device.** Whether a flush truly reached durable
96//! media is invisible here and everywhere else short of a power cut.
97//! - **That a caller pushed anything sensible.** This checks conservation of
98//! what was pushed, not that pushing it was a good idea.
99
100use std::collections::{HashMap, HashSet, VecDeque};
101use std::fmt;
102
103/// A conservation rule this crate promises, broken.
104#[derive(Clone, Debug, PartialEq, Eq)]
105pub enum Violation {
106 /// A completion arrived carrying `user_data` that no observed push
107 /// produced.
108 ///
109 /// Either the ring completed something twice, or the caller's own
110 /// bookkeeping lost a push -- and distinguishing those is why the message
111 /// says which is possible rather than asserting one.
112 ///
113 /// When the completions reported here come from an [`crate::IoRing`]'s own
114 /// pops, the ring has already checked the identity against what it minted
115 /// and panics on one that is not in flight
116 /// ([D-79](../DESIGN-NOTES.md#d-79)), so from such a ring this points at
117 /// the caller's reporting rather than at the kernel.
118 UnexpectedCompletion {
119 /// The identity the unrecognised completion carried.
120 user_data: usize,
121 },
122 /// Two completions arrived for the same `user_data`.
123 ///
124 /// The "exactly" half of "exactly one completion". Counted separately from
125 /// [`Violation::UnexpectedCompletion`] because a duplicate is a much
126 /// stronger signal than an unrecognised identity: it cannot be explained
127 /// by a caller forgetting to report a push. A ring's own pop panics on a
128 /// duplicate first ([D-79](../DESIGN-NOTES.md#d-79)), so from a real ring
129 /// this means the caller reported one completion twice.
130 DuplicateCompletion {
131 /// The identity that completed more than once.
132 user_data: usize,
133 },
134 /// An operation was pushed and never completed.
135 Outstanding {
136 /// The identity that was pushed and never completed.
137 user_data: usize,
138 },
139 /// A registered buffer still had operations outstanding at quiescence.
140 BufferStillInUse {
141 /// Position of the buffer within its registration.
142 index: u32,
143 /// How many operations it still had in flight.
144 outstanding: usize,
145 },
146}
147
148impl fmt::Display for Violation {
149 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150 match self {
151 Self::UnexpectedCompletion { user_data } => write!(
152 f,
153 "completion for user_data {user_data:#x} matches no observed push: either it \
154 completed twice, or a push was not reported to the contract"
155 ),
156 Self::DuplicateCompletion { user_data } => write!(
157 f,
158 "user_data {user_data:#x} completed more than once, but every queued SQE \
159 produces exactly one completion"
160 ),
161 Self::Outstanding { user_data } => {
162 write!(f, "user_data {user_data:#x} was pushed and never completed")
163 }
164 Self::BufferStillInUse { index, outstanding } => write!(
165 f,
166 "registered buffer {index} still has {outstanding} operation(s) outstanding at \
167 quiescence"
168 ),
169 }
170 }
171}
172
173/// What one operation *still being tracked* is known to have done so far.
174///
175/// Terminal outcomes are deliberately absent. An operation that finishes --
176/// which since [D-74](../DESIGN-NOTES.md#d-74) means simply that its
177/// completion was popped -- leaves this map entirely and its identity moves
178/// to the bounded history described
179/// on [`RingContract`], because retaining a terminal entry per operation is
180/// what made the oracle grow without limit (M28.2).
181#[derive(Clone, Copy, Debug, PartialEq, Eq)]
182enum State {
183 /// Queued, no completion seen.
184 ///
185 /// **One push state, as of `D-74`.** There were two, because a `_raw`
186 /// push returned a bare `user_data` while the others returned a
187 /// `Token`, and conflating them reported a leak for every
188 /// tokenless push -- a violation the caller could not satisfy, since
189 /// there was no token to claim. With the ring holding what an operation
190 /// carries, no push hands the caller anything to lose, so the distinction
191 /// has nothing left to distinguish.
192 Pushed,
193}
194
195/// How many finished identities the oracle remembers for duplicate diagnosis.
196///
197/// A **diagnostic window, not a correctness parameter.** Nothing is missed
198/// when a duplicate falls outside it: the completion is still reported, as
199/// [`Violation::UnexpectedCompletion`] rather than
200/// [`Violation::DuplicateCompletion`]. What the window buys is the *stronger*
201/// of the two claims, which the variants' own docs explain is worth keeping --
202/// a duplicate cannot be explained away by a caller forgetting to report a
203/// push, and an unrecognised identity can.
204///
205/// Changing it is not a breaking change. It trades a fixed amount of memory
206/// for how far back a duplicate is still named precisely.
207const FINISHED_HISTORY: usize = 1024;
208
209/// An executable form of this crate's conservation rules.
210///
211/// Fed by the caller rather than wired into [`crate::Batch`], for two reasons.
212/// A ring can be driven through `push_raw` and the `_raw` entry points without
213/// any of this crate's bookkeeping being involved at all, so an internal hook
214/// would silently cover less than it appears to. And a consumer validating its
215/// own harness needs to drive the same rules from outside.
216///
217/// # What this costs to run, which is bounded (M28.2)
218///
219/// Memory here is a function of **operations in flight**, not of operations
220/// ever performed. An operation that finishes leaves the tracking map, so a
221/// consumer that pushes and claims forever holds a map whose size follows its
222/// own concurrency rather than its uptime.
223///
224/// That was not true before M28.2: a claimed operation was marked `Completed`
225/// and kept, so the oracle retained one entry per operation for the life of
226/// the process. The sample never showed it because it appends 24 records, and
227/// nothing documented it -- so a long-running consumer following this crate's
228/// own recommendation grew without limit. It is also why an always-on checked
229/// inventory was ruled out in `M23.3`, since a check that cannot be left
230/// running is not a check.
231///
232/// Retiring an entry would lose the ability to call a later completion for it
233/// a *duplicate* rather than merely unrecognised, so the last
234/// `FINISHED_HISTORY` finished identities are remembered for that purpose
235/// alone. Beyond that window a duplicate is still reported, under the weaker
236/// name.
237///
238/// # Example
239///
240/// ```
241/// use windows_ioring_sys::contract::RingContract;
242///
243/// let mut contract = RingContract::new();
244/// contract.observe_push(0x1234);
245/// contract.observe_completion(0x1234);
246/// assert!(contract.check_quiescent().is_empty());
247/// ```
248#[derive(Debug, Default)]
249pub struct RingContract {
250 /// Operations still being tracked. Terminal outcomes are not kept here;
251 /// see the type's own documentation for why.
252 operations: HashMap<usize, State>,
253 /// Identities that finished, most recent last, capped at
254 /// [`FINISHED_HISTORY`]. Paired with `finished_set` so the membership test
255 /// stays O(1) -- the queue exists only to know which one to evict.
256 finished: VecDeque<usize>,
257 /// Membership index over `finished`, held in step with it.
258 finished_set: HashSet<usize>,
259 /// Per registered-buffer outstanding counts, as the caller reports them.
260 buffers: HashMap<u32, usize>,
261 violations: Vec<Violation>,
262}
263
264impl RingContract {
265 /// A contract with nothing observed yet.
266 #[must_use]
267 pub fn new() -> Self {
268 Self::default()
269 }
270
271 /// Record that an SQE was successfully queued with this `user_data`.
272 ///
273 /// Report only pushes that **queued**. A `Build*` call that failed
274 /// synchronously releases its reservation and produces no completion, so
275 /// reporting it would manufacture an [`Violation::Outstanding`] at
276 /// teardown.
277 ///
278 /// There is one push observer, as of `M28.5`. There were two:
279 /// `observe_push` recorded a push that returned a token, and
280 /// `observe_tokenless_push` recorded an SQE that carried no token to
281 /// claim -- the `_raw` flush and cancel entry points -- because reporting
282 /// one through `observe_push` produced a leak violation its caller had no
283 /// way to satisfy, there being no token to claim. [D-74](../DESIGN-NOTES.md#d-74)
284 /// removed the leak rule, which made the two identical in body as well as
285 /// in purpose, and two names for one behaviour is a restatement waiting
286 /// for the next change to reach only one of them.
287 pub fn observe_push(&mut self, user_data: usize) {
288 self.operations.insert(user_data, State::Pushed);
289 }
290
291 /// Record a completion popped from the ring.
292 pub fn observe_completion(&mut self, user_data: usize) {
293 match self.operations.get(&user_data) {
294 None => {
295 // Not tracked. Either it finished already -- which the
296 // history can still prove, and which is the stronger claim --
297 // or no observed push ever produced it.
298 if self.finished_set.contains(&user_data) {
299 self.violations
300 .push(Violation::DuplicateCompletion { user_data });
301 } else {
302 self.violations
303 .push(Violation::UnexpectedCompletion { user_data });
304 }
305 }
306 Some(State::Pushed) => {
307 // Terminal. A completion used to be *provisional* here,
308 // parked in a leaked state until `observe_claim` corrected
309 // it, because a completion whose token was never claimed was
310 // a real leak. The pop is the claim now (`D-74`), so there is
311 // no second step for the caller to omit and nothing for this
312 // to wait on.
313 self.retire(user_data);
314 }
315 }
316 }
317
318 /// Retire a finished identity: out of the tracking map, into the bounded
319 /// history.
320 ///
321 /// This is the one place an operation stops costing memory proportional to
322 /// how many have run, which is what `M28.2` was about.
323 fn retire(&mut self, user_data: usize) {
324 self.operations.remove(&user_data);
325 if self.finished_set.insert(user_data) {
326 self.finished.push_back(user_data);
327 if self.finished.len() > FINISHED_HISTORY
328 && let Some(evicted) = self.finished.pop_front()
329 {
330 self.finished_set.remove(&evicted);
331 }
332 }
333 }
334
335 /// How many operations have been pushed and not yet completed.
336 ///
337 /// The same number [`RingContract::check_quiescent`] turns into one
338 /// [`Violation::Outstanding`] each, offered directly because a consumer
339 /// usually wants the count rather than the diagnosis -- a drain loop asks
340 /// "is anything left?", not "what is wrong?".
341 ///
342 /// **This exists because two callers had already written it by hand**, and
343 /// both were a restatement of what this type already knew.
344 /// `epoch_log`'s appender kept a `usize` beside its contract and drove it
345 /// at every push and every completion, which is precisely the
346 /// record-the-same-event-twice shape that
347 /// [D-55](../DESIGN-NOTES.md#d-55) exists to remove. The other counted
348 /// `Outstanding` violations out of `check_quiescent`, allocating a `Vec`
349 /// to answer a question about a map's length.
350 #[must_use]
351 pub fn in_flight(&self) -> usize {
352 self.operations.len()
353 }
354 /// Record a registered buffer's outstanding count, as
355 /// [`crate::RegisteredBuffers::outstanding`] reports it.
356 pub fn observe_buffer(&mut self, index: u32, outstanding: usize) {
357 self.buffers.insert(index, outstanding);
358 }
359
360 /// Violations seen so far, without asking about quiescence.
361 #[must_use]
362 pub fn violations(&self) -> &[Violation] {
363 &self.violations
364 }
365
366 /// Every violation, including operations still outstanding and buffers
367 /// still in use.
368 ///
369 /// Call once everything is expected to have finished. An empty result is
370 /// the claim that nothing was lost.
371 #[must_use]
372 pub fn check_quiescent(&self) -> Vec<Violation> {
373 let mut all = self.violations.clone();
374
375 // Sorted so a failure reads the same way twice. `HashMap` iteration
376 // order is deliberately unspecified, and an oracle whose output
377 // reorders between runs is one nobody can diff.
378 let mut pending: Vec<_> = self
379 .operations
380 .iter()
381 // Not a `filter_map`: since `M28.2` retired the terminal states,
382 // every entry still tracked is a violation at quiescence.
383 .map(|(user_data, state)| match state {
384 State::Pushed => Violation::Outstanding {
385 user_data: *user_data,
386 },
387 })
388 .collect();
389 pending.sort_by_key(|violation| match violation {
390 Violation::Outstanding { user_data } => *user_data,
391 _ => 0,
392 });
393 all.extend(pending);
394
395 let mut busy: Vec<_> = self
396 .buffers
397 .iter()
398 .filter(|(_, outstanding)| **outstanding > 0)
399 .map(|(index, outstanding)| Violation::BufferStillInUse {
400 index: *index,
401 outstanding: *outstanding,
402 })
403 .collect();
404 busy.sort_by_key(|violation| match violation {
405 Violation::BufferStillInUse { index, .. } => *index,
406 _ => 0,
407 });
408 all.extend(busy);
409
410 all
411 }
412
413 /// Panic with every violation if the contract was broken.
414 ///
415 /// The convenience form for a test. Reports **all** violations rather than
416 /// the first, because they are usually one cause and seeing only one of
417 /// them invites fixing a symptom.
418 ///
419 /// # Panics
420 ///
421 /// If [`RingContract::check_quiescent`] finds anything.
422 pub fn assert_quiescent(&self) {
423 let violations = self.check_quiescent();
424 assert!(
425 violations.is_empty(),
426 "the ring contract was broken:\n{}",
427 violations
428 .iter()
429 .map(|violation| format!(" - {violation}"))
430 .collect::<Vec<_>>()
431 .join("\n")
432 );
433 }
434}
435
436#[cfg(test)]
437mod tests;