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