Skip to main content

darkbio_wire/protocol/
session.rs

1// wire-rs: encrypted protocol between Ark and host
2// Copyright 2026 Dark Bio AG. All rights reserved.
3//
4// Use of this source code is governed by a BSD-style
5// license that can be found in the LICENSE file.
6
7//! Session state, request queues, and the reader, writer, and deadline workers.
8
9use super::envelope::{Header, IncomingEnvelope, MessageKind, Parity, Side};
10use super::operation::{
11    OperationHandle, OperationKey, OutgoingBody, OutgoingMessage, PendingOperation,
12};
13use super::promise::{Notifications, PromiseResult};
14use super::worker;
15use super::{
16    Closer, DEFAULT_AUTOREPLY_TIMEOUT, DEFAULT_MAX_INBOUND_BYTES, DEFAULT_MAX_INBOUND_REQUESTS,
17    Error, Message, Promise, Requester, Responder, schema,
18};
19use crate::LogId;
20use crate::transport::{self, Read, Stream, Verifier, Write};
21use darkbio_clock::Clock;
22use darkbio_clock::sync::{Condvar, Mutex};
23use prost::bytes::Bytes;
24use std::collections::{HashMap, HashSet, VecDeque};
25use std::fmt;
26use std::sync::Arc;
27use std::sync::atomic::{AtomicUsize, Ordering};
28use std::time::{Duration, Instant};
29
30/// Establishes a client session, verifies the peer and returns the verifier's info.
31///
32/// Takes ownership of the stream and constructs the transport internally.
33/// Failure closes the client stream, and the application can open another
34/// stream and reconnect. The verifier is only borrowed during this blocking
35/// call. Failure to start a required worker or an escaping worker panic aborts
36/// the process.
37pub fn connect<R, W, V>(stream: Stream<R, W>, verifier: &V) -> Result<(Session, V::Info), Error>
38where
39    R: Read + Send + 'static,
40    W: Write + Send + 'static,
41    V: Verifier,
42{
43    // Take the stream's clock before the client consumes the stream
44    let clock = stream.clock();
45    let mut client = transport::Client::new(stream);
46
47    // Establish the session before starting its protocol workers
48    let (sender, info) = client.connect(verifier)?;
49    #[cfg(any(test, feature = "fuzz"))]
50    let workers = Arc::new(worker::Tracker::default());
51    let session = Session::start(
52        Side::Client,
53        clock,
54        sender,
55        Some(client.closer()),
56        #[cfg(any(test, feature = "fuzz"))]
57        workers.clone(),
58    );
59    let inner = session.inner.clone();
60
61    // Let the reader route transport messages into the shared session
62    worker::spawn(
63        "wire-client-reader",
64        #[cfg(any(test, feature = "fuzz"))]
65        &workers,
66        move || run_reader(client, inner),
67    );
68    Ok((session, info))
69}
70
71/// Receives and handles messages for one client session.
72///
73/// The reader retains its session state until it exits. Closing the session
74/// shuts down the stream and wakes a blocked transport read.
75fn run_reader<R: Read, W: Write>(mut client: transport::Client<R, W>, session: Arc<SessionInner>) {
76    loop {
77        let result = client
78            .recv()
79            .map_err(Error::from)
80            .and_then(|bytes| session.handle_message(bytes));
81        if let Err(error) = result {
82            // Receive and decode failures close this client session and its
83            // stream, waking any other blocked transport I/O
84            session.close(error);
85            break;
86        }
87    }
88}
89
90/// Owner of one session and its incoming request queue.
91///
92/// Incoming requests use [`Message`], paired with a common responder. The
93/// application selects the reply type; there is no static request/response pairing
94/// table. The host/server role and envelope direction are handled internally.
95///
96/// Closing or dropping the session fails pending promises, discards queued requests,
97/// and wakes blocked [`Self::recv`] calls. Completed promises keep their results.
98/// Application jobs keep running, but their handles still refer to the closed
99/// session and cannot send messages through a replacement session.
100///
101/// A client session also closes its stream. A server session leaves the server's
102/// stream available for another handshake. Handles do not keep the session open.
103/// The owner cannot be cloned; obtain requesters or closers for other threads:
104///
105/// ```compile_fail,E0599
106/// use darkbio_wire::protocol::Session;
107/// fn duplicate(session: Session) { let _ = session.clone(); }
108/// ```
109pub struct Session {
110    /// Queues and pending operations shared with this session's workers.
111    pub(super) inner: Arc<SessionInner>,
112}
113
114impl Session {
115    /// Returns the clock of the stream this session runs on, which request and
116    /// reply deadlines are measured on.
117    pub fn clock(&self) -> Clock {
118        self.inner.clock.clone()
119    }
120
121    /// Sets the timeout for automatic `UNANSWERED` and `UNKNOWN` replies.
122    ///
123    /// Defaults to [`DEFAULT_AUTOREPLY_TIMEOUT`]. Replies already queued keep
124    /// their deadlines.
125    ///
126    /// The budget starts when a responder is dropped or an unknown request is
127    /// received, and includes queueing. Expiry discards a queued reply, while a
128    /// write already started still runs under the transport's independent
129    /// timeout. Explicit request and reply deadlines are unaffected. Zero or an
130    /// unrepresentable deadline expires immediately.
131    /// Use [`super::Server::set_autoreply_timeout`] to also set the timeout for
132    /// future server sessions.
133    pub fn set_autoreply_timeout(self, timeout: Duration) -> Self {
134        self.inner.set_autoreply_timeout(timeout);
135        self
136    }
137
138    /// Sets the maximum accepted peer requests and buffered incoming bytes together.
139    ///
140    /// Defaults to [`DEFAULT_MAX_INBOUND_REQUESTS`] and [`DEFAULT_MAX_INBOUND_BYTES`].
141    ///
142    /// `requests` counts queued requests, held responders and queued replies,
143    /// including automatic replies to requests with unknown content.
144    /// A slot is freed when the writer takes the reply or the reply is discarded.
145    /// Zero refuses all peer requests but still allows responses to our requests.
146    ///
147    /// `bytes` counts the full encoded envelopes of queued requests and unread
148    /// responses. [`Self::recv`], [`Promise::wait`] or dropping a response promise
149    /// releases that space in the budget. Zero allows no buffered envelopes.
150    /// Decoded application data, outgoing messages and transport buffers are
151    /// excluded.
152    ///
153    /// Exceeding either limit closes this session with
154    /// [`Error::InboundRequestLimitExceeded`] or [`Error::InboundByteLimitExceeded`].
155    /// The reader never waits for space. Lowering a limit below usage also closes
156    /// the session. Completed promises keep their results and bytes until read or
157    /// dropped. Raising limits does not reopen a closed session.
158    pub fn set_inbound_limits(self, requests: usize, bytes: usize) -> Self {
159        let mut notifications = Notifications::default();
160        self.inner
161            .set_inbound_limits(requests, bytes, &mut notifications);
162        self
163    }
164
165    /// Returns a clonable requester bound to this session.
166    pub fn requester(&self) -> Requester {
167        Requester::new(Arc::downgrade(&self.inner), &self.inner.clock)
168    }
169
170    /// Blocks for the next peer request and its [`Responder`].
171    ///
172    /// Closing the session wakes this call with the error that closed it. The
173    /// caller decides how to handle each request.
174    ///
175    /// Taking a request removes its bytes from the inbound byte count before
176    /// decoding it. The request still counts toward the inbound request limit
177    /// while its responder is held. Invalid protobuf returns [`Error::Malformed`]
178    /// and closes this session, which runs the notify callbacks of its pending
179    /// promises on the calling thread.
180    ///
181    /// If another thread closes the session after this call takes a request from
182    /// the queue, it can still return that request. Replying after closure
183    /// returns an error.
184    pub fn recv(&mut self) -> Result<(Message, Responder), Error> {
185        self.inner.recv()
186    }
187
188    /// Returns a clonable handle for closing this session from another thread,
189    /// including while its owner is blocked in [`Self::recv`].
190    pub fn closer(&self) -> Closer {
191        Closer::session(Arc::downgrade(&self.inner))
192    }
193
194    /// Closes this session, discarding queued messages and failing pending
195    /// promises.
196    ///
197    /// Repeated calls have no further effect. A client session also closes its
198    /// stream, waiting for adapter calls in progress to return. This does not
199    /// wait for application jobs or guarantee the peer has observed closure. A
200    /// transport write already started may still finish, but cannot change a
201    /// completed promise's result or affect a replacement session.
202    pub fn close(&self) {
203        self.inner.close(Error::Closed);
204    }
205}
206
207impl Drop for Session {
208    /// Closes the session even when requesters, responders or closers remain.
209    fn drop(&mut self) {
210        self.close();
211    }
212}
213
214impl fmt::Debug for Session {
215    /// Shows the session label and whether the session is still open.
216    ///
217    /// A state lock held elsewhere leaves the state out.
218    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
219        let mut session = f.debug_struct("Session");
220        session.field("id", &self.inner.log_id);
221        if let Ok(state) = self.inner.state.try_lock() {
222            session.field("open", &matches!(*state, State::Open { .. }));
223        }
224        session.finish_non_exhaustive()
225    }
226}
227
228/// Queues and pending operations for one session.
229///
230/// Requesters, responders and closers hold weak references to this object,
231/// even after a new session connects.
232pub(super) struct SessionInner {
233    /// Clock inherited from the stream that established this session.
234    pub(super) clock: Clock,
235    /// Queues, pending operations and the transition to [`State::Closed`],
236    /// under one lock.
237    state: Mutex<State>,
238    /// Signal waking [`Self::recv`], the writer and the deadline worker when
239    /// `state` changes.
240    changed: Condvar,
241    /// Incoming bytes held by queued requests and unread responses.
242    ///
243    /// Accepting messages and changing limits hold `state`. Consumers can
244    /// release bytes without that lock. Unread promises keep this counter alive
245    /// after closure.
246    retained_bytes: Arc<AtomicUsize>,
247    /// Envelope direction and request parity, fixed for the whole session.
248    side: Side,
249    /// Label of the transport session in log lines, unset without a transport.
250    pub(super) log_id: LogId,
251    /// Closer of the client's stream.
252    ///
253    /// Server sessions and tests without a stream leave it empty, since
254    /// [`Server`](super::Server) closes a server's stream.
255    stream_closer: Option<transport::Closer>,
256    /// Tracker letting tests wait for worker threads to exit.
257    #[cfg(any(test, feature = "fuzz"))]
258    pub(super) workers: Arc<worker::Tracker>,
259    /// Channel notifying tests when the last `Arc<SessionInner>` is dropped.
260    #[cfg(any(test, feature = "fuzz"))]
261    drop_hook: std::sync::Mutex<Option<std::sync::mpsc::Sender<()>>>,
262    /// Hook pausing the writer before `sender.disconnect()` in replacement tests.
263    #[cfg(any(test, feature = "fuzz"))]
264    disconnect_hook:
265        std::sync::Mutex<Option<(std::sync::mpsc::Sender<()>, std::sync::mpsc::Receiver<()>)>>,
266}
267
268/// An open session's queues, or the error that closed the session.
269///
270/// Test and fuzz builds keep the same inline layout, although their wait hook
271/// crosses Clippy's size threshold for the difference between variants.
272#[cfg_attr(any(test, feature = "fuzz"), allow(clippy::large_enum_variant))]
273enum State {
274    /// Open session with its queued messages and pending operations.
275    ///
276    /// [`State::close`] replaces it with [`State::Closed`], and the caller drops
277    /// the queues after releasing the state lock.
278    Open {
279        /// Ceiling for accepted requests, including application-held responders.
280        max_inbound_requests: usize,
281        /// Ceiling for encoded requests and unread response promises.
282        max_inbound_bytes: usize,
283        /// Timeout selected when an automatic reply is queued.
284        autoreply_timeout: Duration,
285
286        /// Peer requests awaiting application receipt, paired with their request IDs.
287        incoming: VecDeque<(u64, IncomingEnvelope)>,
288        /// IDs of accepted peer requests, whose count the request limit applies to.
289        ///
290        /// A duplicate ID is rejected while the receive queue, a responder or a
291        /// queued reply holds it. Taking a reply into the writer or discarding
292        /// an expired reply releases its ID. Release happens before writing,
293        /// since the peer can receive the reply and reuse its ID before our
294        /// local flush returns.
295        reserved_ids: HashSet<u64>,
296
297        /// Requests and replies waiting for the writer to take them.
298        outgoing: VecDeque<OutgoingMessage>,
299        /// Next locally allocated ID, or `None` once exhausted, never wrapping
300        /// or reusing an ID.
301        next_id: Option<u64>,
302        /// Operation keys of sent requests, by wire ID.
303        ///
304        /// An entry stays after its promise times out, until the peer responds
305        /// or the session closes. A request that fails to encode is removed at
306        /// once.
307        outstanding: HashMap<u64, OperationKey>,
308        /// Operations waiting to send a result to their promise.
309        ///
310        /// Each is removed before sending that result, so a promise is completed
311        /// only once.
312        operations: HashMap<OperationKey, PendingOperation>,
313
314        /// One-shot test notification sent under the state lock before waiting.
315        #[cfg(any(test, feature = "fuzz"))]
316        wait_hook: Option<std::sync::mpsc::Sender<()>>,
317    },
318    /// Closed session, with the first closing reason returned to every later
319    /// operation.
320    Closed(Error),
321}
322
323impl SessionInner {
324    /// Creates empty queues and pending-operation maps before starting workers.
325    fn new(
326        side: Side,
327        clock: Clock,
328        log_id: LogId,
329        stream_closer: Option<transport::Closer>,
330        #[cfg(any(test, feature = "fuzz"))] workers: Arc<worker::Tracker>,
331    ) -> Self {
332        Self {
333            changed: Condvar::new(&clock),
334            clock,
335            state: Mutex::new(State::Open {
336                max_inbound_requests: DEFAULT_MAX_INBOUND_REQUESTS,
337                max_inbound_bytes: DEFAULT_MAX_INBOUND_BYTES,
338                autoreply_timeout: DEFAULT_AUTOREPLY_TIMEOUT,
339
340                incoming: VecDeque::new(),
341                reserved_ids: HashSet::new(),
342
343                outgoing: VecDeque::new(),
344                next_id: Some(Parity::from(side).first()),
345                outstanding: HashMap::new(),
346                operations: HashMap::new(),
347
348                #[cfg(any(test, feature = "fuzz"))]
349                wait_hook: None,
350            }),
351            retained_bytes: Arc::new(AtomicUsize::new(0)),
352            side,
353            log_id,
354            stream_closer,
355            #[cfg(any(test, feature = "fuzz"))]
356            workers,
357            #[cfg(any(test, feature = "fuzz"))]
358            drop_hook: std::sync::Mutex::new(None),
359            #[cfg(any(test, feature = "fuzz"))]
360            disconnect_hook: std::sync::Mutex::new(None),
361        }
362    }
363
364    /// Changes the timeout for subsequent automatic replies.
365    ///
366    /// Queued replies keep their original deadlines.
367    pub(super) fn set_autoreply_timeout(&self, timeout: Duration) {
368        let mut state = self.state.lock().expect("session state not poisoned");
369        if let State::Open {
370            autoreply_timeout, ..
371        } = &mut *state
372        {
373            *autoreply_timeout = timeout;
374        }
375    }
376
377    /// Updates both limits and closes the session if usage is too high.
378    ///
379    /// It holds the same lock used to accept incoming messages.
380    pub(super) fn set_inbound_limits(
381        &self,
382        requests: usize,
383        bytes: usize,
384        notifications: &mut Notifications,
385    ) {
386        // Apply policy under the lock shared with incoming admission
387        let removed = {
388            let mut state = self.state.lock().expect("session state not poisoned");
389            let State::Open {
390                max_inbound_requests,
391                max_inbound_bytes,
392                reserved_ids,
393                ..
394            } = &mut *state
395            else {
396                return;
397            };
398            *max_inbound_requests = requests;
399            *max_inbound_bytes = bytes;
400            let error = if reserved_ids.len() > *max_inbound_requests {
401                Some(Error::InboundRequestLimitExceeded(*max_inbound_requests))
402            } else if self.retained_bytes.load(Ordering::Relaxed) > *max_inbound_bytes {
403                Some(Error::InboundByteLimitExceeded(*max_inbound_bytes))
404            } else {
405                None
406            };
407            error.and_then(|error| {
408                let (pending, queued) = state.workload();
409                let removed = state.close(error.clone(), self.now(), notifications);
410                if removed.is_some() {
411                    self.log_closed(&error, pending, queued);
412                }
413                removed
414            })
415        };
416
417        // Release queued work and wake waiters after unlocking
418        if removed.is_some() {
419            self.finish_close(removed);
420        }
421    }
422
423    /// Counts the envelope's original bytes under the session lock.
424    ///
425    /// Decoding or dropping it later releases those bytes to this session's
426    /// counter.
427    fn retain_incoming(
428        self: &Arc<Self>,
429        bytes: Bytes,
430        header: Header,
431        limit: usize,
432    ) -> Result<IncomingEnvelope, Error> {
433        IncomingEnvelope::new(
434            bytes,
435            header,
436            &self.retained_bytes,
437            limit,
438            self.side,
439            Arc::downgrade(self),
440        )
441    }
442
443    /// Takes the next request from `incoming` and creates its responder.
444    ///
445    /// If the queue is empty, it waits on `changed`. Closing the session wakes
446    /// the wait and returns the error stored in [`State::Closed`]. It decodes
447    /// after releasing the lock, so the reader can keep receiving messages.
448    fn recv(self: &Arc<Self>) -> Result<(Message, Responder), Error> {
449        let mut state = self.state.lock().expect("session state not poisoned");
450        let (message, responder) = loop {
451            match &mut *state {
452                State::Closed(error) => return Err(error.clone()),
453                State::Open {
454                    incoming,
455                    #[cfg(any(test, feature = "fuzz"))]
456                    wait_hook,
457                    ..
458                } => {
459                    if let Some((id, message)) = incoming.pop_front() {
460                        break (
461                            message,
462                            Responder::new(Arc::downgrade(self), &self.clock, id),
463                        );
464                    }
465                    #[cfg(any(test, feature = "fuzz"))]
466                    if let Some(wait_hook) = wait_hook.take() {
467                        let _ = wait_hook.send(());
468                    }
469                    state = self
470                        .changed
471                        .wait(state)
472                        .expect("session state not poisoned");
473                }
474            }
475        };
476        drop(state);
477        Ok((message.decode()?, responder))
478    }
479
480    /// Replaces `Open` with `Closed` and fails pending promises under the state lock.
481    ///
482    /// Expired operations receive [`Error::Timeout`], and the rest receive the
483    /// closing error. Every call wakes `changed` and finishes any required
484    /// stream shutdown.
485    pub(super) fn close(&self, error: Error) {
486        // Publish closing results while preserving callbacks past the lock scope
487        let mut notifications = Notifications::default();
488        let (removed, pending, queued) = {
489            let mut state = self.state.lock().expect("session state not poisoned");
490            let (pending, queued) = state.workload();
491            (
492                state.close(error.clone(), self.now(), &mut notifications),
493                pending,
494                queued,
495            )
496        };
497
498        // Wake waiters and finish shutdown before running the callbacks
499        if removed.is_some() {
500            self.log_closed(&error, pending, queued);
501        }
502        self.finish_close(removed);
503    }
504
505    /// Logs the end of the session with its reason, the operations it failed
506    /// and the messages it dropped.
507    ///
508    /// Orderly endings are informational, and failures are warnings.
509    fn log_closed(&self, error: &Error, pending: usize, queued: usize) {
510        match error {
511            Error::Closed => tracing::info!(
512                "wire session {} closed locally (pending {}, queued {})",
513                self.log_id,
514                pending,
515                queued
516            ),
517            _ if error.orderly() => tracing::info!(
518                "wire session {} closed: {} (pending {}, queued {})",
519                self.log_id,
520                error.reason(),
521                pending,
522                queued
523            ),
524            _ => tracing::warn!(
525                "wire session {} failed: {} (pending {}, queued {})",
526                self.log_id,
527                error.reason(),
528                pending,
529                queued
530            ),
531        }
532    }
533
534    /// Drops queued work and wakes waiters outside the session lock.
535    fn finish_close(&self, removed: Option<State>) {
536        // Wake local waiters and drop queued work before adapter shutdown, which
537        // may wait for transport I/O already running to return
538        self.changed.notify_all();
539        drop(removed);
540        if let Some(stream_closer) = &self.stream_closer {
541            stream_closer.close();
542        }
543    }
544
545    /// Queues an `UNANSWERED` reply when a responder is dropped.
546    ///
547    /// The writer sends the reply later.
548    pub(super) fn reply_unanswered(self: &Arc<Self>, id: u64) {
549        self.autoreply(
550            id,
551            "unanswered",
552            schema::Error::reserved(
553                schema::ReservedErrors::Unanswered,
554                "request left unanswered",
555            ),
556        );
557    }
558
559    /// Queues an `UNKNOWN` reply to a request whose content this build does not
560    /// know, the application never seeing it.
561    ///
562    /// The writer sends the reply later.
563    fn reply_unknown(self: &Arc<Self>, id: u64) {
564        self.autoreply(
565            id,
566            "unknown",
567            schema::Error::reserved(schema::ReservedErrors::Unknown, "request not known"),
568        );
569    }
570
571    /// Queues an automatic reply under the configured autoreply timeout.
572    ///
573    /// The budget starts on entry, before acquiring the session lock, and
574    /// includes queueing. An unrepresentable deadline expires immediately
575    /// instead of panicking from `Drop`. A closed session queues nothing.
576    fn autoreply(self: &Arc<Self>, id: u64, reason: &str, error: schema::Error) {
577        // Start the budget on entry, then read the timeout of an open session
578        let now = self.now();
579        let timeout = {
580            let state = self.state.lock().expect("session state not poisoned");
581            match &*state {
582                State::Open {
583                    autoreply_timeout, ..
584                } => *autoreply_timeout,
585                State::Closed(_) => return,
586            }
587        };
588        tracing::debug!("answering request {} as {}", id, reason);
589
590        // Fix this reply's deadline on entry. Later changes to the session's
591        // configuration do not retime already submitted work.
592        let deadline = now.checked_add(timeout).unwrap_or(now);
593        let _ = self.reply(id, Err(error), deadline);
594    }
595
596    /// Creates a promise and queues a request through [`Self::enqueue`].
597    ///
598    /// If the deadline has already passed, the promise gets [`Error::Timeout`]
599    /// and nothing is queued.
600    pub(super) fn request(
601        self: &Arc<Self>,
602        request: Message,
603        deadline: Instant,
604    ) -> Result<Promise<Message>, Error> {
605        let (sender, promise) = Promise::pair(Arc::downgrade(self), deadline, true);
606        self.enqueue(
607            OutgoingBody::Request(request),
608            PendingOperation {
609                deadline,
610                sender,
611                log_id: None,
612            },
613        )?;
614        Ok(promise)
615    }
616
617    /// Queues a reply to `id` through [`Self::enqueue`].
618    ///
619    /// Its promise waits for the write and flush to finish, or fails if its
620    /// deadline expires first.
621    pub(super) fn reply(
622        self: &Arc<Self>,
623        id: u64,
624        result: Result<Message, schema::Error>,
625        deadline: Instant,
626    ) -> Result<Promise<()>, Error> {
627        let (sender, promise) = Promise::pair(Arc::downgrade(self), deadline, false);
628        self.enqueue(
629            OutgoingBody::Reply { id, result },
630            PendingOperation {
631                deadline,
632                sender,
633                log_id: Some(LogId::from(id)),
634            },
635        )?;
636        Ok(promise)
637    }
638
639    /// Adds a [`PendingOperation`] and its [`OutgoingMessage`] under the state lock.
640    ///
641    /// A closed session returns its error directly. An expired deadline fails
642    /// the promise instead, releasing the ID of an expired reply.
643    fn enqueue(
644        self: &Arc<Self>,
645        body: OutgoingBody,
646        operation: PendingOperation,
647    ) -> Result<(), Error> {
648        // Keep callbacks outside the lock even when admission expires immediately
649        let mut notifications = Notifications::default();
650        {
651            let mut state = self.state.lock().expect("session state not poisoned");
652            let (operations, outgoing, reserved_ids) = match &mut *state {
653                State::Open {
654                    operations,
655                    outgoing,
656                    reserved_ids,
657                    ..
658                } => (operations, outgoing, reserved_ids),
659                State::Closed(error) => return Err(error.clone()),
660            };
661
662            // Fail an expired operation at once, releasing a reply's reserved ID
663            let now = self.now();
664            if now >= operation.deadline {
665                if let OutgoingBody::Reply { id, .. } = body {
666                    reserved_ids.remove(&id);
667                }
668                notifications.push(operation.fail(Error::Timeout, now));
669                return Ok(());
670            }
671
672            // Queue the message and track its operation under a fresh key
673            let key = OperationKey::new();
674            outgoing.push_back(OutgoingMessage {
675                body,
676                operation: OperationHandle {
677                    session: Arc::downgrade(self),
678                    key: key.clone(),
679                },
680                #[cfg(any(test, feature = "fuzz"))]
681                deadline: operation.deadline,
682            });
683            operations.insert(key, operation);
684        }
685
686        // Wake the writer and deadline worker after publishing the queued work
687        self.changed.notify_all();
688        Ok(())
689    }
690
691    /// Fails expired operations and removes their queued messages.
692    ///
693    /// A promise waiter can call this if the deadline worker has not yet
694    /// processed its timeout. Requests already sent remain in `outstanding`
695    /// until answered or closed.
696    pub(super) fn expire(&self) {
697        // Drop the state guard before notifying completed promises
698        let mut notifications = Notifications::default();
699        let mut state = self.state.lock().expect("session state not poisoned");
700        state.expire(self.now(), &mut notifications);
701    }
702
703    /// Returns the earliest pending operation deadline for scenario assertions.
704    #[cfg(any(test, feature = "fuzz"))]
705    pub(super) fn next_deadline(&self) -> Option<Instant> {
706        let state = self.state.lock().expect("session state not poisoned");
707        match &*state {
708            State::Open { operations, .. } => operations
709                .values()
710                .map(|operation| operation.deadline)
711                .min(),
712            State::Closed(_) => None,
713        }
714    }
715
716    /// Takes the next unexpired [`OutgoingMessage`] for tests that drive writing
717    /// themselves.
718    #[cfg(any(test, feature = "fuzz"))]
719    pub(super) fn take_outgoing(&self) -> Option<OutgoingMessage> {
720        // Settle expired operations before selecting queued work
721        let mut notifications = Notifications::default();
722        let mut state = self.state.lock().expect("session state not poisoned");
723        state.expire(self.now(), &mut notifications);
724
725        // Release a reply's reservation when the writer takes it
726        match &mut *state {
727            State::Open {
728                outgoing,
729                reserved_ids,
730                ..
731            } => {
732                let message = outgoing.pop_front()?;
733                if let OutgoingBody::Reply { id, .. } = &message.body {
734                    reserved_ids.remove(id);
735                }
736                Some(message)
737            }
738            State::Closed(_) => None,
739        }
740    }
741
742    /// Records a write result under the state lock.
743    ///
744    /// A successful request write leaves its operation waiting for an answer,
745    /// while a reply write completes it. A failed or late write fails the
746    /// promise.
747    pub(super) fn record_write(&self, key: &OperationKey, result: Result<(), Error>) {
748        // Find the operation while deferring callbacks past the state guard
749        let mut notifications = Notifications::default();
750        let mut state = self.state.lock().expect("session state not poisoned");
751        let State::Open { operations, .. } = &mut *state else {
752            return;
753        };
754        let Some(operation) = operations.get(key) else {
755            return;
756        };
757
758        // Publish failures and reply completion while requests keep awaiting answers
759        let now = self.now();
760        if now >= operation.deadline || result.is_err() {
761            let operation = operations.remove(key).expect("operation held under lock");
762            notifications.push(operation.fail(result.err().unwrap_or(Error::Timeout), now));
763        } else if !operation.sender.response {
764            let operation = operations.remove(key).expect("operation held under lock");
765            notifications.push(operation.sender.send(Ok(PromiseResult::Written)));
766        }
767    }
768
769    /// Supplies a response to a fixture operation without depending on wire IDs.
770    ///
771    /// The fixture still encodes and retains bytes, matching real promise behavior.
772    #[cfg(any(test, feature = "fuzz"))]
773    pub(super) fn record_response(
774        self: &Arc<Self>,
775        key: &OperationKey,
776        result: Result<Message, Error>,
777    ) {
778        // Remove the fixture operation and defer its callback past the lock
779        let mut notifications = Notifications::default();
780        let mut state = self.state.lock().expect("session state not poisoned");
781        let State::Open {
782            operations,
783            max_inbound_bytes,
784            ..
785        } = &mut *state
786        else {
787            return;
788        };
789        let Some(operation) = operations.remove(key) else {
790            return;
791        };
792        let result = match result {
793            Ok(message) => Ok(message),
794            Err(Error::Remote(error)) => Err(error),
795            Err(error) => {
796                notifications.push(operation.fail(error, self.now()));
797                return;
798            }
799        };
800
801        // Encode the fixture response through the same envelope admission as the reader
802        let peer = match self.side {
803            Side::Client => Side::Server,
804            Side::Server => Side::Client,
805        };
806        let bytes = peer
807            .encode(0, result)
808            .expect("fixture response belongs to the peer");
809        let bytes = Bytes::from(bytes.into_boxed_slice());
810        let header = self
811            .side
812            .decode_header(bytes.clone())
813            .expect("fixture response has a valid envelope");
814        let result = operation.complete_response(self.now(), &mut notifications, || {
815            self.retain_incoming(bytes, header, *max_inbound_bytes)
816        });
817
818        // Close on admission failure after releasing the session lock
819        drop(state);
820        if let Err(error) = result {
821            self.close(error);
822        }
823    }
824
825    /// Returns the stream clock's time.
826    ///
827    /// Deadline checks hold `state`, and [`Self::autoreply`] also calls this
828    /// before waiting for that lock.
829    fn now(&self) -> Instant {
830        self.clock.now()
831    }
832
833    /// Checks the outer envelope and routes its original bytes.
834    ///
835    /// [`Self::recv`] and [`Promise::wait`] decode nested payloads. Unmatched and
836    /// late responses are discarded without decoding their bodies. The reader
837    /// closes the session on error.
838    pub(super) fn handle_message(self: &Arc<Self>, bytes: Vec<u8>) -> Result<(), Error> {
839        // Decode routing metadata before taking the session lock
840        let bytes = Bytes::from(bytes.into_boxed_slice());
841        let header = self.side.decode_header(bytes.clone())?;
842        let mut unknown = None;
843
844        // Publish accepted responses while retaining callbacks beyond the lock scope
845        let mut notifications = Notifications::default();
846        {
847            let mut state = self.state.lock().expect("session state not poisoned");
848            match MessageKind::from_id(header.id, self.side.into()) {
849                MessageKind::Request => {
850                    if header.failed {
851                        return Err(self.side.malformed(
852                            Some(header),
853                            bytes.len(),
854                            "envelope",
855                            "request contains an error",
856                        ));
857                    }
858                    // Unknown content reserves a slot for its automatic reply,
859                    // which is queued once this lock is released
860                    self.admit_request(&mut state, header, bytes)?;
861                    if header.unknown {
862                        unknown = Some(header.id);
863                    }
864                }
865                MessageKind::Response => {
866                    let State::Open {
867                        operations,
868                        outstanding,
869                        max_inbound_bytes,
870                        ..
871                    } = &mut *state
872                    else {
873                        let State::Closed(error) = &*state else {
874                            unreachable!()
875                        };
876                        return Err(error.clone());
877                    };
878
879                    // A sent request keeps its ID here until answered, so an ID
880                    // without an operation belongs to a request that timed out
881                    if let Some(key) = outstanding.remove(&header.id) {
882                        if let Some(operation) = operations.remove(&key) {
883                            tracing::trace!(
884                                "received response {} ({})",
885                                header.id,
886                                header.payload.unwrap_or("none")
887                            );
888                            operation.complete_response(self.now(), &mut notifications, || {
889                                self.retain_incoming(bytes, header, *max_inbound_bytes)
890                            })?;
891                        } else {
892                            tracing::debug!("discarding late response {}", header.id);
893                        }
894                    } else {
895                        tracing::warn!("discarding unmatched response {}", header.id);
896                    }
897                }
898            }
899        }
900
901        // Queue automatic replies and wake workers after releasing the state lock
902        if let Some(id) = unknown {
903            self.reply_unknown(id);
904        }
905        self.changed.notify_all();
906        Ok(())
907    }
908
909    /// Reserves a request slot and buffers known content under the session lock.
910    ///
911    /// Unknown content keeps only its ID for the automatic reply. If a limit is
912    /// exceeded, admission leaves the queues untouched. It never waits for the
913    /// application.
914    fn admit_request(
915        self: &Arc<Self>,
916        state: &mut State,
917        header: Header,
918        bytes: Bytes,
919    ) -> Result<(), Error> {
920        let State::Open {
921            incoming,
922            reserved_ids,
923            max_inbound_requests,
924            max_inbound_bytes,
925            ..
926        } = state
927        else {
928            let State::Closed(error) = state else {
929                unreachable!()
930            };
931            return Err(error.clone());
932        };
933
934        // Refuse an ID still in use, then a request beyond the limit
935        if reserved_ids.contains(&header.id) {
936            return Err(self.side.malformed(
937                Some(header),
938                bytes.len(),
939                "envelope",
940                "duplicate request ID",
941            ));
942        }
943        let id = header.id;
944        if reserved_ids.len() >= *max_inbound_requests {
945            tracing::warn!(
946                "inbound request limit exceeded (id: {}, used: {}, limit: {})",
947                id,
948                reserved_ids.len(),
949                max_inbound_requests
950            );
951            return Err(Error::InboundRequestLimitExceeded(*max_inbound_requests));
952        }
953
954        // Buffer known content, while unknown content keeps only its ID
955        let payload = header.payload.unwrap_or("none");
956        let message = if header.unknown {
957            None
958        } else {
959            Some(self.retain_incoming(bytes, header, *max_inbound_bytes)?)
960        };
961        reserved_ids.insert(id);
962        if let Some(message) = message {
963            incoming.push_back((id, message));
964        }
965        tracing::trace!("received request {} ({})", id, payload);
966        Ok(())
967    }
968
969    /// Waits for and takes the next queued message, or returns `None` on closure.
970    ///
971    /// Under the state lock, it assigns each request an ID and records its
972    /// operation key in `outstanding`. [`Self::handle_message`] can then match
973    /// the peer's response, even one arriving before the write finishes.
974    ///
975    /// # Panics
976    ///
977    /// Panics once the session has used up its request IDs, which aborts the
978    /// process when the writer calls it.
979    pub(super) fn next_outgoing(&self) -> Option<(u64, OutgoingMessage)> {
980        // Keep callback ownership outside the lock across every queue check
981        let mut notifications = Notifications::default();
982        let mut state = self.state.lock().expect("session state not poisoned");
983        loop {
984            // Run expiry callbacks before selecting work or waiting again
985            state.expire(self.now(), &mut notifications);
986            if !notifications.is_empty() {
987                drop(state);
988                drop(notifications);
989                notifications = Notifications::default();
990                state = self.state.lock().expect("session state not poisoned");
991                continue;
992            }
993
994            // Assign an ID and reserve the response route before releasing the lock
995            let State::Open {
996                outgoing,
997                next_id,
998                outstanding,
999                operations,
1000                reserved_ids,
1001                ..
1002            } = &mut *state
1003            else {
1004                return None;
1005            };
1006            if let Some(outgoing) = outgoing.pop_front() {
1007                let id = match &outgoing.body {
1008                    OutgoingBody::Request(_) => {
1009                        let id = next_id.expect("wire request IDs exhausted");
1010                        *next_id = id.checked_add(2);
1011                        // Store the ID before releasing the lock, since a response
1012                        // can arrive before the outgoing send finishes locally
1013                        outstanding.insert(id, outgoing.operation.key.clone());
1014                        if let Some(operation) = operations.get_mut(&outgoing.operation.key) {
1015                            operation.log_id = Some(LogId::from(id));
1016                        }
1017                        id
1018                    }
1019                    OutgoingBody::Reply { id, .. } => {
1020                        // The peer may receive this reply and reuse the ID before
1021                        // our flush returns. Finishing this write must not remove
1022                        // a newer request that reuses the same ID.
1023                        reserved_ids.remove(id);
1024                        *id
1025                    }
1026                };
1027                return Some((id, outgoing));
1028            }
1029
1030            // Sleep until work arrives or the session closes
1031            state = self
1032                .changed
1033                .wait(state)
1034                .expect("session state not poisoned");
1035        }
1036    }
1037
1038    /// Sends queued messages through `sender` until the session closes.
1039    ///
1040    /// Transport errors close the session, while messages that cannot be encoded
1041    /// fail only their own promise. On exit, it disconnects the transport
1042    /// session this sender belongs to.
1043    fn run_writer(&self, sender: transport::Sender<impl Write>) {
1044        while let Some((id, outgoing)) = self.next_outgoing() {
1045            // `next_outgoing` released the state lock. The reader and deadline
1046            // worker can continue while encoding or sending this message blocks.
1047            let request = matches!(outgoing.body, OutgoingBody::Request(_));
1048            let kind = if request { "request" } else { "reply" };
1049            let body = match outgoing.body {
1050                OutgoingBody::Request(body) => Ok(body),
1051                OutgoingBody::Reply { result, .. } => result,
1052            };
1053            let payload = match &body {
1054                Ok(message) => message.field_name(),
1055                Err(_) => "err",
1056            };
1057            let result = self.side.encode(id, body).and_then(|bytes| {
1058                // Announced before the transport confirms the write, so a
1059                // message reads top down in the log
1060                tracing::trace!("sending {} {} ({})", kind, id, payload);
1061                sender.send(&bytes).map_err(Error::from)
1062            });
1063
1064            // Close the session on a wire failure, and log a local refusal
1065            match &result {
1066                Ok(()) => {}
1067                Err(Error::Transport(error)) => {
1068                    // Wire failure ends the session even if this operation's promise
1069                    // has already timed out while the transport write was blocked
1070                    self.close(Error::Transport(error.clone()));
1071                    break;
1072                }
1073                Err(error) => tracing::debug!("not sending {} {}: {}", kind, id, error),
1074            }
1075
1076            {
1077                // Remaining failures are local encoding refusals. No request was
1078                // sent, so there is no future answer to retain an ID for.
1079                let mut state = self.state.lock().expect("session state not poisoned");
1080                if let State::Open { outstanding, .. } = &mut *state
1081                    && request
1082                    && result.is_err()
1083                {
1084                    outstanding.remove(&id);
1085                }
1086            }
1087
1088            // Report this operation's write result, if it is still pending.
1089            // A response or timeout may have completed it during the write.
1090            outgoing.operation.record_write(result);
1091        }
1092
1093        // Let replacement tests pause the writer before it disconnects
1094        #[cfg(any(test, feature = "fuzz"))]
1095        if let Some((entered, released)) = self.disconnect_hook.lock().unwrap().take() {
1096            let _ = entered.send(());
1097            let _ = released.recv();
1098        }
1099
1100        // Disconnect the session this sender belongs to. The transport ignores
1101        // this call if a new handshake has already replaced that session.
1102        if let Err(error) = sender.disconnect() {
1103            tracing::debug!(
1104                "failed to signal dropped session {}: {}",
1105                sender.log_id(),
1106                error
1107            );
1108        }
1109    }
1110
1111    /// Expires pending operations even when no caller is waiting on a promise.
1112    ///
1113    /// It waits on `changed` until the next deadline or until new work arrives,
1114    /// and returns once the session closes.
1115    pub(super) fn run_deadlines(&self) {
1116        // Keep callbacks outside the guard even when the session closes
1117        let mut notifications = Notifications::default();
1118        let mut state = self.state.lock().expect("session state not poisoned");
1119        loop {
1120            // Run settled callbacks before recomputing deadlines or waiting again
1121            state.expire(self.now(), &mut notifications);
1122            if !notifications.is_empty() {
1123                drop(state);
1124                drop(notifications);
1125                notifications = Notifications::default();
1126                state = self.state.lock().expect("session state not poisoned");
1127                continue;
1128            }
1129            let State::Open { operations, .. } = &*state else {
1130                return;
1131            };
1132
1133            // Submitting an earlier deadline wakes this wait. Every wakeup
1134            // recomputes the minimum under the same lock used by submission.
1135            state = match operations
1136                .values()
1137                .map(|operation| operation.deadline)
1138                .min()
1139            {
1140                Some(deadline) => {
1141                    self.changed
1142                        .wait_deadline(state, deadline)
1143                        .expect("session state not poisoned")
1144                        .0
1145                }
1146                None => self
1147                    .changed
1148                    .wait(state)
1149                    .expect("session state not poisoned"),
1150            };
1151        }
1152    }
1153}
1154
1155impl Session {
1156    /// Creates the session and starts its writer and deadline threads.
1157    ///
1158    /// Only client sessions receive a stream closer, since
1159    /// [`Server`](super::Server) closes server streams.
1160    pub(super) fn start<W: Write + Send + 'static>(
1161        side: Side,
1162        clock: Clock,
1163        sender: transport::Sender<W>,
1164        stream_closer: Option<transport::Closer>,
1165        #[cfg(any(test, feature = "fuzz"))] workers: Arc<worker::Tracker>,
1166    ) -> Self {
1167        // Keep the stream clock with the session's queues and operations
1168        let session = Self {
1169            inner: Arc::new(SessionInner::new(
1170                side,
1171                clock,
1172                sender.log_id(),
1173                stream_closer,
1174                #[cfg(any(test, feature = "fuzz"))]
1175                workers.clone(),
1176            )),
1177        };
1178
1179        // Start the writer independently of the thread reading the stream
1180        let inner = session.inner.clone();
1181        worker::spawn(
1182            "wire-writer",
1183            #[cfg(any(test, feature = "fuzz"))]
1184            &workers,
1185            move || inner.run_writer(sender),
1186        );
1187
1188        // Expire operations even when their promises have no waiter
1189        let inner = session.inner.clone();
1190        worker::spawn(
1191            "wire-deadlines",
1192            #[cfg(any(test, feature = "fuzz"))]
1193            &workers,
1194            move || inner.run_deadlines(),
1195        );
1196        session
1197    }
1198}
1199
1200impl State {
1201    /// Counts the pending operations and the queued messages of an open session.
1202    fn workload(&self) -> (usize, usize) {
1203        match self {
1204            Self::Open {
1205                operations,
1206                outgoing,
1207                incoming,
1208                ..
1209            } => (operations.len(), outgoing.len() + incoming.len()),
1210            Self::Closed(_) => (0, 0),
1211        }
1212    }
1213
1214    /// Stops accepting messages and fails pending operations under the session lock.
1215    ///
1216    /// Returns the old queues to be dropped after releasing the lock. An already
1217    /// closed session keeps its first reason and returns `None`.
1218    fn close(
1219        &mut self,
1220        error: Error,
1221        now: Instant,
1222        notifications: &mut Notifications,
1223    ) -> Option<Self> {
1224        // Preserve the first closing reason
1225        if let Self::Closed(_) = self {
1226            return None;
1227        }
1228
1229        // Publish failures now and let the caller run callbacks after unlocking
1230        let mut removed = std::mem::replace(self, Self::Closed(error.clone()));
1231        if let Self::Open { operations, .. } = &mut removed {
1232            for (_, operation) in operations.drain() {
1233                notifications.push(operation.fail(error.clone(), now));
1234            }
1235        }
1236        Some(removed)
1237    }
1238
1239    /// Removes expired entries from `operations`, sends [`Error::Timeout`] to
1240    /// their promises, and discards any messages they still have in `outgoing`.
1241    fn expire(&mut self, now: Instant, notifications: &mut Notifications) {
1242        if let Self::Open {
1243            operations,
1244            outgoing,
1245            reserved_ids,
1246            ..
1247        } = self
1248        {
1249            // Remove each expired operation before publishing its terminal result
1250            let expired: Vec<_> = operations
1251                .iter()
1252                .filter(|(_, operation)| now >= operation.deadline)
1253                .map(|(key, _)| key.clone())
1254                .collect();
1255            for key in expired {
1256                notifications.push(
1257                    operations
1258                        .remove(&key)
1259                        .expect("expired operation held under lock")
1260                        .fail(Error::Timeout, now),
1261                );
1262            }
1263
1264            // Only messages still in this queue can be discarded. Writes already
1265            // started keep running with their independent transport timeout.
1266            outgoing.retain(|outgoing| {
1267                let retained = operations.contains_key(&outgoing.operation.key);
1268                if !retained && let OutgoingBody::Reply { id, .. } = outgoing.body {
1269                    reserved_ids.remove(&id);
1270                }
1271                retained
1272            });
1273        }
1274    }
1275}
1276
1277// Workerless sessions let tests drive time, requests and writes themselves
1278#[cfg(any(test, feature = "fuzz"))]
1279impl Session {
1280    /// Creates a server session without a stream or workers.
1281    #[cfg(test)]
1282    pub(super) fn fixture() -> Self {
1283        Self::fixture_for(Side::Server)
1284    }
1285
1286    /// Creates a session for either side, without a stream or workers.
1287    #[cfg(test)]
1288    pub(super) fn fixture_for(side: Side) -> Self {
1289        Self::fixture_with_clock(side, crate::transport::testing::test_clock().clock())
1290    }
1291
1292    /// Creates a workerless session whose operations use the supplied clock.
1293    pub(super) fn fixture_with_clock(side: Side, clock: Clock) -> Self {
1294        Self {
1295            inner: Arc::new(SessionInner::new(
1296                side,
1297                clock,
1298                LogId::default(),
1299                None,
1300                Arc::new(worker::Tracker::default()),
1301            )),
1302        }
1303    }
1304}
1305
1306#[cfg(any(test, feature = "fuzz"))]
1307impl SessionInner {
1308    /// Arms a pause of the writer before it disconnects, so a test can connect a
1309    /// replacement session before letting the old writer finish.
1310    ///
1311    /// The returned receiver signals that the writer paused, and the sender
1312    /// releases it.
1313    pub(super) fn pause_disconnect(
1314        &self,
1315    ) -> (std::sync::mpsc::Receiver<()>, std::sync::mpsc::Sender<()>) {
1316        let (entered, observed) = std::sync::mpsc::channel();
1317        let (release, released) = std::sync::mpsc::channel();
1318        *self.disconnect_hook.lock().unwrap() = Some((entered, released));
1319        (observed, release)
1320    }
1321
1322    /// Returns a receiver notified when the last `Arc<SessionInner>` is dropped.
1323    pub(super) fn watch_drop(&self) -> std::sync::mpsc::Receiver<()> {
1324        let (sender, receiver) = std::sync::mpsc::channel();
1325        *self.drop_hook.lock().unwrap() = Some(sender);
1326        receiver
1327    }
1328
1329    /// Moves a fresh scenario session to its last allocatable request ID.
1330    ///
1331    /// # Panics
1332    ///
1333    /// Panics if the session is closed or has requests waiting for responses.
1334    pub(super) fn use_last_request_id(&self) {
1335        let mut state = self.state.lock().unwrap();
1336        let State::Open {
1337            next_id,
1338            outstanding,
1339            ..
1340        } = &mut *state
1341        else {
1342            panic!("open session required")
1343        };
1344        assert!(outstanding.is_empty());
1345        *next_id = Some(if self.side == Side::Client {
1346            u64::MAX
1347        } else {
1348            u64::MAX - 1
1349        });
1350    }
1351
1352    /// Waits for the reader to remove the ID of a request sent earlier.
1353    ///
1354    /// Tests use this to leave a completed promise unread before changing limits
1355    /// or sessions.
1356    ///
1357    /// # Panics
1358    ///
1359    /// Panics if the session closes before the reader removes the ID.
1360    pub(super) fn wait_response(&self, id: u64) {
1361        // Wait until the reader removes the request or the session closes
1362        let mut state = self.state.lock().unwrap();
1363        while matches!(&*state, State::Open { outstanding, .. } if outstanding.contains_key(&id)) {
1364            state = self.changed.wait(state).unwrap();
1365        }
1366
1367        // Require response processing before closure
1368        let State::Open { outstanding, .. } = &*state else {
1369            panic!("session closed before response fence");
1370        };
1371        assert!(
1372            !outstanding.contains_key(&id),
1373            "reader did not process response"
1374        );
1375    }
1376
1377    /// Returns the sorted request IDs still waiting for peer responses.
1378    ///
1379    /// # Panics
1380    ///
1381    /// Panics if the session is closed.
1382    pub(super) fn outstanding_ids(&self) -> Vec<u64> {
1383        let state = self.state.lock().unwrap();
1384        let State::Open { outstanding, .. } = &*state else {
1385            panic!("open session required")
1386        };
1387        let mut ids: Vec<_> = outstanding.keys().copied().collect();
1388        ids.sort_unstable();
1389        ids
1390    }
1391
1392    /// Supplies a request directly to the fixture's admission path, independently
1393    /// of parity.
1394    ///
1395    /// The connection scenarios cover wire routing. A failed admission closes
1396    /// the session and returns its error.
1397    pub(super) fn inject_request(self: &Arc<Self>, id: u64, message: Message) -> Result<(), Error> {
1398        // Encode the request as the peer would send it
1399        let peer = match self.side {
1400            Side::Client => Side::Server,
1401            Side::Server => Side::Client,
1402        };
1403        let bytes = peer
1404            .encode(id, Ok(message))
1405            .expect("fixture request belongs to peer");
1406        let bytes = Bytes::from(bytes.into_boxed_slice());
1407        let header = self.side.decode_header(bytes.clone())?;
1408
1409        // Admit it as the reader would, closing the session on failure
1410        let result = {
1411            let mut state = self.state.lock().expect("session state not poisoned");
1412            self.admit_request(&mut state, header, bytes)
1413        };
1414        self.changed.notify_all();
1415        if let Err(error) = &result {
1416            self.close(error.clone());
1417        }
1418        result
1419    }
1420
1421    /// Returns the accepted request count and retained byte count for test checks.
1422    pub(super) fn inbound_usage(&self) -> (usize, usize) {
1423        let state = self.state.lock().expect("session state not poisoned");
1424        let requests = match &*state {
1425            State::Open { reserved_ids, .. } => reserved_ids.len(),
1426            State::Closed(_) => 0,
1427        };
1428        (requests, self.retained_bytes.load(Ordering::Relaxed))
1429    }
1430
1431    /// Arms a one-shot notification for the next receive waiting on an empty queue.
1432    ///
1433    /// The notification is sent while holding the lock, immediately before the
1434    /// condition-variable wait releases it, so a later close cannot run too early.
1435    ///
1436    /// # Panics
1437    ///
1438    /// Panics if the fixture is closed or already has a queued request.
1439    pub(super) fn watch_recv_wait(&self) -> std::sync::mpsc::Receiver<()> {
1440        let (sender, receiver) = std::sync::mpsc::channel();
1441        let mut state = self.state.lock().expect("session state not poisoned");
1442        let State::Open {
1443            incoming,
1444            wait_hook,
1445            ..
1446        } = &mut *state
1447        else {
1448            panic!("only watch an open session receive");
1449        };
1450        assert!(incoming.is_empty());
1451        *wait_hook = Some(sender);
1452        receiver
1453    }
1454}
1455
1456#[cfg(any(test, feature = "fuzz"))]
1457impl Drop for SessionInner {
1458    /// Notifies the test when the last `Arc<SessionInner>` is dropped.
1459    fn drop(&mut self) {
1460        if let Some(sender) = self.drop_hook.get_mut().unwrap().take() {
1461            let _ = sender.send(());
1462        }
1463    }
1464}
1465
1466/// Checks callback lock release and session ownership bounds, and compiles the
1467/// client construction API.
1468#[cfg(test)]
1469#[cfg_attr(coverage_nightly, coverage(off))]
1470mod tests {
1471    use super::{Notifications, SessionInner, Side};
1472    use crate::protocol::{self, Error, Session};
1473    use crate::transport::{Read, Stream, Verifier, Write};
1474    use std::fmt::Debug;
1475
1476    /// Checks that the deadline worker releases both locks before notifying and
1477    /// waiting again.
1478    #[test]
1479    fn test_deadline_callback_releases_locks_before_worker_waits() {
1480        use std::sync::mpsc;
1481        use std::thread;
1482        use std::time::Duration;
1483
1484        // Register a callback before starting the fixture's only worker
1485        let mut tester = crate::transport::testing::test_clock();
1486        let session = Session::fixture_with_clock(Side::Client, tester.clock());
1487        let deadline = tester.clock().now() + Duration::from_secs(5);
1488        let mut promise = session.requester().request(vec![1], deadline).unwrap();
1489        let notification = promise.notification_unlocked();
1490        let state = session.inner.clone();
1491        let (observed, receiver) = mpsc::channel();
1492        promise.notify(move || {
1493            let _ = observed.send((state.state.try_lock().is_ok(), notification()));
1494        });
1495        let state = session.inner.clone();
1496        let worker = thread::spawn(move || state.run_deadlines());
1497
1498        // Reach the registered deadline without asking the promise to expire itself
1499        tester.wait_registered(1);
1500        tester.advance_to(deadline);
1501        let unlocked = receiver.recv().unwrap();
1502
1503        // Stop the worker and require unlocked notification with a timeout result
1504        session.close();
1505        worker.join().unwrap();
1506        assert_eq!(unlocked, (true, true));
1507        assert!(matches!(promise.wait::<Vec<u8>>(), Err(Error::Timeout)));
1508    }
1509
1510    /// Checks that callbacks and their captures run with both locks free, on the
1511    /// thread responsible for notification.
1512    #[test]
1513    fn test_notification_releases_locks_on_every_settlement_path() {
1514        use crate::protocol::Promise;
1515        use std::sync::{Arc, mpsc};
1516        use std::thread::{self, ThreadId};
1517        use std::time::Duration;
1518
1519        /// Capture checking callback execution and its own destruction against
1520        /// both locks.
1521        struct Capture {
1522            /// Session whose state must be unlocked during notification.
1523            session: Arc<SessionInner>,
1524            /// Probe attempting to acquire the promise's notification lock.
1525            notification: Box<dyn Fn() -> bool + Send>,
1526            /// Channel reporting lock availability and the executing thread.
1527            observed: mpsc::Sender<(bool, bool, ThreadId)>,
1528        }
1529
1530        impl Capture {
1531            /// Reports lock availability from application code without blocking.
1532            fn check(&self) {
1533                let _ = self.observed.send((
1534                    self.session.state.try_lock().is_ok(),
1535                    (self.notification)(),
1536                    thread::current().id(),
1537                ));
1538            }
1539        }
1540
1541        impl Drop for Capture {
1542            /// Checks locks again when the callback's captures are destroyed.
1543            fn drop(&mut self) {
1544                self.check();
1545            }
1546        }
1547
1548        /// Registers on either side of settlement and checks both callback contexts.
1549        fn check<T>(
1550            mut promise: Promise<T>,
1551            session: Arc<SessionInner>,
1552            completed: bool,
1553            path: &str,
1554            settle: impl FnOnce() + Send,
1555        ) {
1556            // Prepare a capture that checks both application execution points
1557            let (observed, receiver) = mpsc::channel();
1558            let capture = Capture {
1559                session,
1560                notification: Box::new(promise.notification_unlocked()),
1561                observed,
1562            };
1563
1564            // Settle on another thread to distinguish it from registration
1565            let expected = thread::scope(|scope| {
1566                if completed {
1567                    scope.spawn(settle).join().unwrap();
1568                    promise.notify(move || capture.check());
1569                    thread::current().id()
1570                } else {
1571                    promise.notify(move || capture.check());
1572                    let settling = scope.spawn(move || {
1573                        settle();
1574                        thread::current().id()
1575                    });
1576                    settling.join().unwrap()
1577                }
1578            });
1579
1580            // Require one unlocked callback and one unlocked capture destructor
1581            let observations: Vec<_> = receiver.try_iter().collect();
1582            assert_eq!(
1583                observations,
1584                vec![(true, true, expected); 2],
1585                "{path}, completed={completed}"
1586            );
1587        }
1588
1589        // Exercise result publication through each session path without workers
1590        for path in [
1591            "response",
1592            "response limit",
1593            "late response",
1594            "fixture response",
1595            "fixture error",
1596            "write failure",
1597            "late write",
1598            "reply",
1599            "close",
1600            "expire",
1601            "take outgoing",
1602            "next outgoing",
1603            "limits",
1604        ] {
1605            for completed in [false, true] {
1606                // Create a session whose deadlines follow a paused clock
1607                let mut tester = crate::transport::testing::test_clock();
1608                let clock = tester.clock();
1609                let deadline = clock.now() + Duration::from_secs(5);
1610                let mut session = Session::fixture_with_clock(Side::Client, clock);
1611                let inner = session.inner.clone();
1612
1613                // Complete a real responder through the writer's success path
1614                if path == "reply" {
1615                    inner.inject_request(2, vec![1].into()).unwrap();
1616                    let (_, responder) = session.recv().unwrap();
1617                    let promise = responder.reply(vec![2], deadline).unwrap();
1618                    let outgoing = inner.take_outgoing().unwrap();
1619                    check(promise, inner, completed, path, move || {
1620                        outgoing.operation.record_write(Ok(()))
1621                    });
1622                    continue;
1623                }
1624
1625                // Prepare requests and any admission limits before notification
1626                if path == "response limit" {
1627                    session = session.set_inbound_limits(1, 0);
1628                }
1629                if path == "limits" {
1630                    inner.inject_request(2, vec![1].into()).unwrap();
1631                }
1632                let promise = session.requester().request(vec![1], deadline).unwrap();
1633                let (id, outgoing) = inner.next_outgoing().unwrap();
1634                let next = if path == "next outgoing" {
1635                    Some(
1636                        session
1637                            .requester()
1638                            .request(vec![2], deadline + Duration::from_secs(5))
1639                            .unwrap(),
1640                    )
1641                } else {
1642                    None
1643                };
1644
1645                // Publish through the chosen path and verify registration before
1646                // or after it
1647                check(promise, inner.clone(), completed, path, || match path {
1648                    "response" | "response limit" | "late response" => {
1649                        // Let response completion detect expiry without the
1650                        // deadline worker
1651                        if path == "late response" {
1652                            tester.advance_to(deadline);
1653                        }
1654
1655                        // Route the response through the reader's publication path
1656                        let bytes = Side::Server.encode(id, Ok(vec![2].into())).unwrap();
1657                        let result = inner.handle_message(bytes);
1658                        assert_eq!(result.is_ok(), path != "response limit");
1659                    }
1660                    "fixture response" => outgoing.operation.record_response(Ok(vec![2].into())),
1661                    "fixture error" => {
1662                        inner.record_response(&outgoing.operation.key, Err(Error::Closed))
1663                    }
1664                    "write failure" => outgoing.operation.record_write(Err(Error::Closed)),
1665                    "late write" => {
1666                        tester.advance_to(deadline);
1667                        outgoing.operation.record_write(Ok(()));
1668                    }
1669                    "close" => inner.close(Error::Closed),
1670                    "limits" => {
1671                        let mut notifications = Notifications::default();
1672                        inner.set_inbound_limits(0, 1024, &mut notifications);
1673                    }
1674                    "expire" | "take outgoing" | "next outgoing" => {
1675                        tester.advance_to(deadline);
1676                        match path {
1677                            "expire" => inner.expire(),
1678                            "take outgoing" => assert!(inner.take_outgoing().is_none()),
1679                            "next outgoing" => assert!(inner.next_outgoing().is_some()),
1680                            _ => unreachable!(),
1681                        }
1682                    }
1683                    _ => unreachable!(),
1684                });
1685                drop(next);
1686            }
1687        }
1688    }
1689
1690    /// Compiles client construction with verifier-specific information in the result.
1691    #[allow(dead_code)]
1692    fn connect<R, W, V>(stream: Stream<R, W>, verifier: &V) -> Result<(Session, V::Info), Error>
1693    where
1694        R: Read + Send + 'static,
1695        W: Write + Send + 'static,
1696        V: Verifier,
1697    {
1698        protocol::connect(stream, verifier)
1699    }
1700
1701    /// Checks the bounds required to move the session to an application thread
1702    /// and to print it.
1703    #[test]
1704    fn test_thread_capabilities() {
1705        /// Requires an owned value to be printable and transferable to a
1706        /// background thread.
1707        fn movable<T: Debug + Send + 'static>() {}
1708        movable::<Session>();
1709    }
1710}