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