Skip to main content

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;