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