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