windows_file_enumeration_sys/session.rs
1// Copyright (c) 2026 Mike Grier
2//! The session: two bounded rings, one registry, and one drain authority.
3//!
4//! # Shape
5//!
6//! A [`Session`] is the producing half and clones freely, so several threads can
7//! submit into one session. A [`Receiver`] is the consuming half and does not
8//! clone, because the completion ring's ordering guarantees are stated for one
9//! observer.
10//!
11//! Everything a client asks of a session -- start, cancel, abandon -- enters
12//! through the submission ring and is applied by the servicer. The servicer
13//! mutates the registry and marks enumerations runnable; it never performs a
14//! directory query itself, because a query may block and the servicer is the
15//! only thing that can start or stop anything.
16//!
17//! # A worker reports; the servicer applies
18//!
19//! Work runs on a second thread-pool object, whose callback claims one runnable
20//! enumeration and runs one quantum. That worker delivers entries and its own
21//! terminal to the completion ring and then *reports* retirement through the
22//! submission ring. It never removes a registry entry.
23//!
24//! That is not tidiness. If a worker finished its own enumeration it would drop
25//! that enumeration's state, and any thread-pool object living in that state
26//! would be closed from inside its own callback -- which waits for the callback
27//! doing the waiting and then frees the closure still running. Reporting instead
28//! of acting removes the hazard, and keeping no thread-pool object per
29//! enumeration removes it structurally: abandonment releases entries that own
30//! nothing the pool must be drained for.
31//!
32//! # Who owns the pool objects
33//!
34//! The shared state owns them, and the *last client handle* releases them on its
35//! own thread before letting go of its share of that state. A callback therefore
36//! never holds the last reference to anything whose release would wait on that
37//! callback: by the time the shared state can be dropped, the pool objects are
38//! already closed.
39
40use std::io;
41use std::os::windows::io::{BorrowedHandle, OwnedHandle};
42#[cfg(test)]
43use std::sync::atomic::AtomicBool;
44use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
45use std::sync::{Arc, Mutex, MutexGuard, Weak};
46use std::time::Duration;
47
48use windows_impersonation_token_sys::ImpersonationToken;
49use windows_threadpool_sys::callback_env::CallbackEnviron;
50use windows_threadpool_sys::work::ThreadpoolWork;
51
52use crate::admission::{self, EnumerationHandle};
53use crate::completion::{Completion, EnumerationId, TerminalOutcome};
54use crate::completion_ring::{CompletionRing, MINIMUM_COMPLETION_CAPACITY};
55use crate::engine::{self, EngineState};
56use crate::error::{BeginError, SessionError, SessionFailure};
57use crate::registry::{EnumerationState, Registry};
58use crate::request::EnumerationRequest;
59use crate::submission_ring::{
60 AbandonSlot, ControlMessage, PushOutcome, SubmissionRing, release_retire_slot,
61};
62
63/// The smallest submission-ring capacity that can carry one enumeration.
64///
65/// Four, and each one is load-bearing: the session's standing abandon message,
66/// one enumeration's reserved cancellation, its reserved retirement report, and
67/// one ordinary begin. A smaller ring could be built but could never start
68/// anything, which is not a bound worth offering.
69pub const MINIMUM_SUBMISSION_CAPACITY: usize = 4;
70
71/// The smallest completion-ring capacity that can carry one enumeration.
72///
73/// Two: one reserved terminal outcome and one entry. Reservations never consume
74/// the last slot, so a ring of one could not hold both.
75pub const MINIMUM_COMPLETION_RING_CAPACITY: usize = MINIMUM_COMPLETION_CAPACITY;
76
77/// What one quantum of work decided.
78#[derive(Debug)]
79pub(crate) enum QuantumOutcome {
80 /// Nothing to do. The enumeration stays registered and idle.
81 ///
82 /// The native engine never returns this itself -- claiming an enumeration
83 /// always leaves it with a refill or a parse to do. It exists for the
84 /// state-machine model, which scripts it to stand in for a quantum that
85 /// found nothing runnable.
86 #[allow(
87 dead_code,
88 reason = "only the cfg(test) state-machine model constructs this; a plain lib build never does"
89 )]
90 Idle,
91 /// Progress was made and there is more to do.
92 ///
93 /// A quantum performs at most one refill and then hands the worker back,
94 /// which puts a scheduling point at every place a synchronous directory
95 /// query could have blocked.
96 Yielded,
97 /// Stopped for want of completion-ring room; resume on consumer progress.
98 Parked,
99 /// The enumeration is over, with this outcome.
100 Finished(TerminalOutcome),
101}
102
103/// The session's thread-pool objects.
104///
105/// Two, deliberately: the servicer must stay responsive, so it is not the object
106/// marked as running long. Only the engine may block on a directory query.
107struct SessionWork {
108 servicer: ThreadpoolWork,
109 engine: ThreadpoolWork,
110 /// Set by the state-machine model, which drives both callbacks on its own
111 /// thread so a scenario decides exactly when they run.
112 #[cfg(test)]
113 suppressed: AtomicBool,
114}
115
116impl SessionWork {
117 #[cfg(test)]
118 fn is_suppressed(&self) -> bool {
119 self.suppressed.load(Ordering::Acquire)
120 }
121
122 #[cfg(not(test))]
123 fn is_suppressed(&self) -> bool {
124 false
125 }
126
127 /// Queue one drain of the submission ring.
128 fn submit_servicer(&self) {
129 if !self.is_suppressed() {
130 self.servicer.submit();
131 }
132 }
133
134 /// Queue one quantum of enumeration work.
135 fn submit_engine(&self) {
136 if !self.is_suppressed() {
137 self.engine.submit();
138 }
139 }
140}
141
142/// State both halves of a session share.
143pub(crate) struct SessionShared {
144 pub(crate) completions: Arc<CompletionRing>,
145 pub(crate) submissions: SubmissionRing,
146 registry: Mutex<Registry>,
147 next_id: AtomicU64,
148 /// The pool objects, taken and dropped by the last client handle.
149 ///
150 /// `None` once a session has been torn down, after which nothing further is
151 /// scheduled -- which is correct, because nothing is left to observe it.
152 work: Mutex<Option<SessionWork>>,
153 /// Live [`Session`] and [`Receiver`] handles. Not the `Arc` strong count,
154 /// which a callback transiently inflates.
155 handles: AtomicUsize,
156 /// Quantum outcomes the state-machine model has scripted, standing in for
157 /// the native engine.
158 #[cfg(test)]
159 scripted: Mutex<std::collections::VecDeque<QuantumOutcome>>,
160}
161
162impl SessionShared {
163 fn registry(&self) -> MutexGuard<'_, Registry> {
164 self.registry
165 .lock()
166 .unwrap_or_else(|poison| poison.into_inner())
167 }
168
169 fn work(&self) -> MutexGuard<'_, Option<SessionWork>> {
170 self.work
171 .lock()
172 .unwrap_or_else(|poison| poison.into_inner())
173 }
174
175 /// Allocate the next identifier.
176 ///
177 /// Monotonic within the session, so an identifier retained past its
178 /// enumeration names nothing rather than aliasing a later one.
179 pub(crate) fn next_enumeration_id(&self) -> EnumerationId {
180 EnumerationId::from_raw(self.next_id.fetch_add(1, Ordering::Relaxed))
181 }
182
183 /// Whether this enumeration is still registered.
184 #[cfg(test)]
185 pub(crate) fn contains(&self, enumeration: EnumerationId) -> bool {
186 self.registry().contains(enumeration)
187 }
188
189 /// How many enumerations the session is carrying.
190 pub(crate) fn registered(&self) -> usize {
191 self.registry().len()
192 }
193
194 /// How many enumerations are waiting for a worker.
195 #[cfg(test)]
196 pub(crate) fn ready(&self) -> usize {
197 self.registry().ready_len()
198 }
199
200 /// Queue a drain if this submission is the one that must schedule it.
201 pub(crate) fn ring_servicer(&self, outcome: PushOutcome) {
202 if outcome != PushOutcome::RingDoorbell {
203 return;
204 }
205 if let Some(work) = self.work().as_ref() {
206 work.submit_servicer();
207 }
208 }
209
210 /// Note one more client handle.
211 fn acquire_handle(&self) {
212 self.handles.fetch_add(1, Ordering::AcqRel);
213 }
214
215 /// Release one client handle, tearing the session's pool objects down when
216 /// it was the last.
217 ///
218 /// Runs on whichever thread dropped the handle -- always a client thread,
219 /// never a callback -- which is exactly the precondition for waiting out
220 /// in-flight callbacks.
221 fn release_handle(&self) {
222 if self.handles.fetch_sub(1, Ordering::AcqRel) != 1 {
223 return;
224 }
225 // Taken under the lock and dropped outside it, so a callback blocked on
226 // this lock cannot be what the drop is waiting for.
227 let work = self.work().take();
228 drop(work);
229 }
230
231 /// Service every queued control message, in order, until the ring is empty.
232 ///
233 /// The ring clears its own drain flag when it runs out, under the same lock
234 /// a producer uses to decide whether to ring the doorbell, so this loop and
235 /// the next submission cannot both conclude that the other will do the work.
236 pub(crate) fn drain_submissions(&self) {
237 while let Some(message) = self.submissions.take_for_service() {
238 match message {
239 ControlMessage::Begin(begin) => self.service_begin(*begin),
240 ControlMessage::Cancel(enumeration) => self.service_cancel(enumeration),
241 ControlMessage::Retire(enumeration) => self.service_retire(enumeration),
242 ControlMessage::Abandon => self.service_abandon(),
243 }
244 }
245 }
246
247 /// Register an admitted enumeration and make it runnable.
248 fn service_begin(&self, begin: crate::submission_ring::BeginMessage) {
249 let enumeration = begin.enumeration;
250 {
251 let mut registry = self.registry();
252 if !registry.is_accepting() {
253 // Abandoned between admission and servicing. Releasing the
254 // message's slots without spending them is correct: no receiver
255 // remains to owe an outcome to. Done after the registry lock is
256 // released, because releasing takes the other rings' locks.
257 drop(registry);
258 self.retire_state(
259 EnumerationState::new(begin.engine, begin.terminal, begin.retire),
260 None,
261 );
262 return;
263 }
264 registry.insert(
265 enumeration,
266 EnumerationState::new(begin.engine, begin.terminal, begin.retire),
267 );
268 }
269 self.schedule(enumeration);
270 }
271
272 /// Stop one enumeration.
273 ///
274 /// A quantum in flight cannot be preempted, so this only records the
275 /// intention; the worker holding it applies the outcome when it reports.
276 /// Only a quiescent enumeration is finished here, which is what keeps
277 /// exactly one terminal per enumeration.
278 fn service_cancel(&self, enumeration: EnumerationId) {
279 let finished = {
280 let mut registry = self.registry();
281 let Some(state) = registry.get_mut(enumeration) else {
282 // Already finished. A cancellation that lost the race is not an
283 // error: the enumeration is over either way.
284 return;
285 };
286 state.cancelled = true;
287 state.parked = false;
288 if state.is_quiescent() {
289 registry.remove(enumeration)
290 } else {
291 None
292 }
293 };
294 if let Some(state) = finished {
295 self.retire_state(state, Some(TerminalOutcome::Cancelled));
296 }
297 }
298
299 /// Release an enumeration whose worker has reported itself finished.
300 ///
301 /// The terminal was already delivered by that worker, so nothing is owed
302 /// here; this returns what the entry still holds.
303 fn service_retire(&self, enumeration: EnumerationId) {
304 let state = self.registry().remove(enumeration);
305 if let Some(state) = state {
306 self.retire_state(state, None);
307 }
308 }
309
310 /// Tear the session down because its receiver is gone.
311 ///
312 /// No terminal outcomes are delivered, because nothing remains to observe
313 /// them; the reserved slots are simply released. Nothing here waits on a
314 /// worker, because a registry entry owns no thread-pool object.
315 fn service_abandon(&self) {
316 let abandoned = {
317 let mut registry = self.registry();
318 registry.stop_accepting();
319 registry.drain_all()
320 };
321 for (_, state) in abandoned {
322 self.retire_state(state, None);
323 }
324 }
325
326 /// Release everything a removed entry still holds, delivering `outcome` if
327 /// one is still owed.
328 ///
329 /// An unspent slot of either kind returns to its ring rather than being
330 /// leaked; a terminal slot dropped without an outcome releases its
331 /// completion-ring reservation.
332 fn retire_state(&self, mut state: EnumerationState, outcome: Option<TerminalOutcome>) {
333 if let Some(retire) = state.retire.take() {
334 release_retire_slot(&self.submissions, retire);
335 }
336 match (state.terminal.take(), outcome) {
337 (Some(terminal), Some(outcome)) => terminal.send(outcome),
338 (slot, _) => drop(slot),
339 }
340 }
341
342 /// Make one enumeration runnable and ask for a worker.
343 pub(crate) fn schedule(&self, enumeration: EnumerationId) {
344 self.registry().mark_ready(enumeration);
345 if let Some(work) = self.work().as_ref() {
346 work.submit_engine();
347 }
348 }
349
350 /// Run one quantum for one runnable enumeration.
351 ///
352 /// This is the engine callback's whole body. Claiming is single-flight, so
353 /// an enumeration already held by another worker is skipped rather than run
354 /// twice over the same buffer and cursor.
355 pub(crate) fn run_engine_quantum(&self) {
356 let Some((enumeration, mut engine)) = self.claim_next() else {
357 return;
358 };
359 let outcome = self.advance(enumeration, &mut engine);
360 self.report_quantum(enumeration, engine, outcome);
361 }
362
363 /// Claim the next runnable enumeration, taking its engine state with it.
364 pub(crate) fn claim_next(&self) -> Option<(EnumerationId, EngineState)> {
365 self.registry().claim_next()
366 }
367
368 /// Advance one enumeration by one bounded quantum.
369 ///
370 /// Runs with no lock held, because a quantum performs a synchronous
371 /// directory query. A scripted outcome takes precedence so the
372 /// state-machine model can drive the shell without touching a filesystem.
373 fn advance(&self, enumeration: EnumerationId, engine: &mut EngineState) -> QuantumOutcome {
374 #[cfg(test)]
375 if let Some(scripted) = self
376 .scripted
377 .lock()
378 .unwrap_or_else(|poison| poison.into_inner())
379 .pop_front()
380 {
381 return scripted;
382 }
383 engine::advance(engine, enumeration, &self.completions)
384 }
385
386 /// Hand the claim back, engine state and all, and apply whatever the
387 /// quantum decided.
388 pub(crate) fn report_quantum(
389 &self,
390 enumeration: EnumerationId,
391 engine: EngineState,
392 outcome: QuantumOutcome,
393 ) {
394 let mut resume = false;
395 let finish = {
396 let mut registry = self.registry();
397 let Some(state) = registry.get_mut(enumeration) else {
398 // Removed while this worker held it, which is what abandonment
399 // does. Nothing is owed; dropping the engine state here is what
400 // releases its directory handle and buffer.
401 return;
402 };
403 state.running = false;
404 state.engine = Some(engine);
405 match outcome {
406 // The worker reached a real conclusion, which wins over a
407 // cancellation that arrived while it was doing so.
408 QuantumOutcome::Finished(outcome) => Some(outcome),
409 _ if state.cancelled => Some(TerminalOutcome::Cancelled),
410 QuantumOutcome::Yielded => {
411 resume = true;
412 None
413 }
414 QuantumOutcome::Parked => {
415 // `advance` checked room with no lock held, so a receiver
416 // may already have drained the record this worker was
417 // waiting on -- and already called `resume_parked` and
418 // found nothing, because this worker had not yet reached
419 // this lock to say it was waiting. Re-checking room here,
420 // while still holding the same lock `resume_parked` reads
421 // under, closes that window: whichever of the two sides
422 // runs last is the one that observes the other's update,
423 // so the enumeration can never be left parked with room
424 // already available and nothing left to wake it.
425 if self.completions.has_data_room() {
426 resume = true;
427 } else {
428 state.parked = true;
429 }
430 None
431 }
432 QuantumOutcome::Idle => None,
433 }
434 };
435 if let Some(outcome) = finish {
436 self.finish_from_worker(enumeration, outcome);
437 } else if resume {
438 // Outside the lock: scheduling takes it again.
439 self.schedule(enumeration);
440 }
441 }
442
443 /// Deliver a worker's terminal and report the enumeration for retirement.
444 ///
445 /// The worker owns the terminal slot, so delivery cannot fail. Removing the
446 /// entry is the servicer's job, which is why this reports rather than
447 /// removes.
448 fn finish_from_worker(&self, enumeration: EnumerationId, outcome: TerminalOutcome) {
449 let (terminal, retire) = {
450 let mut registry = self.registry();
451 match registry.get_mut(enumeration) {
452 Some(state) => (state.terminal.take(), state.retire.take()),
453 None => return,
454 }
455 };
456 if let Some(terminal) = terminal {
457 terminal.send(outcome);
458 }
459 if let Some(retire) = retire {
460 let pushed = self.submissions.push_retire(retire, enumeration);
461 self.ring_servicer(pushed);
462 }
463 }
464
465 /// Resume every enumeration that stopped for want of completion-ring room.
466 ///
467 /// Called after a receiver takes a record, which is the only event that can
468 /// create that room.
469 pub(crate) fn resume_parked(&self) {
470 let parked = {
471 let registry = self.registry();
472 registry.parked()
473 };
474 for enumeration in parked {
475 self.schedule(enumeration);
476 }
477 }
478
479 /// Push a scripted quantum outcome for the state-machine model.
480 #[cfg(test)]
481 pub(crate) fn script_quantum(&self, outcome: QuantumOutcome) {
482 self.scripted
483 .lock()
484 .unwrap_or_else(|poison| poison.into_inner())
485 .push_back(outcome);
486 }
487}
488
489/// The producing half of a session.
490///
491/// Clone it to submit from several threads; every clone feeds the same
492/// submission ring and the same receiver.
493pub struct Session {
494 pub(crate) shared: Arc<SessionShared>,
495}
496
497impl Session {
498 /// Build a session and its receiver.
499 ///
500 /// `submission_capacity` bounds outstanding control messages and
501 /// `completion_capacity` bounds undelivered entries and outcomes. Both are
502 /// hard bounds: the session never allocates past them in response to load.
503 ///
504 /// # Errors
505 ///
506 /// Returns [`SessionError`] if either capacity is below the minimum that can
507 /// carry one enumeration, or if the thread pool refused to create either of
508 /// the session's work objects.
509 pub fn new(
510 submission_capacity: usize,
511 completion_capacity: usize,
512 ) -> Result<(Session, Receiver), SessionError> {
513 if submission_capacity < MINIMUM_SUBMISSION_CAPACITY {
514 return Err(SessionError::new(
515 SessionFailure::SubmissionCapacityTooSmall,
516 ));
517 }
518 if completion_capacity < MINIMUM_COMPLETION_RING_CAPACITY {
519 return Err(SessionError::new(
520 SessionFailure::CompletionCapacityTooSmall,
521 ));
522 }
523
524 let shared = Arc::new(SessionShared {
525 completions: Arc::new(CompletionRing::new(completion_capacity)),
526 submissions: SubmissionRing::new(submission_capacity),
527 registry: Mutex::new(Registry::new()),
528 next_id: AtomicU64::new(1),
529 work: Mutex::new(None),
530 // The session and its receiver.
531 handles: AtomicUsize::new(2),
532 #[cfg(test)]
533 scripted: Mutex::new(std::collections::VecDeque::new()),
534 });
535
536 // Both callbacks hold only a `Weak`, so neither keeps the session alive
537 // and neither can become the owner that closes its own work object.
538 let servicer = {
539 let weak: Weak<SessionShared> = Arc::downgrade(&shared);
540 ThreadpoolWork::new(
541 move || {
542 if let Some(shared) = weak.upgrade() {
543 shared.drain_submissions();
544 }
545 },
546 None,
547 )
548 .map_err(|error| SessionError::with_source(SessionFailure::WorkObject, error))?
549 };
550 let engine = {
551 let weak: Weak<SessionShared> = Arc::downgrade(&shared);
552 // Any quantum may perform a synchronous directory query, so the pool
553 // is told these callbacks can block. That accounting belongs to the
554 // engine alone: the servicer must stay responsive.
555 let mut environment = CallbackEnviron::new();
556 environment.set_runs_long();
557 ThreadpoolWork::new(
558 move || {
559 if let Some(shared) = weak.upgrade() {
560 shared.run_engine_quantum();
561 }
562 },
563 Some(&mut environment),
564 )
565 .map_err(|error| SessionError::with_source(SessionFailure::WorkObject, error))?
566 };
567 *shared.work() = Some(SessionWork {
568 servicer,
569 engine,
570 #[cfg(test)]
571 suppressed: AtomicBool::new(false),
572 });
573
574 // Claimed now, not at receiver drop: at drop there is nowhere to report
575 // that the ring had no room, and abandonment must never be the message
576 // that could not be sent. The minimum capacity guarantees room on a
577 // freshly built ring.
578 let abandon = shared
579 .submissions
580 .reserve_abandon()
581 .expect("a fresh submission ring always has room for the abandon slot");
582
583 let receiver = Receiver {
584 shared: Arc::clone(&shared),
585 abandon: Some(abandon),
586 };
587 Ok((Session { shared }, receiver))
588 }
589
590 /// The submission ring's bound.
591 #[must_use]
592 pub fn submission_capacity(&self) -> usize {
593 self.shared.submissions.capacity()
594 }
595
596 /// The completion ring's bound.
597 #[must_use]
598 pub fn completion_capacity(&self) -> usize {
599 self.shared.completions.capacity()
600 }
601
602 /// How many enumerations this session is currently carrying.
603 #[must_use]
604 pub fn enumerations(&self) -> usize {
605 self.shared.registered()
606 }
607
608 /// Whether the receiver has abandoned this session, so no further
609 /// enumeration can be started.
610 #[must_use]
611 pub fn is_abandoned(&self) -> bool {
612 self.shared.submissions.is_abandoned()
613 }
614
615 /// Leave servicing entirely to explicit drains, for the state-machine
616 /// model, which must decide when each step happens.
617 #[cfg(test)]
618 pub(crate) fn suppress_pool(&self) {
619 if let Some(work) = self.shared.work().as_ref() {
620 work.suppressed.store(true, Ordering::Release);
621 }
622 }
623
624 /// Start enumerating one directory under the caller's own security context.
625 ///
626 /// The context is captured synchronously, here, before the request becomes
627 /// visible to the session. The directory is opened later on a thread-pool
628 /// worker, whose own identity is unrelated, so capturing at submission is
629 /// what makes the open happen as whoever asked for it.
630 ///
631 /// Returns immediately with an affine [`EnumerationHandle`]. Dropping that
632 /// handle cancels the enumeration; [`EnumerationHandle::detach`] lets it run
633 /// to completion instead.
634 ///
635 /// # Errors
636 ///
637 /// Returns [`BeginError`] when the caller's context cannot be captured, when
638 /// either ring cannot secure the room this enumeration would need, or when
639 /// the receiver has already abandoned the session. Nothing is accepted in
640 /// any of those cases, and the request comes back with the error.
641 pub fn try_begin(&self, request: EnumerationRequest) -> Result<EnumerationHandle, BeginError> {
642 admission::try_begin(&self.shared, request)
643 }
644
645 /// Start enumerating one directory under an already-captured context.
646 ///
647 /// This is the form a traversal layer wants: capture once when the traversal
648 /// is submitted, then reuse that one context for every directory in the
649 /// tree, rather than re-capturing per directory on whatever thread happens
650 /// to be submitting.
651 ///
652 /// # Errors
653 ///
654 /// As [`try_begin`](Self::try_begin), except that no capture is attempted
655 /// and so [`BeginFailure::TokenCapture`](crate::BeginFailure::TokenCapture)
656 /// cannot occur.
657 pub fn try_begin_with_token(
658 &self,
659 request: EnumerationRequest,
660 token: ImpersonationToken,
661 ) -> Result<EnumerationHandle, BeginError> {
662 admission::try_begin_with_token(&self.shared, request, token)
663 }
664}
665
666impl Clone for Session {
667 fn clone(&self) -> Self {
668 self.shared.completions.add_session();
669 self.shared.acquire_handle();
670 Self {
671 shared: Arc::clone(&self.shared),
672 }
673 }
674}
675
676impl Drop for Session {
677 fn drop(&mut self) {
678 // One fewer producer. When the last one goes and nothing is still
679 // enumerating, the receiver learns the stream has ended rather than
680 // blocking on a record that can never arrive.
681 self.shared.completions.remove_session();
682 self.shared.release_handle();
683 }
684}
685
686impl std::fmt::Debug for Session {
687 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
688 f.debug_struct("Session")
689 .field("submission_capacity", &self.submission_capacity())
690 .field("completion_capacity", &self.completion_capacity())
691 .field("enumerations", &self.enumerations())
692 .finish_non_exhaustive()
693 }
694}
695
696/// The consuming half of a session: the only way to observe completions.
697///
698/// Not clonable. The ordering the completion ring promises -- every entry of one
699/// enumeration before its terminal -- is a statement about one observer, and two
700/// receivers racing on the same ring would each see an arbitrary subsequence of
701/// it.
702///
703/// Dropping the receiver abandons the session: the session stops accepting
704/// enumerations and releases the ones it is carrying, without delivering any
705/// terminal outcome, because no observer remains to owe one to.
706pub struct Receiver {
707 shared: Arc<SessionShared>,
708 /// The standing abandon reservation, claimed when the session was built so
709 /// that `Drop` has nowhere to fail.
710 abandon: Option<AbandonSlot>,
711}
712
713impl Receiver {
714 /// Take the next record if one is already queued.
715 #[must_use]
716 pub fn try_recv(&self) -> Option<Completion> {
717 let record = self.shared.completions.try_take();
718 if record.is_some() {
719 // Taking a record is the only thing that creates room, so it is
720 // also the only thing that can un-park a backpressured enumeration.
721 self.shared.resume_parked();
722 }
723 record
724 }
725
726 /// Block until a record is available, or until the stream ends.
727 ///
728 /// Returns `None` only when nothing is queued, no session handle remains,
729 /// and no enumeration is still outstanding.
730 #[must_use]
731 pub fn recv(&self) -> Option<Completion> {
732 let record = self.shared.completions.take_blocking(None);
733 if record.is_some() {
734 self.shared.resume_parked();
735 }
736 record
737 }
738
739 /// Block for at most `timeout`.
740 ///
741 /// Returns `None` on timeout as well as at the end of the stream; a caller
742 /// that must tell them apart can check
743 /// [`is_disconnected`](Self::is_disconnected).
744 #[must_use]
745 pub fn recv_timeout(&self, timeout: Duration) -> Option<Completion> {
746 let record = self.shared.completions.take_blocking(Some(timeout));
747 if record.is_some() {
748 self.shared.resume_parked();
749 }
750 record
751 }
752
753 /// A manual-reset event, signalled exactly while this receiver has something
754 /// to take.
755 ///
756 /// This is what lets a client integrate with its own thread pool instead of
757 /// dedicating a thread to [`recv`](Self::recv): wait on the handle, then
758 /// drain with [`try_recv`](Self::try_recv) until it yields `None`. It stays
759 /// signalled once the stream has ended, so a waiter learns about that too.
760 ///
761 /// The event is created on the first call, so a client that never asks for
762 /// it pays for no kernel object. The borrow is deliberate: the event belongs
763 /// to the ring and must not be closed by a caller.
764 ///
765 /// # Errors
766 ///
767 /// Returns the error from `CreateEventW` on the first call.
768 pub fn doorbell(&self) -> io::Result<BorrowedHandle<'_>> {
769 self.shared.completions.doorbell()
770 }
771
772 /// A duplicate of [`doorbell`](Self::doorbell) that the caller owns, as
773 /// arming a `ThreadpoolWait` requires.
774 ///
775 /// # Errors
776 ///
777 /// Returns the error from `CreateEventW` or `DuplicateHandle`.
778 pub fn doorbell_owned(&self) -> io::Result<OwnedHandle> {
779 self.shared.completions.doorbell_owned()
780 }
781
782 /// Whether the stream has ended.
783 #[must_use]
784 pub fn is_disconnected(&self) -> bool {
785 self.shared.completions.is_closed()
786 }
787
788 /// How many records are queued right now.
789 #[must_use]
790 pub fn len(&self) -> usize {
791 self.shared.completions.len()
792 }
793
794 /// Whether nothing is queued right now.
795 #[must_use]
796 pub fn is_empty(&self) -> bool {
797 self.len() == 0
798 }
799
800 /// The completion ring's bound.
801 #[must_use]
802 pub fn capacity(&self) -> usize {
803 self.shared.completions.capacity()
804 }
805}
806
807impl Drop for Receiver {
808 fn drop(&mut self) {
809 // Infallible by construction: the slot was claimed when the session was
810 // built precisely so this path cannot fail. Ringing rather than draining
811 // is what makes abandonment asynchronous -- `Drop` never blocks on the
812 // teardown it starts, unless it is also the last handle, in which case
813 // releasing the pool objects necessarily waits out their callbacks.
814 if let Some(slot) = self.abandon.take() {
815 let pushed = self.shared.submissions.push_abandon(slot);
816 self.shared.ring_servicer(pushed);
817 }
818 self.shared.release_handle();
819 }
820}
821
822impl std::fmt::Debug for Receiver {
823 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
824 f.debug_struct("Receiver")
825 .field("queued", &self.len())
826 .field("capacity", &self.capacity())
827 .field("disconnected", &self.is_disconnected())
828 .finish_non_exhaustive()
829 }
830}
831
832#[cfg(test)]
833mod tests;