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//! Session state, request queues, and the reader, writer, and deadline workers.
5
6use super::envelope::{Header, IncomingEnvelope, MessageKind, Parity, Side};
7use super::operation::{
8    OperationHandle, OperationKey, OutgoingBody, OutgoingMessage, PendingOperation,
9};
10use super::promise::PromiseResult;
11use super::worker;
12use super::{
13    Closer, DEFAULT_ABANDONMENT_TIMEOUT, DEFAULT_MAX_INBOUND_BYTES, DEFAULT_MAX_INBOUND_REQUESTS,
14    Error, Message, Promise, RemoteError, Requester, ReservedErrors, Responder,
15};
16use crate::LogId;
17use crate::transport::{self, Read, Stream, Verifier, Write};
18use prost::bytes::Bytes;
19use std::collections::{HashMap, HashSet, VecDeque};
20use std::sync::atomic::{AtomicUsize, Ordering};
21use std::sync::{Arc, Condvar, Mutex};
22use std::time::{Duration, Instant};
23
24/// Establishes a client session, verifies the peer and returns the verifier's info.
25/// Takes ownership of the stream and constructs the transport internally. Failure
26/// closes the client stream; the application can open another stream and reconnect.
27/// The verifier is only borrowed during this blocking call.
28/// Failure to start a required worker or an escaping worker panic aborts the process.
29pub fn connect<R, W, V>(stream: Stream<R, W>, verifier: &V) -> Result<(Session, V::Info), Error>
30where
31    R: Read + Send + 'static,
32    W: Write + Send + 'static,
33    V: Verifier,
34{
35    let mut client = transport::Client::new(stream);
36
37    let (sender, info) = client.connect(verifier)?;
38    #[cfg(any(test, feature = "fuzz"))]
39    let workers = Arc::new(worker::Tracker::default());
40    let session = Session::start(
41        Side::Client,
42        sender,
43        Some(client.closer()),
44        #[cfg(any(test, feature = "fuzz"))]
45        workers.clone(),
46    );
47    let inner = session.inner.clone();
48
49    worker::spawn(
50        "wire-client-reader",
51        #[cfg(any(test, feature = "fuzz"))]
52        &workers,
53        move || run_reader(client, inner),
54    );
55    Ok((session, info))
56}
57
58/// Receives and handles messages for one client session. The reader retains its
59/// session state until it exits; closing the session shuts down the stream and
60/// wakes a blocked transport read.
61fn run_reader<R: Read, W: Write>(mut client: transport::Client<R, W>, session: Arc<SessionInner>) {
62    loop {
63        let result = client
64            .recv()
65            .map_err(Error::from)
66            .and_then(|bytes| session.handle_message(bytes));
67        if let Err(error) = result {
68            // Receive and decode failures close this client session and its
69            // stream, waking any other blocked transport I/O.
70            session.close(error);
71            break;
72        }
73    }
74}
75
76/// Owner of one session and its incoming request queue.
77///
78/// Incoming requests use [`Message`], paired with a common responder. The
79/// application selects the reply type; there is no static request/response pairing
80/// table. The host/server role and envelope direction are handled internally.
81///
82/// Closing or dropping the session fails pending promises, discards queued requests,
83/// and wakes blocked `recv()` calls. Completed promises keep their results.
84/// Application jobs keep running, but their handles still refer to the closed
85/// session and cannot send messages through a replacement session.
86///
87/// A client session also closes its stream. A server session leaves the server's
88/// stream available for another handshake. Handles do not keep the session open.
89/// The owner cannot be cloned; obtain requesters or closers for other threads:
90///
91/// ```compile_fail,E0599
92/// use darkbio_wire::protocol::Session;
93/// fn duplicate(session: Session) { let _ = session.clone(); }
94/// ```
95pub struct Session {
96    /// Queues and pending operations shared with this session's workers.
97    pub(super) inner: Arc<SessionInner>,
98}
99
100impl Session {
101    /// Sets the lifetime of automatic `UNANSWERED` replies when responders are
102    /// subsequently dropped. Defaults to [`DEFAULT_ABANDONMENT_TIMEOUT`]. Replies
103    /// already queued keep their deadlines.
104    ///
105    /// The budget starts when the responder is dropped and includes queueing.
106    /// Expiry discards a queued reply; a write already started still runs under the
107    /// transport's independent timeout. Explicit request/reply deadlines are
108    /// unaffected. Zero or an unrepresentable deadline expires immediately.
109    /// Use [`super::Server::set_abandonment_timeout`] to also set the timeout for
110    /// future server sessions.
111    pub fn set_abandonment_timeout(self, timeout: Duration) -> Self {
112        self.inner.set_abandonment_timeout(timeout);
113        self
114    }
115
116    /// Sets the maximum accepted peer requests and buffered incoming bytes together.
117    /// Defaults to [`DEFAULT_MAX_INBOUND_REQUESTS`] and [`DEFAULT_MAX_INBOUND_BYTES`].
118    ///
119    /// `requests` counts queued requests, held responders and queued replies.
120    /// A slot is freed when the writer takes the reply or the reply is discarded.
121    /// Zero refuses all peer requests but still allows responses to our requests.
122    ///
123    /// `bytes` counts the full encoded envelopes of queued requests and unread
124    /// responses. `recv()`, `wait()` or dropping a response promise releases that
125    /// space in the budget. Zero allows no buffered envelopes. Decoded application
126    /// data, outgoing messages and transport buffers are excluded.
127    ///
128    /// Exceeding either limit closes this session with
129    /// [`Error::InboundRequestLimitExceeded`] or [`Error::InboundByteLimitExceeded`].
130    /// The reader never waits for space. Lowering a limit below usage also closes
131    /// the session. Completed promises keep their results and bytes until read or
132    /// dropped. Raising limits does not reopen a closed session.
133    pub fn set_inbound_limits(self, requests: usize, bytes: usize) -> Self {
134        self.inner.set_inbound_limits(requests, bytes);
135        self
136    }
137
138    /// Returns a clonable requester bound to this session.
139    pub fn requester(&self) -> Requester {
140        Requester::new(Arc::downgrade(&self.inner))
141    }
142
143    /// Blocks for the next peer request and its [`Responder`]. Closing the session
144    /// wakes this call with the error that closed it. The caller decides how to
145    /// handle each request; this method does not run application callbacks.
146    ///
147    /// Taking a request removes its bytes from the inbound byte count before
148    /// decoding it. The request still counts toward the inbound request limit
149    /// while its responder is held.
150    /// Invalid protobuf returns [`Error::Malformed`] and closes this session.
151    ///
152    /// If another thread closes the session after `recv()` takes a request from
153    /// the queue, `recv()` can still return it. Replying after closure returns an error.
154    pub fn recv(&mut self) -> Result<(Message, Responder), Error> {
155        self.inner.recv()
156    }
157
158    /// Returns a clonable handle for closing this session from another thread,
159    /// including while its owner is blocked in [`Self::recv`].
160    pub fn closer(&self) -> Closer {
161        Closer::session(Arc::downgrade(&self.inner))
162    }
163
164    /// Closes this session. Repeated calls have no further effect. This does not
165    /// wait for application jobs or guarantee the peer has observed closure.
166    /// Discards queued messages and fails pending promises. A transport write
167    /// already started may still finish, but cannot change a completed promise's
168    /// result or affect a replacement session.
169    pub fn close(&self) {
170        self.inner.close(Error::Closed);
171    }
172}
173
174impl Drop for Session {
175    /// Closes the session even when requesters, responders or closers remain.
176    fn drop(&mut self) {
177        self.close();
178    }
179}
180
181/// Queues and pending operations for one session. Requesters, responders, and
182/// closers hold weak references to this object, even after a new session connects.
183pub(super) struct SessionInner {
184    /// Protects the queues, pending operations, and transition to `State::Closed`.
185    state: Mutex<State>,
186    /// Wakes `recv()`, the writer, and the deadline worker when `state` changes.
187    changed: Condvar,
188    /// Incoming bytes held by queued requests and unread responses. Accepting
189    /// messages and changing limits hold `state`. Consumers can release bytes
190    /// without that lock. Unread promises keep this counter alive after closure.
191    retained_bytes: Arc<AtomicUsize>,
192    /// Envelope direction and request parity, fixed for the whole session.
193    side: Side,
194    /// Label of the transport session in log lines, unset without a transport.
195    pub(super) log_id: LogId,
196    /// Closes the client's stream. Server sessions and tests without a stream
197    /// leave this empty; a server's stream is closed by `Server`.
198    stream_closer: Option<transport::Closer>,
199    /// Lets tests wait for worker threads to exit.
200    #[cfg(any(test, feature = "fuzz"))]
201    pub(super) workers: Arc<worker::Tracker>,
202    /// Controlled protocol time for scenarios; production always uses Instant::now.
203    #[cfg(any(test, feature = "fuzz"))]
204    time: Mutex<Option<Instant>>,
205    /// Notifies tests when the last `Arc<SessionInner>` is dropped.
206    #[cfg(any(test, feature = "fuzz"))]
207    drop_hook: Mutex<Option<std::sync::mpsc::Sender<()>>>,
208    /// Pauses the writer before `sender.disconnect()` in replacement tests.
209    #[cfg(any(test, feature = "fuzz"))]
210    disconnect_hook: Mutex<Option<(std::sync::mpsc::Sender<()>, std::sync::mpsc::Receiver<()>)>>,
211}
212
213/// An open session's queues, or the error that closed the session.
214// Keep the same inline state layout in tests; the wait hook crosses Clippy's
215// size threshold for the difference between variants.
216#[cfg_attr(any(test, feature = "fuzz"), allow(clippy::large_enum_variant))]
217enum State {
218    /// Holds queued messages and pending operations. `close()` replaces this
219    /// with `Closed`, then drops the queues after releasing the state lock.
220    Open {
221        /// Ceiling for accepted requests, including application-held responders.
222        max_inbound_requests: usize,
223        /// Ceiling for encoded requests and unread response promises.
224        max_inbound_bytes: usize,
225        /// Automatic reply lifetime selected when a responder is dropped.
226        abandonment: Duration,
227
228        /// Peer requests awaiting application receipt, paired with their request IDs.
229        incoming: VecDeque<(u64, IncomingEnvelope)>,
230        /// Rejects duplicate incoming IDs while the receive queue, responder,
231        /// or queued reply holds them. Taking a reply into the writer or
232        /// discarding an expired reply releases its ID. Release happens before
233        /// writing: the peer can receive the reply and reuse its ID before our
234        /// local flush returns.
235        reserved_ids: HashSet<u64>,
236
237        /// Requests and replies waiting for the writer to take them.
238        outgoing: VecDeque<OutgoingMessage>,
239        /// Next locally allocated ID, or exhaustion. Never wraps or reuses an ID.
240        next_id: Option<u64>,
241        /// Maps outgoing request IDs to operation keys. Entries remain after
242        /// a promise times out, until the peer responds or the session closes.
243        outstanding: HashMap<u64, OperationKey>,
244        /// Operations waiting to send a result to their promise. Each is removed
245        /// before sending that result, so a promise is completed only once.
246        operations: HashMap<OperationKey, PendingOperation>,
247
248        /// One-shot test notification sent under the state lock before waiting.
249        #[cfg(any(test, feature = "fuzz"))]
250        wait_hook: Option<std::sync::mpsc::Sender<()>>,
251    },
252    /// Keeps the first closing reason for every subsequent operation.
253    Closed(Error),
254}
255
256impl SessionInner {
257    /// Creates empty queues and pending-operation maps before starting workers.
258    fn new(
259        side: Side,
260        log_id: LogId,
261        stream_closer: Option<transport::Closer>,
262        #[cfg(any(test, feature = "fuzz"))] workers: Arc<worker::Tracker>,
263    ) -> Self {
264        Self {
265            state: Mutex::new(State::Open {
266                max_inbound_requests: DEFAULT_MAX_INBOUND_REQUESTS,
267                max_inbound_bytes: DEFAULT_MAX_INBOUND_BYTES,
268                abandonment: DEFAULT_ABANDONMENT_TIMEOUT,
269
270                incoming: VecDeque::new(),
271                reserved_ids: HashSet::new(),
272
273                outgoing: VecDeque::new(),
274                next_id: Some(Parity::from(side).first()),
275                outstanding: HashMap::new(),
276                operations: HashMap::new(),
277
278                #[cfg(any(test, feature = "fuzz"))]
279                wait_hook: None,
280            }),
281            changed: Condvar::new(),
282            retained_bytes: Arc::new(AtomicUsize::new(0)),
283            side,
284            log_id,
285            stream_closer,
286            #[cfg(any(test, feature = "fuzz"))]
287            workers,
288            #[cfg(any(test, feature = "fuzz"))]
289            time: Mutex::new(None),
290            #[cfg(any(test, feature = "fuzz"))]
291            drop_hook: Mutex::new(None),
292            #[cfg(any(test, feature = "fuzz"))]
293            disconnect_hook: Mutex::new(None),
294        }
295    }
296
297    /// Changes the timeout used when responders are dropped. Queued replies keep
298    /// their original deadlines.
299    pub(super) fn set_abandonment_timeout(&self, timeout: Duration) {
300        let mut state = self.state.lock().expect("session state not poisoned");
301        if let State::Open { abandonment, .. } = &mut *state {
302            *abandonment = timeout;
303        }
304    }
305
306    /// Updates both limits and closes the session if usage is too high. Holds the
307    /// same lock used to accept incoming messages.
308    pub(super) fn set_inbound_limits(&self, requests: usize, bytes: usize) {
309        let removed = {
310            let mut state = self.state.lock().expect("session state not poisoned");
311            let State::Open {
312                max_inbound_requests,
313                max_inbound_bytes,
314                reserved_ids,
315                ..
316            } = &mut *state
317            else {
318                return;
319            };
320            *max_inbound_requests = requests;
321            *max_inbound_bytes = bytes;
322            let error = if reserved_ids.len() > *max_inbound_requests {
323                Some(Error::InboundRequestLimitExceeded(*max_inbound_requests))
324            } else if self.retained_bytes.load(Ordering::Relaxed) > *max_inbound_bytes {
325                Some(Error::InboundByteLimitExceeded(*max_inbound_bytes))
326            } else {
327                None
328            };
329            error.and_then(|error| {
330                let (pending, queued) = state.workload();
331                let removed = state.close(error.clone(), self.now());
332                if removed.is_some() {
333                    self.log_closed(&error, pending, queued);
334                }
335                removed
336            })
337        };
338        if removed.is_some() {
339            self.finish_close(removed);
340        }
341    }
342
343    /// Counts the envelope's original bytes under the session lock. Decoding or
344    /// dropping it later releases those bytes to this session's counter.
345    fn retain_incoming(
346        self: &Arc<Self>,
347        bytes: Bytes,
348        header: Header,
349        limit: usize,
350    ) -> Result<IncomingEnvelope, Error> {
351        IncomingEnvelope::new(
352            bytes,
353            header,
354            &self.retained_bytes,
355            limit,
356            self.side,
357            Arc::downgrade(self),
358        )
359    }
360
361    /// Takes the next request from `incoming` and creates its responder. If the
362    /// queue is empty, waits on `changed`. Closing the session wakes the wait and
363    /// returns the error stored in `State::Closed`.
364    /// Decodes after releasing the lock so the reader can keep receiving messages.
365    fn recv(self: &Arc<Self>) -> Result<(Message, Responder), Error> {
366        let mut state = self.state.lock().expect("session state not poisoned");
367        let (message, responder) = loop {
368            match &mut *state {
369                State::Closed(error) => return Err(error.clone()),
370                State::Open {
371                    incoming,
372                    #[cfg(any(test, feature = "fuzz"))]
373                    wait_hook,
374                    ..
375                } => {
376                    if let Some((id, message)) = incoming.pop_front() {
377                        break (message, Responder::new(Arc::downgrade(self), id));
378                    }
379                    #[cfg(any(test, feature = "fuzz"))]
380                    if let Some(wait_hook) = wait_hook.take() {
381                        let _ = wait_hook.send(());
382                    }
383                    state = self
384                        .changed
385                        .wait(state)
386                        .expect("session state not poisoned");
387                }
388            }
389        };
390        drop(state);
391        Ok((message.decode()?, responder))
392    }
393
394    /// Replaces `Open` with `Closed` and fails pending promises under the state lock.
395    /// Expired operations receive `Timeout`; the rest receive the closing error.
396    /// Every call wakes `changed` and finishes any required stream shutdown.
397    pub(super) fn close(&self, error: Error) {
398        let (removed, pending, queued) = {
399            let mut state = self.state.lock().expect("session state not poisoned");
400            let (pending, queued) = state.workload();
401            (state.close(error.clone(), self.now()), pending, queued)
402        };
403        if removed.is_some() {
404            self.log_closed(&error, pending, queued);
405        }
406        self.finish_close(removed);
407    }
408
409    /// Logs the end of the session with its reason, the operations it failed
410    /// and the messages it dropped. Orderly endings are informational.
411    fn log_closed(&self, error: &Error, pending: usize, queued: usize) {
412        match error {
413            Error::Closed => tracing::info!(
414                "wire session {} closed locally (pending {}, queued {})",
415                self.log_id,
416                pending,
417                queued
418            ),
419            _ if error.orderly() => tracing::info!(
420                "wire session {} closed: {} (pending {}, queued {})",
421                self.log_id,
422                error.reason(),
423                pending,
424                queued
425            ),
426            _ => tracing::warn!(
427                "wire session {} failed: {} (pending {}, queued {})",
428                self.log_id,
429                error.reason(),
430                pending,
431                queued
432            ),
433        }
434    }
435
436    /// Drops queued work and wakes waiters outside the session lock.
437    fn finish_close(&self, removed: Option<State>) {
438        // Wake local waiters and drop queued work before adapter shutdown, which
439        // may wait for transport I/O already running to return.
440        self.changed.notify_all();
441        drop(removed);
442        if let Some(stream_closer) = &self.stream_closer {
443            stream_closer.close();
444        }
445    }
446
447    /// Queues an `UNANSWERED` reply when a responder is dropped. The
448    /// budget starts on entry, before acquiring the session lock, and includes
449    /// queueing. An unrepresentable deadline expires immediately instead of
450    /// panicking from `Drop`. The writer sends the reply later.
451    pub(super) fn reply_unanswered(self: &Arc<Self>, id: u64) {
452        let now = self.now();
453        let timeout = {
454            let state = self.state.lock().expect("session state not poisoned");
455            match &*state {
456                State::Open {
457                    abandonment: abandonment_timeout,
458                    ..
459                } => *abandonment_timeout,
460                State::Closed(_) => return,
461            }
462        };
463        tracing::debug!("answering request {} as unanswered", id);
464
465        // Fix this reply's deadline at drop. Later changes to the session's
466        // configuration do not retime already submitted work.
467        let deadline = now.checked_add(timeout).unwrap_or(now);
468        let _ = self.reply(
469            id,
470            Err(RemoteError::reserved(
471                ReservedErrors::Unanswered,
472                "request left unanswered",
473            )),
474            deadline,
475        );
476    }
477
478    /// Creates a promise and queues a request through `enqueue()`. If the deadline
479    /// has already passed, the promise gets `Timeout` and nothing is queued.
480    pub(super) fn request(
481        self: &Arc<Self>,
482        request: Message,
483        deadline: Instant,
484    ) -> Result<Promise<Message>, Error> {
485        let (sender, promise) = Promise::pair(Arc::downgrade(self), deadline, true);
486        self.enqueue(
487            OutgoingBody::Request(request),
488            PendingOperation {
489                deadline,
490                sender,
491                log_id: None,
492            },
493        )?;
494        Ok(promise)
495    }
496
497    /// Queues a reply to `id` through `enqueue()`. Its promise waits for the write
498    /// and flush to finish, or fails if its deadline expires first.
499    pub(super) fn reply(
500        self: &Arc<Self>,
501        id: u64,
502        result: Result<Message, RemoteError>,
503        deadline: Instant,
504    ) -> Result<Promise<()>, Error> {
505        let (sender, promise) = Promise::pair(Arc::downgrade(self), deadline, false);
506        self.enqueue(
507            OutgoingBody::Reply { id, result },
508            PendingOperation {
509                deadline,
510                sender,
511                log_id: Some(LogId::from(id)),
512            },
513        )?;
514        Ok(promise)
515    }
516
517    /// Adds a `PendingOperation` and its `OutgoingMessage` under the state lock.
518    /// A closed session returns its error directly; an expired deadline fails
519    /// the promise.
520    fn enqueue(
521        self: &Arc<Self>,
522        body: OutgoingBody,
523        operation: PendingOperation,
524    ) -> Result<(), Error> {
525        {
526            let mut state = self.state.lock().expect("session state not poisoned");
527            let (operations, outgoing, reserved_ids) = match &mut *state {
528                State::Open {
529                    operations,
530                    outgoing,
531                    reserved_ids,
532                    ..
533                } => (operations, outgoing, reserved_ids),
534                State::Closed(error) => return Err(error.clone()),
535            };
536            let now = self.now();
537            if now >= operation.deadline {
538                if let OutgoingBody::Reply { id, .. } = body {
539                    reserved_ids.remove(&id);
540                }
541                operation.fail(Error::Timeout, now);
542                return Ok(());
543            }
544            let key = OperationKey::new();
545            outgoing.push_back(OutgoingMessage {
546                body,
547                operation: OperationHandle {
548                    session: Arc::downgrade(self),
549                    key: key.clone(),
550                },
551                #[cfg(any(test, feature = "fuzz"))]
552                deadline: operation.deadline,
553            });
554            operations.insert(key, operation);
555        }
556        self.changed.notify_all();
557        Ok(())
558    }
559
560    /// Fails expired operations and removes their queued messages. A promise waiter
561    /// can call this if the deadline worker has not yet processed its timeout.
562    /// Requests already sent remain in `outstanding` until answered or closed.
563    pub(super) fn expire(&self) {
564        let mut state = self.state.lock().expect("session state not poisoned");
565        state.expire(self.now());
566    }
567
568    /// Returns the earliest pending operation deadline for scenario assertions.
569    #[cfg(any(test, feature = "fuzz"))]
570    pub(super) fn next_deadline(&self) -> Option<Instant> {
571        let state = self.state.lock().expect("session state not poisoned");
572        match &*state {
573            State::Open { operations, .. } => operations
574                .values()
575                .map(|operation| operation.deadline)
576                .min(),
577            State::Closed(_) => None,
578        }
579    }
580
581    /// Takes the next unexpired `OutgoingMessage` for tests that drive writing themselves.
582    #[cfg(any(test, feature = "fuzz"))]
583    pub(super) fn take_outgoing(&self) -> Option<OutgoingMessage> {
584        let mut state = self.state.lock().expect("session state not poisoned");
585        state.expire(self.now());
586        match &mut *state {
587            State::Open {
588                outgoing,
589                reserved_ids,
590                ..
591            } => {
592                let message = outgoing.pop_front()?;
593                if let OutgoingBody::Reply { id, .. } = &message.body {
594                    reserved_ids.remove(id);
595                }
596                Some(message)
597            }
598            State::Closed(_) => None,
599        }
600    }
601
602    /// Records a write result under the state lock. A successful request write
603    /// leaves its operation waiting for an answer; a reply write completes it.
604    pub(super) fn record_write(&self, key: &OperationKey, result: Result<(), Error>) {
605        let mut state = self.state.lock().expect("session state not poisoned");
606        let State::Open { operations, .. } = &mut *state else {
607            return;
608        };
609        let Some(operation) = operations.get(key) else {
610            return;
611        };
612        let now = self.now();
613        if now >= operation.deadline || result.is_err() {
614            let operation = operations.remove(key).expect("operation held under lock");
615            operation.fail(result.err().unwrap_or(Error::Timeout), now);
616        } else if !operation.sender.response {
617            let operation = operations.remove(key).expect("operation held under lock");
618            let _ = operation.sender.send(Ok(PromiseResult::Written));
619        }
620    }
621
622    /// Supplies a response to a fixture operation without depending on wire IDs.
623    /// The fixture still encodes and retains bytes, matching real promise behavior.
624    #[cfg(any(test, feature = "fuzz"))]
625    pub(super) fn record_response(
626        self: &Arc<Self>,
627        key: &OperationKey,
628        result: Result<Message, Error>,
629    ) {
630        let mut state = self.state.lock().expect("session state not poisoned");
631        let State::Open {
632            operations,
633            max_inbound_bytes,
634            ..
635        } = &mut *state
636        else {
637            return;
638        };
639        let Some(operation) = operations.remove(key) else {
640            return;
641        };
642        let result = match result {
643            Ok(message) => Ok(message),
644            Err(Error::Remote(error)) => Err(error),
645            Err(error) => {
646                operation.fail(error, self.now());
647                return;
648            }
649        };
650        let peer = match self.side {
651            Side::Client => Side::Server,
652            Side::Server => Side::Client,
653        };
654        let bytes = peer
655            .encode(0, result)
656            .expect("fixture response belongs to the peer");
657        let bytes = Bytes::from(bytes.into_boxed_slice());
658        let header = self
659            .side
660            .decode_header(bytes.clone())
661            .expect("fixture response has a valid envelope");
662        let result = operation.complete_response(self.now(), || {
663            self.retain_incoming(bytes, header, *max_inbound_bytes)
664        });
665        drop(state);
666        if let Err(error) = result {
667            self.close(error);
668        }
669    }
670
671    /// Returns `Instant::now()` or the test clock. Deadline checks use this while
672    /// holding `state`; `reply_unanswered()` also calls it before waiting for that lock.
673    fn now(&self) -> Instant {
674        #[cfg(any(test, feature = "fuzz"))]
675        if let Some(now) = *self.time.lock().expect("scenario clock not poisoned") {
676            return now;
677        }
678        Instant::now()
679    }
680
681    /// Checks the outer envelope and routes its original bytes. `recv()` and
682    /// `wait()` decode nested payloads. Unknown and late responses are discarded
683    /// without decoding their bodies. The reader closes the session on error.
684    pub(super) fn handle_message(self: &Arc<Self>, bytes: Vec<u8>) -> Result<(), Error> {
685        let bytes = Bytes::from(bytes.into_boxed_slice());
686        let header = self.side.decode_header(bytes.clone())?;
687        {
688            let mut state = self.state.lock().expect("session state not poisoned");
689            match MessageKind::from_id(header.id, self.side.into()) {
690                MessageKind::Request => {
691                    if header.failed {
692                        return Err(self.side.malformed(
693                            Some(header),
694                            bytes.len(),
695                            "envelope",
696                            "request contains an error",
697                        ));
698                    }
699                    self.queue_request(&mut state, header, bytes)?;
700                }
701                MessageKind::Response => {
702                    let State::Open {
703                        operations,
704                        outstanding,
705                        max_inbound_bytes,
706                        ..
707                    } = &mut *state
708                    else {
709                        let State::Closed(error) = &*state else {
710                            unreachable!()
711                        };
712                        return Err(error.clone());
713                    };
714                    // A sent request keeps its ID here until answered, so an ID
715                    // without an operation belongs to a request that timed out
716                    if let Some(key) = outstanding.remove(&header.id) {
717                        if let Some(operation) = operations.remove(&key) {
718                            tracing::trace!(
719                                "received response {} ({})",
720                                header.id,
721                                header.payload.unwrap_or("none")
722                            );
723                            operation.complete_response(self.now(), || {
724                                self.retain_incoming(bytes, header, *max_inbound_bytes)
725                            })?;
726                        } else {
727                            tracing::debug!("discarding late response {}", header.id);
728                        }
729                    } else {
730                        tracing::warn!("discarding unmatched response {}", header.id);
731                    }
732                }
733            }
734        }
735        self.changed.notify_all();
736        Ok(())
737    }
738
739    /// Reserves a request slot and bytes under the session lock. If either limit
740    /// is exceeded, the queue stays untouched. Never waits for the application.
741    fn queue_request(
742        self: &Arc<Self>,
743        state: &mut State,
744        header: Header,
745        bytes: Bytes,
746    ) -> Result<(), Error> {
747        let State::Open {
748            incoming,
749            reserved_ids,
750            max_inbound_requests,
751            max_inbound_bytes,
752            ..
753        } = state
754        else {
755            let State::Closed(error) = state else {
756                unreachable!()
757            };
758            return Err(error.clone());
759        };
760        let id = header.id;
761        if reserved_ids.contains(&id) {
762            return Err(self.side.malformed(
763                Some(header),
764                bytes.len(),
765                "envelope",
766                "duplicate request ID",
767            ));
768        }
769        if reserved_ids.len() >= *max_inbound_requests {
770            tracing::warn!(
771                "inbound request limit exceeded (id: {}, used: {}, limit: {})",
772                id,
773                reserved_ids.len(),
774                max_inbound_requests
775            );
776            return Err(Error::InboundRequestLimitExceeded(*max_inbound_requests));
777        }
778        let payload = header.payload.unwrap_or("none");
779        let message = self.retain_incoming(bytes, header, *max_inbound_bytes)?;
780        reserved_ids.insert(id);
781        incoming.push_back((id, message));
782        tracing::trace!("received request {} ({})", id, payload);
783        Ok(())
784    }
785
786    /// Waits for and takes the next queued message, or returns `None` on closure.
787    /// Under the state lock, assigns each request an ID and records its operation
788    /// key in `outstanding`, so `handle_message()` can match the peer's response
789    /// even if it arrives before the write finishes.
790    pub(super) fn next_outgoing(&self) -> Option<(u64, OutgoingMessage)> {
791        let mut state = self.state.lock().expect("session state not poisoned");
792        loop {
793            // Remove expired messages before choosing the next one to send.
794            state.expire(self.now());
795            let State::Open {
796                outgoing,
797                next_id,
798                outstanding,
799                operations,
800                reserved_ids,
801                ..
802            } = &mut *state
803            else {
804                return None;
805            };
806            if let Some(outgoing) = outgoing.pop_front() {
807                let id = match &outgoing.body {
808                    OutgoingBody::Request(_) => {
809                        let id = next_id.expect("wire request IDs exhausted");
810                        *next_id = id.checked_add(2);
811                        // Store the ID before releasing the lock: a response can
812                        // arrive before the outgoing send finishes locally.
813                        outstanding.insert(id, outgoing.operation.key.clone());
814                        if let Some(operation) = operations.get_mut(&outgoing.operation.key) {
815                            operation.log_id = Some(LogId::from(id));
816                        }
817                        id
818                    }
819                    OutgoingBody::Reply { id, .. } => {
820                        // The peer may receive this reply and reuse the ID before
821                        // our flush returns. Finishing this write must not remove
822                        // a newer request that reuses the same ID.
823                        reserved_ids.remove(id);
824                        *id
825                    }
826                };
827                return Some((id, outgoing));
828            }
829            state = self
830                .changed
831                .wait(state)
832                .expect("session state not poisoned");
833        }
834    }
835
836    /// Sends queued messages through `sender`. Transport errors close the session;
837    /// messages that cannot be encoded fail only their own promise.
838    fn run_writer(&self, sender: transport::Sender<impl Write>) {
839        while let Some((id, outgoing)) = self.next_outgoing() {
840            // next_outgoing() released the state lock. The reader and deadline
841            // worker can continue while encoding or sending this message blocks.
842            let request = matches!(outgoing.body, OutgoingBody::Request(_));
843            let kind = if request { "request" } else { "reply" };
844            let body = match outgoing.body {
845                OutgoingBody::Request(body) => Ok(body),
846                OutgoingBody::Reply { result, .. } => result,
847            };
848            let payload = match &body {
849                Ok(message) => message.field_name(),
850                Err(_) => "err",
851            };
852            let result = self.side.encode(id, body).and_then(|bytes| {
853                // Announced before the transport confirms the write, so a
854                // message reads top down in the log
855                tracing::trace!("sending {} {} ({})", kind, id, payload);
856                sender.send(&bytes).map_err(Error::from)
857            });
858            match &result {
859                Ok(()) => {}
860                Err(Error::Transport(error)) => {
861                    // Wire failure ends the session even if this operation's promise
862                    // has already timed out while the transport write was blocked.
863                    self.close(Error::Transport(error.clone()));
864                    break;
865                }
866                Err(error) => tracing::debug!("not sending {} {}: {}", kind, id, error),
867            }
868            {
869                // Remaining failures are local encoding refusals. No request was
870                // sent, so there is no future answer to retain an ID for.
871                let mut state = self.state.lock().expect("session state not poisoned");
872                if let State::Open { outstanding, .. } = &mut *state
873                    && request
874                    && result.is_err()
875                {
876                    outstanding.remove(&id);
877                }
878            }
879            // Report this operation's write result, if it is still pending.
880            // A response or timeout may have completed it during the write.
881            outgoing.operation.record_write(result);
882        }
883        #[cfg(any(test, feature = "fuzz"))]
884        if let Some((entered, released)) = self.disconnect_hook.lock().unwrap().take() {
885            let _ = entered.send(());
886            let _ = released.recv();
887        }
888        // Disconnect the session this sender belongs to. The transport ignores
889        // this call if a new handshake has already replaced that session.
890        if let Err(error) = sender.disconnect() {
891            tracing::debug!(
892                "failed to signal dropped session {}: {}",
893                sender.log_id(),
894                error
895            );
896        }
897    }
898
899    /// Expires pending operations even when no caller is waiting on a promise.
900    /// Waits on `changed` until the next deadline or until new work arrives.
901    fn run_deadlines(&self) {
902        let mut state = self.state.lock().expect("session state not poisoned");
903        loop {
904            state.expire(self.now());
905            let State::Open { operations, .. } = &*state else {
906                return;
907            };
908            // Submitting an earlier deadline wakes this wait. Every wakeup
909            // recomputes the minimum under the same lock used by submission.
910            state = match operations
911                .values()
912                .map(|operation| operation.deadline)
913                .min()
914            {
915                Some(deadline) => {
916                    self.changed
917                        .wait_timeout(state, deadline.saturating_duration_since(self.now()))
918                        .expect("session state not poisoned")
919                        .0
920                }
921                None => self
922                    .changed
923                    .wait(state)
924                    .expect("session state not poisoned"),
925            };
926        }
927    }
928}
929
930impl Session {
931    /// Creates the session and starts its writer and deadline threads. Only
932    /// client sessions receive a stream closer; `Server` closes server streams.
933    pub(super) fn start<W: Write + Send + 'static>(
934        side: Side,
935        sender: transport::Sender<W>,
936        stream_closer: Option<transport::Closer>,
937        #[cfg(any(test, feature = "fuzz"))] workers: Arc<worker::Tracker>,
938    ) -> Self {
939        let session = Self {
940            inner: Arc::new(SessionInner::new(
941                side,
942                sender.log_id(),
943                stream_closer,
944                #[cfg(any(test, feature = "fuzz"))]
945                workers.clone(),
946            )),
947        };
948        let inner = session.inner.clone();
949        worker::spawn(
950            "wire-writer",
951            #[cfg(any(test, feature = "fuzz"))]
952            &workers,
953            move || inner.run_writer(sender),
954        );
955        let inner = session.inner.clone();
956        worker::spawn(
957            "wire-deadlines",
958            #[cfg(any(test, feature = "fuzz"))]
959            &workers,
960            move || inner.run_deadlines(),
961        );
962        session
963    }
964}
965
966impl State {
967    /// Counts the pending operations and the queued messages of an open session.
968    fn workload(&self) -> (usize, usize) {
969        match self {
970            Self::Open {
971                operations,
972                outgoing,
973                incoming,
974                ..
975            } => (operations.len(), outgoing.len() + incoming.len()),
976            Self::Closed(_) => (0, 0),
977        }
978    }
979
980    /// Stops accepting messages and fails pending operations under the session lock.
981    /// Returns the old queues to be dropped after releasing the lock.
982    fn close(&mut self, error: Error, now: Instant) -> Option<Self> {
983        if let Self::Closed(_) = self {
984            return None;
985        }
986        let mut removed = std::mem::replace(self, Self::Closed(error.clone()));
987        if let Self::Open { operations, .. } = &mut removed {
988            for (_, operation) in operations.drain() {
989                operation.fail(error.clone(), now);
990            }
991        }
992        Some(removed)
993    }
994
995    /// Removes expired entries from `operations`, sends `Timeout` to their
996    /// promises, and discards any messages they still have in `outgoing`.
997    fn expire(&mut self, now: Instant) {
998        if let Self::Open {
999            operations,
1000            outgoing,
1001            reserved_ids,
1002            ..
1003        } = self
1004        {
1005            let expired: Vec<_> = operations
1006                .iter()
1007                .filter(|(_, operation)| now >= operation.deadline)
1008                .map(|(key, _)| key.clone())
1009                .collect();
1010            for key in expired {
1011                operations
1012                    .remove(&key)
1013                    .expect("expired operation held under lock")
1014                    .fail(Error::Timeout, now);
1015            }
1016            // Only messages still in this queue can be discarded. Writes already
1017            // started keep running with their independent transport timeout.
1018            outgoing.retain(|outgoing| {
1019                let retained = operations.contains_key(&outgoing.operation.key);
1020                if !retained && let OutgoingBody::Reply { id, .. } = outgoing.body {
1021                    reserved_ids.remove(&id);
1022                }
1023                retained
1024            });
1025        }
1026    }
1027}
1028
1029// These fixtures let tests drive time, incoming requests, and write results.
1030#[cfg(any(test, feature = "fuzz"))]
1031impl Session {
1032    /// Creates a session without a stream or workers for lifecycle scenarios.
1033    pub(super) fn fixture() -> Self {
1034        Self::fixture_for(Side::Server)
1035    }
1036
1037    /// Creates either envelope direction without a stream or workers.
1038    pub(super) fn fixture_for(side: Side) -> Self {
1039        Self {
1040            inner: Arc::new(SessionInner::new(
1041                side,
1042                LogId::default(),
1043                None,
1044                Arc::new(worker::Tracker::default()),
1045            )),
1046        }
1047    }
1048}
1049
1050#[cfg(any(test, feature = "fuzz"))]
1051impl SessionInner {
1052    /// Pauses the writer before `sender.disconnect()`, so a test can connect a
1053    /// replacement session before letting the old writer finish.
1054    pub(super) fn pause_disconnect(
1055        &self,
1056    ) -> (std::sync::mpsc::Receiver<()>, std::sync::mpsc::Sender<()>) {
1057        let (entered, observed) = std::sync::mpsc::channel();
1058        let (release, released) = std::sync::mpsc::channel();
1059        *self.disconnect_hook.lock().unwrap() = Some((entered, released));
1060        (observed, release)
1061    }
1062
1063    /// Returns a receiver notified when the last `Arc<SessionInner>` is dropped.
1064    pub(super) fn watch_drop(&self) -> std::sync::mpsc::Receiver<()> {
1065        let (sender, receiver) = std::sync::mpsc::channel();
1066        *self.drop_hook.lock().unwrap() = Some(sender);
1067        receiver
1068    }
1069
1070    /// Moves a fresh scenario session to its last allocatable request ID.
1071    pub(super) fn use_last_request_id(&self) {
1072        let mut state = self.state.lock().unwrap();
1073        let State::Open {
1074            next_id,
1075            outstanding,
1076            ..
1077        } = &mut *state
1078        else {
1079            panic!("open session required")
1080        };
1081        assert!(outstanding.is_empty());
1082        *next_id = Some(if self.side == Side::Client {
1083            u64::MAX
1084        } else {
1085            u64::MAX - 1
1086        });
1087    }
1088
1089    /// Waits for the reader to remove a previously sent request's ID. Tests use
1090    /// this to leave a completed promise unread before changing limits or sessions.
1091    pub(super) fn wait_response(&self, id: u64) {
1092        let state = self.state.lock().unwrap();
1093        let (state, _) = self.changed.wait_timeout_while(state, Duration::from_secs(3), |state| {
1094            matches!(state, State::Open { outstanding, .. } if outstanding.contains_key(&id))
1095        }).unwrap();
1096        let State::Open { outstanding, .. } = &*state else {
1097            panic!("session closed before response fence");
1098        };
1099        assert!(
1100            !outstanding.contains_key(&id),
1101            "reader did not process response"
1102        );
1103    }
1104
1105    /// Returns the sorted request IDs still waiting for peer responses.
1106    pub(super) fn outstanding_ids(&self) -> Vec<u64> {
1107        let state = self.state.lock().unwrap();
1108        let State::Open { outstanding, .. } = &*state else {
1109            panic!("open session required")
1110        };
1111        let mut ids: Vec<_> = outstanding.keys().copied().collect();
1112        ids.sort_unstable();
1113        ids
1114    }
1115
1116    /// Supplies a request directly to the fixture's admission path, independently
1117    /// of parity (wire routing is covered by the connection scenarios).
1118    pub(super) fn inject_request(self: &Arc<Self>, id: u64, message: Message) -> Result<(), Error> {
1119        let peer = match self.side {
1120            Side::Client => Side::Server,
1121            Side::Server => Side::Client,
1122        };
1123        let bytes = peer
1124            .encode(id, Ok(message))
1125            .expect("fixture request belongs to peer");
1126        let bytes = Bytes::from(bytes.into_boxed_slice());
1127        let header = self.side.decode_header(bytes.clone())?;
1128        let result = {
1129            let mut state = self.state.lock().expect("session state not poisoned");
1130            self.queue_request(&mut state, header, bytes)
1131        };
1132        self.changed.notify_all();
1133        if let Err(error) = &result {
1134            self.close(error.clone());
1135        }
1136        result
1137    }
1138
1139    /// Returns the accepted request count and retained byte count for test checks.
1140    pub(super) fn inbound_usage(&self) -> (usize, usize) {
1141        let state = self.state.lock().expect("session state not poisoned");
1142        let requests = match &*state {
1143            State::Open { reserved_ids, .. } => reserved_ids.len(),
1144            State::Closed(_) => 0,
1145        };
1146        (requests, self.retained_bytes.load(Ordering::Relaxed))
1147    }
1148
1149    /// Advances the test clock without calling `expire()`, so tests can deliver
1150    /// results after a deadline but before the timeout has been processed.
1151    pub(super) fn set_time(&self, now: Instant) {
1152        let _state = self.state.lock().expect("session state not poisoned");
1153        let mut time = self.time.lock().expect("scenario clock not poisoned");
1154        assert!(
1155            time.is_none_or(|previous| now >= previous),
1156            "clock cannot go backwards"
1157        );
1158        *time = Some(now);
1159    }
1160
1161    /// Restores wall-clock time for scenarios that exercise the waiter's real timer.
1162    pub(super) fn use_realtime(&self) {
1163        let _state = self.state.lock().expect("session state not poisoned");
1164        *self.time.lock().expect("scenario clock not poisoned") = None;
1165    }
1166
1167    /// Arms a one-shot notification for the next receive waiting on an empty queue.
1168    /// The notification is sent while holding the lock, immediately before the
1169    /// condition-variable wait releases it, so a later close cannot run too early.
1170    ///
1171    /// # Panics
1172    /// The fixture must still be open and have no queued request.
1173    pub(super) fn watch_recv_wait(&self) -> std::sync::mpsc::Receiver<()> {
1174        let (sender, receiver) = std::sync::mpsc::channel();
1175        let mut state = self.state.lock().expect("session state not poisoned");
1176        let State::Open {
1177            incoming,
1178            wait_hook,
1179            ..
1180        } = &mut *state
1181        else {
1182            panic!("only watch an open session receive");
1183        };
1184        assert!(incoming.is_empty());
1185        *wait_hook = Some(sender);
1186        receiver
1187    }
1188}
1189
1190#[cfg(any(test, feature = "fuzz"))]
1191impl Drop for SessionInner {
1192    /// Notifies the test when the last `Arc<SessionInner>` is dropped.
1193    fn drop(&mut self) {
1194        if let Some(sender) = self.drop_hook.get_mut().unwrap().take() {
1195            let _ = sender.send(());
1196        }
1197    }
1198}
1199
1200/// Checks session ownership bounds and compiles the client construction API.
1201#[cfg(test)]
1202#[cfg_attr(coverage_nightly, coverage(off))]
1203mod tests {
1204    use crate::protocol::{self, Error, Session};
1205    use crate::transport::{Read, Stream, Verifier, Write};
1206
1207    /// Compiles client construction with verifier-specific information in the result.
1208    #[allow(dead_code)]
1209    fn connect<R, W, V>(stream: Stream<R, W>, verifier: &V) -> Result<(Session, V::Info), Error>
1210    where
1211        R: Read + Send + 'static,
1212        W: Write + Send + 'static,
1213        V: Verifier,
1214    {
1215        protocol::connect(stream, verifier)
1216    }
1217
1218    /// Checks the send bound required to transfer ownership to an application thread.
1219    #[test]
1220    fn test_thread_capabilities() {
1221        /// Requires an owned value to be transferable to a background thread.
1222        fn movable<T: Send + 'static>() {}
1223        movable::<Session>();
1224    }
1225}