1use std::collections::HashMap;
2use std::collections::VecDeque;
3use std::future::Future;
4use std::pin::Pin;
5use std::sync::atomic::{AtomicBool, Ordering};
6use std::sync::Arc;
7
8use bytes::Bytes;
9use tokio::sync::{mpsc, oneshot, OwnedSemaphorePermit, Semaphore};
10use unb_core::{
11 ClientDelivery as CoreClientDelivery, ClientOperationId, CoreEffect, CoreInput, EffectId,
12 Envelope, ProtocolCore, RetirementReason, SendResult, SessionId,
13};
14use web_time::Instant;
15
16use crate::cancellation::CancellationToken;
17use crate::client::{ClientDelivery, ClientSession};
18use crate::error::WsError;
19use crate::transport::{Pipe, PipeReader, PipeWriter};
20use crate::wire::{Directive, Wire};
21use crate::{BodyStream, WireBody};
22
23const CORE_QUEUE: usize = 256;
24const SESSION_QUEUE: usize = 64;
25const OUTBOUND_QUEUE: usize = 8_192;
26const OUTBOUND_BYTE_BUDGET: usize = 64 * 1024 * 1024;
27const BODY_WINDOW_BYTES: usize = 256 * 1024;
28const WRITE_BATCH: usize = 8;
29const RELAY_BACKPRESSURE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
30const TERMINAL_SEND_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
31const CLIENT_DELIVERY_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
32const ESTABLISHMENT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
33const PENDING_MODE_WRITES_MAX: usize = 128;
34const OPERATION_WRITE_QUEUE: usize = OUTBOUND_QUEUE;
35const OPERATION_ACTIVE_MAX: usize = 128;
36const OPERATION_BODY_CHUNK_BYTES: usize = 64 * 1024;
37
38#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
39fn clock_now() -> Instant {
40 n0_future::time::Instant::now().into_std()
41}
42
43#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
44async fn sleep_to(deadline: Instant) {
45 n0_future::time::sleep_until(n0_future::time::Instant::from_std(deadline)).await
46}
47
48#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
49fn clock_now() -> Instant {
50 n0_future::time::Instant::now()
51}
52
53#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
54async fn sleep_to(deadline: Instant) {
55 n0_future::time::sleep_until(deadline).await
56}
57
58#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
59pub use tokio::runtime::Handle as RuntimeHandle;
60
61#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
62#[derive(Clone, Debug)]
63pub struct RuntimeHandle;
64
65#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
66impl RuntimeHandle {
67 pub fn current() -> Self {
68 RuntimeHandle
69 }
70
71 pub fn spawn<F>(&self, future: F)
72 where
73 F: Future<Output = ()> + 'static,
74 {
75 n0_future::task::spawn(future);
76 }
77}
78
79pub type EffectFuture = Pin<Box<dyn Future<Output = Option<CoreInput>> + Send>>;
80
81pub trait EffectExecutor: Send + Sync + 'static {
82 fn execute(&self, effect: CoreEffect, handle: ProtocolCoreHandle) -> EffectFuture;
83}
84
85#[derive(Clone, Debug)]
86pub enum SessionOutcome {
87 Established,
88 Retired(RetirementReason),
89}
90
91pub trait SessionHandler: Send + 'static {
92 fn deliver(&mut self, envelope: Envelope) -> impl Future<Output = ()> + Send;
93
94 fn stream_closed(&mut self, _operation: ClientOperationId) -> impl Future<Output = ()> + Send {
95 async {}
96 }
97}
98
99impl SessionHandler for () {
100 async fn deliver(&mut self, _envelope: Envelope) {}
101}
102
103impl SessionHandler for mpsc::Sender<Envelope> {
104 async fn deliver(&mut self, envelope: Envelope) {
105 let _ = self.send(envelope).await;
106 }
107}
108
109impl SessionHandler for tokio::sync::broadcast::Sender<Envelope> {
110 async fn deliver(&mut self, envelope: Envelope) {
111 let _ = self.send(envelope);
112 }
113}
114
115type TokenBodies = Arc<std::sync::Mutex<HashMap<(SessionId, String), WireBody>>>;
116
117type BodyPumps = Arc<std::sync::Mutex<HashMap<(SessionId, String), (String, CancellationToken)>>>;
118
119const SESSION_BODY_TOKENS_MAX: usize = OUTBOUND_QUEUE;
120const SESSION_BODY_BYTES_MAX: usize = OUTBOUND_BYTE_BUDGET;
121
122fn mint_body_token() -> String {
123 use std::hash::{BuildHasher, Hasher};
124 type TokenKeys = (
125 std::collections::hash_map::RandomState,
126 std::collections::hash_map::RandomState,
127 );
128 static KEYS: std::sync::OnceLock<TokenKeys> = std::sync::OnceLock::new();
129 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
130 let (low, high) = KEYS.get_or_init(TokenKeys::default);
131 let serial = NEXT.fetch_add(1, Ordering::Relaxed);
132 let mut a = low.build_hasher();
133 a.write_u64(serial);
134 let mut b = high.build_hasher();
135 b.write_u64(serial);
136 format!("b{:016x}{:016x}", a.finish(), b.finish())
137}
138
139fn application_core_input(
140 handle: &ProtocolCoreHandle,
141 session: &SessionId,
142 mut envelope: Envelope,
143) -> Result<CoreInput, unb_core::CoreError> {
144 if envelope.kind == unb_core::Kind::Discover {
145 return Ok(CoreInput::FrameReceived {
146 session: session.clone(),
147 envelope,
148 });
149 }
150 if !(envelope.kind.is_application_request()
151 || (envelope.corr.is_some()
152 && (envelope.kind.is_application_response()
153 || envelope.kind == unb_core::Kind::Cancel)))
154 {
155 return Ok(CoreInput::FrameReceived {
156 session: session.clone(),
157 envelope,
158 });
159 }
160 if envelope.body_token.is_some() && !envelope.payload.is_empty() {
161 return Err(unb_core::CoreError::Malformed(
162 "application frame cannot carry both inline and referenced bodies".into(),
163 ));
164 }
165 if envelope.body_token.is_none()
166 && envelope.kind != unb_core::Kind::Error
167 && !envelope.payload.is_empty()
168 {
169 let payload = std::mem::take(&mut envelope.payload);
170 envelope.body_token = Some(
171 handle
172 .register_body(session, WireBody::Bytes(payload))?
173 .to_string(),
174 );
175 }
176 Ok(CoreInput::ApplicationFrameReceived {
177 session: session.clone(),
178 frame: unb_core::ApplicationFrame::from_envelope(&envelope)?,
179 })
180}
181
182fn register_outbound_body(
183 handle: &ProtocolCoreHandle,
184 session: &SessionId,
185 payload: Bytes,
186 body: Option<BodyStream>,
187) -> Result<Option<unb_core::BodyId>, unb_core::CoreError> {
188 if body.is_some() && !payload.is_empty() {
189 return Err(unb_core::CoreError::Malformed(
190 "an operation cannot provide both unary bytes and a streaming body".into(),
191 ));
192 }
193 let body = match body {
194 Some(body) => Some(WireBody::Stream(body)),
195 None if !payload.is_empty() => Some(WireBody::Bytes(payload)),
196 None => None,
197 };
198 body.map(|body| handle.register_body(session, body))
199 .transpose()
200}
201
202#[derive(Clone)]
203pub struct ProtocolCoreHandle {
204 inputs: mpsc::Sender<ActorInput>,
205 cancellation: CancellationToken,
206 token_bodies: TokenBodies,
207 body_pumps: BodyPumps,
208}
209
210impl ProtocolCoreHandle {
211 pub fn spawn(
212 core: ProtocolCore,
213 executor: Arc<dyn EffectExecutor>,
214 cancellation: CancellationToken,
215 runtime: &RuntimeHandle,
216 ) -> Self {
217 let (inputs, receiver) = mpsc::channel(CORE_QUEUE);
218 let handle = Self {
219 inputs,
220 cancellation,
221 token_bodies: Arc::new(std::sync::Mutex::new(HashMap::new())),
222 body_pumps: Arc::new(std::sync::Mutex::new(HashMap::new())),
223 };
224 runtime.spawn(CoreRuntime::new(core, receiver, handle.clone(), executor).run());
225 handle
226 }
227
228 pub fn claim_body(&self, session: &SessionId, token: &str) -> Option<WireBody> {
229 self.token_bodies
230 .lock()
231 .expect("body token registry")
232 .remove(&(session.clone(), token.to_string()))
233 }
234
235 pub fn claim_body_stream(&self, session: &SessionId, token: &str) -> Option<BodyStream> {
236 self.claim_body(session, token).map(wire_body_stream)
237 }
238
239 pub fn register_body(
240 &self,
241 session: &SessionId,
242 body: WireBody,
243 ) -> Result<unb_core::BodyId, unb_core::CoreError> {
244 let mut registry = self.token_bodies.lock().expect("body token registry");
245 let owned = registry
246 .keys()
247 .filter(|(owner, _)| owner == session)
248 .count();
249 if owned >= SESSION_BODY_TOKENS_MAX {
250 return Err(unb_core::CoreError::EffectQueueFull);
251 }
252 let incoming = match &body {
253 WireBody::Bytes(payload) => payload.len(),
254 WireBody::Stream(_) => 0,
255 };
256 let retained = registry
257 .iter()
258 .filter_map(|((owner, _), body)| {
259 (owner == session).then_some(match body {
260 WireBody::Bytes(payload) => payload.len(),
261 WireBody::Stream(_) => 0,
262 })
263 })
264 .sum::<usize>();
265 if retained.saturating_add(incoming) > SESSION_BODY_BYTES_MAX {
266 return Err(unb_core::CoreError::BodyRegistryFull(
267 SESSION_BODY_BYTES_MAX,
268 ));
269 }
270 let token = mint_body_token();
271 registry.insert((session.clone(), token.clone()), body);
272 Ok(unb_core::BodyId::from(token))
273 }
274
275 fn register_body_pump(
276 &self,
277 session: &SessionId,
278 corr: &str,
279 token: &str,
280 cancellation: CancellationToken,
281 ) {
282 self.body_pumps.lock().expect("body pump registry").insert(
283 (session.clone(), corr.to_string()),
284 (token.to_string(), cancellation),
285 );
286 }
287
288 fn cancel_body_pump(&self, session: &SessionId, corr: &str) {
289 let cancelled = self
290 .body_pumps
291 .lock()
292 .expect("body pump registry")
293 .remove(&(session.clone(), corr.to_string()));
294 if let Some((_, cancellation)) = cancelled {
295 cancellation.cancel();
296 }
297 }
298
299 fn release_body_pump(&self, session: &SessionId, corr: &str, token: &str) {
300 let mut pumps = self.body_pumps.lock().expect("body pump registry");
301 let key = (session.clone(), corr.to_string());
302 if pumps.get(&key).is_some_and(|(owner, _)| owner == token) {
303 pumps.remove(&key);
304 }
305 }
306
307 pub async fn submit(&self, input: CoreInput) -> Result<(), WsError> {
308 self.inputs
309 .send(ActorInput::Core(input))
310 .await
311 .map_err(|_| WsError::Gone)
312 }
313
314 pub async fn submit_batch(&self, inputs: Vec<CoreInput>) -> Result<(), WsError> {
315 self.inputs
316 .send(ActorInput::CoreBatch(inputs))
317 .await
318 .map_err(|_| WsError::Gone)
319 }
320
321 pub async fn install<F>(&self, input: CoreInput, publish: F) -> Result<(), WsError>
322 where
323 F: FnOnce() + Send + 'static,
324 {
325 let (reply, response) = oneshot::channel();
326 self.inputs
327 .send(ActorInput::Install {
328 input,
329 publish: Box::new(publish),
330 reply,
331 })
332 .await
333 .map_err(|_| WsError::Gone)?;
334 response.await.map_err(|_| WsError::Gone)?
335 }
336
337 pub async fn attach<H: SessionHandler>(
338 &self,
339 session: SessionId,
340 transport: Pipe,
341 expected_peer: Option<String>,
342 handler: H,
343 ) -> Result<Arc<Wire>, WsError> {
344 self.attach_with_ceiling(
345 session,
346 transport,
347 expected_peer,
348 handler,
349 unb_transport::DEFAULT_MAX_FRAME_SIZE,
350 )
351 .await
352 }
353
354 pub async fn attach_with_ceiling<H: SessionHandler>(
355 &self,
356 session: SessionId,
357 transport: Pipe,
358 expected_peer: Option<String>,
359 handler: H,
360 ws_collect_ceiling: usize,
361 ) -> Result<Arc<Wire>, WsError> {
362 let cancellation = self.cancellation.child_token();
363 let (wire, directives, outcome, routes) = Wire::channel(cancellation.clone());
364 let (reply, response) = oneshot::channel();
365 self.inputs
366 .send(ActorInput::Attach {
367 session,
368 transport,
369 expected_peer,
370 directives,
371 handler: handler_channel(handler),
372 client: wire.client_session(),
373 cancellation,
374 establish_peer: true,
375 outcome,
376 routes,
377 ws_collect_ceiling,
378 reply,
379 })
380 .await
381 .map_err(|_| WsError::Gone)?;
382 response.await.map_err(|_| WsError::Gone)??;
383 Ok(wire)
384 }
385
386 pub fn shutdown(&self) {
387 self.cancellation.cancel();
388 }
389
390 pub(crate) fn open(transport: Pipe, runtime: &RuntimeHandle) -> (Arc<Wire>, Self) {
391 struct StandaloneExecutor;
392
393 impl EffectExecutor for StandaloneExecutor {
394 fn execute(&self, _effect: CoreEffect, _handle: ProtocolCoreHandle) -> EffectFuture {
395 Box::pin(async { None })
396 }
397 }
398
399 let cancellation = CancellationToken::new();
400 let handle = Self::spawn(
401 ProtocolCore::new("client"),
402 Arc::new(StandaloneExecutor),
403 cancellation.clone(),
404 runtime,
405 );
406 let session_cancellation = cancellation.child_token();
407 let (wire, directives, outcome, routes) =
408 Wire::standalone_channel(session_cancellation.clone());
409 let (reply, _response) = oneshot::channel();
410 handle
411 .inputs
412 .try_send(ActorInput::Attach {
413 session: SessionId::from("client-session"),
414 transport,
415 expected_peer: None,
416 directives,
417 handler: handler_channel(wire.observation_sender()),
418 client: wire.client_session(),
419 cancellation: session_cancellation,
420 establish_peer: false,
421 outcome,
422 routes,
423 ws_collect_ceiling: unb_transport::DEFAULT_MAX_FRAME_SIZE,
424 reply,
425 })
426 .expect("new protocol core input queue");
427 (wire, handle)
428 }
429}
430
431fn wire_body_stream(body: WireBody) -> BodyStream {
432 match body {
433 WireBody::Bytes(bytes) => Box::pin(futures_util::stream::once(async move { Ok(bytes) })),
434 WireBody::Stream(stream) => stream,
435 }
436}
437
438enum ActorInput {
439 Core(CoreInput),
440 CoreBatch(Vec<CoreInput>),
441 Install {
442 input: CoreInput,
443 publish: Box<dyn FnOnce() + Send>,
444 reply: oneshot::Sender<Result<(), WsError>>,
445 },
446 Directive(SessionId, Directive),
447 Attach {
448 session: SessionId,
449 transport: Pipe,
450 expected_peer: Option<String>,
451 directives: mpsc::Receiver<Directive>,
452 handler: mpsc::Sender<HandlerInput>,
453 client: ClientSession,
454 cancellation: CancellationToken,
455 establish_peer: bool,
456 outcome: tokio::sync::watch::Sender<Option<SessionOutcome>>,
457 routes: tokio::sync::watch::Sender<bool>,
458 ws_collect_ceiling: usize,
459 reply: oneshot::Sender<Result<(), WsError>>,
460 },
461 Write {
462 session: SessionId,
463 envelope: Envelope,
464 completion: Option<EffectId>,
465 relay: bool,
466 },
467 TransportModeSelected {
468 session: SessionId,
469 mode: unb_transport::TransportMode,
470 },
471}
472
473struct SessionIo {
474 outbound: mpsc::Sender<WriteRequest>,
475 byte_budget: Arc<Semaphore>,
476 ws_collect_ceiling: usize,
477 bodies: crate::transport::SessionStreams,
478 handler: mpsc::Sender<HandlerInput>,
479 client: ClientSession,
480 cancellation: CancellationToken,
481 reservation_pending: Arc<AtomicBool>,
482 establish_peer: bool,
483 deadline: tokio::sync::watch::Sender<Option<Instant>>,
484 routes: tokio::sync::watch::Sender<bool>,
485 transport_mode: Option<unb_transport::TransportMode>,
486 pending_mode_writes: VecDeque<PendingModeWrite>,
487 operation_lanes: OperationLanes,
488 operation_slots: Arc<Semaphore>,
489}
490
491struct PendingModeWrite {
492 envelope: Envelope,
493 completion: Option<EffectId>,
494 relay: bool,
495}
496
497type OperationLanes = Arc<std::sync::Mutex<HashMap<String, OperationLane>>>;
498
499#[derive(Clone)]
500struct OperationLane {
501 outbound: Option<mpsc::Sender<OperationWriteRequest>>,
502 control: Option<unb_transport::operation::OperationControl>,
503}
504
505struct OperationWriteRequest {
506 envelope: Envelope,
507 completion: Option<EffectId>,
508 relay: bool,
509 body: Option<BodyStream>,
510 _budget: OwnedSemaphorePermit,
511}
512
513struct WriteRequest {
514 envelope: Envelope,
515 completion: Option<EffectId>,
516 relay: bool,
517 _budget: OwnedSemaphorePermit,
518}
519
520impl WriteRequest {
521 fn weight(envelope: &Envelope) -> u32 {
522 let head = envelope.target.len()
523 + envelope.subject.len()
524 + envelope.id.len()
525 + envelope.corr.as_ref().map_or(0, String::len)
526 + envelope.path.iter().map(String::len).sum::<usize>()
527 + envelope
528 .headers
529 .iter()
530 .map(|(name, value)| name.len() + value.as_str().map_or(16, str::len))
531 .sum::<usize>();
532 (envelope.payload.len() + head).clamp(1, OUTBOUND_BYTE_BUDGET) as u32
533 }
534}
535
536#[derive(Debug)]
537pub(crate) struct MeteredChunk {
538 pub(crate) bytes: Bytes,
539 _window: OwnedSemaphorePermit,
540 _budget: OwnedSemaphorePermit,
541}
542
543pub(crate) struct MeteredBody {
544 stream: BodyStream,
545 budget: Arc<Semaphore>,
546 window: Arc<Semaphore>,
547 pending: Option<Bytes>,
548 cancellation: CancellationToken,
549}
550
551impl MeteredBody {
552 pub(crate) fn new(
553 stream: BodyStream,
554 budget: Arc<Semaphore>,
555 cancellation: CancellationToken,
556 ) -> MeteredBody {
557 MeteredBody {
558 stream,
559 budget,
560 window: Arc::new(Semaphore::new(BODY_WINDOW_BYTES)),
561 pending: None,
562 cancellation,
563 }
564 }
565
566 pub(crate) async fn next_chunk(&mut self) -> Result<Option<MeteredChunk>, WsError> {
567 let chunk = match self.pending.take() {
568 Some(chunk) => chunk,
569 None => {
570 let next = tokio::select! {
571 biased;
572 () = self.cancellation.cancelled() => return Ok(None),
573 next = std::future::poll_fn(|cx| self.stream.as_mut().poll_next(cx)) => next,
574 };
575 match next {
576 Some(Ok(chunk)) => chunk,
577 Some(Err(error)) => return Err(error.into()),
578 None => return Ok(None),
579 }
580 }
581 };
582 let len = chunk.len();
583 if len > unb_transport::DEFAULT_MAX_FRAME_SIZE {
584 return Err(
585 unb_core::CoreError::BodyTooLarge(unb_transport::DEFAULT_MAX_FRAME_SIZE).into(),
586 );
587 }
588 self.pending = Some(chunk);
589 let window_weight = len.clamp(1, BODY_WINDOW_BYTES) as u32;
590 let budget_weight = len.max(1) as u32;
591 let acquire = async {
592 let window = self
593 .window
594 .clone()
595 .acquire_many_owned(window_weight)
596 .await
597 .map_err(|_| unb_core::CoreError::BodyStalled)?;
598 let budget = self
599 .budget
600 .clone()
601 .acquire_many_owned(budget_weight)
602 .await
603 .map_err(|_| unb_core::CoreError::BodyStalled)?;
604 Ok::<_, unb_core::CoreError>((window, budget))
605 };
606 let acquired = tokio::select! {
607 biased;
608 () = self.cancellation.cancelled() => return Ok(None),
609 acquired = n0_future::time::timeout(RELAY_BACKPRESSURE_TIMEOUT, acquire) => acquired,
610 };
611 match acquired {
612 Ok(Ok((window, budget))) => Ok(Some(MeteredChunk {
613 bytes: self.pending.take().expect("pending chunk present"),
614 _window: window,
615 _budget: budget,
616 })),
617 Ok(Err(error)) => Err(error.into()),
618 Err(_) => Err(unb_core::CoreError::BodyStalled.into()),
619 }
620 }
621}
622
623struct CoreRuntime {
624 core: ProtocolCore,
625 inputs: mpsc::Receiver<ActorInput>,
626 handle: ProtocolCoreHandle,
627 executor: Arc<dyn EffectExecutor>,
628 sessions: HashMap<SessionId, SessionIo>,
629 outcomes: HashMap<SessionId, tokio::sync::watch::Sender<Option<SessionOutcome>>>,
630 client_starts: HashMap<SessionId, PendingClientStart>,
631 pending_bodies: std::sync::Mutex<HashMap<(SessionId, String), BodyStream>>,
632}
633
634struct PendingClientStart {
635 sender: mpsc::Sender<ClientDelivery>,
636 reply: oneshot::Sender<Result<ClientOperationId, unb_core::CoreError>>,
637}
638
639impl CoreRuntime {
640 fn new(
641 core: ProtocolCore,
642 inputs: mpsc::Receiver<ActorInput>,
643 handle: ProtocolCoreHandle,
644 executor: Arc<dyn EffectExecutor>,
645 ) -> Self {
646 Self {
647 core,
648 inputs,
649 handle,
650 executor,
651 sessions: HashMap::new(),
652 outcomes: HashMap::new(),
653 client_starts: HashMap::new(),
654 pending_bodies: std::sync::Mutex::new(HashMap::new()),
655 }
656 }
657
658 async fn run(mut self) {
659 loop {
660 let input = tokio::select! {
661 biased;
662 () = self.handle.cancellation.cancelled() => return,
663 input = self.inputs.recv() => input,
664 };
665 let Some(input) = input else { return };
666 match input {
667 ActorInput::Core(input) => {
668 let _ = self.core.handle(clock_now(), input);
669 }
670 ActorInput::CoreBatch(inputs) => {
671 let now = clock_now();
672 for input in inputs {
673 let _ = self.core.handle(now, input);
674 }
675 }
676 ActorInput::Install {
677 input,
678 publish,
679 reply,
680 } => {
681 let result = self.core.handle(clock_now(), input).map_err(WsError::from);
682 if result.is_ok() {
683 publish();
684 }
685 let _ = reply.send(result);
686 }
687 ActorInput::Directive(session, directive) => {
688 self.directive(&session, directive).await;
689 }
690 ActorInput::Attach {
691 session,
692 transport,
693 expected_peer,
694 directives,
695 handler,
696 client,
697 cancellation,
698 establish_peer,
699 outcome,
700 routes,
701 ws_collect_ceiling,
702 reply,
703 } => {
704 let result = self
705 .attach(
706 session,
707 transport,
708 expected_peer,
709 directives,
710 handler,
711 client,
712 cancellation,
713 establish_peer,
714 outcome,
715 routes,
716 ws_collect_ceiling,
717 )
718 .await;
719 let _ = reply.send(result);
720 }
721 ActorInput::Write {
722 session,
723 envelope,
724 completion,
725 relay,
726 } => {
727 self.write(&session, envelope, completion, relay).await;
728 }
729 ActorInput::TransportModeSelected { session, mode } => {
730 let pending = if let Some(io) = self.sessions.get_mut(&session) {
731 io.transport_mode = Some(mode);
732 std::mem::take(&mut io.pending_mode_writes)
733 } else {
734 VecDeque::new()
735 };
736 if mode == unb_transport::TransportMode::OperationStreamsV1 {
737 self.start_operation_acceptor(&session);
738 }
739 for pending in pending {
740 self.write(
741 &session,
742 pending.envelope,
743 pending.completion,
744 pending.relay,
745 )
746 .await;
747 }
748 }
749 }
750 self.drain().await;
751 }
752 }
753
754 #[allow(clippy::too_many_arguments)]
755 async fn attach(
756 &mut self,
757 session: SessionId,
758 transport: Pipe,
759 expected_peer: Option<String>,
760 directives: mpsc::Receiver<Directive>,
761 handler: mpsc::Sender<HandlerInput>,
762 client: ClientSession,
763 cancellation: CancellationToken,
764 establish_peer: bool,
765 outcome: tokio::sync::watch::Sender<Option<SessionOutcome>>,
766 routes: tokio::sync::watch::Sender<bool>,
767 ws_collect_ceiling: usize,
768 ) -> Result<(), WsError> {
769 let initiator = transport.initiator();
770 let bodies = transport.streams();
771 let (reader, writer) = transport.split();
772 let (outbound, outbound_rx) = mpsc::channel(OUTBOUND_QUEUE);
773 let (deadline, deadline_rx) = tokio::sync::watch::channel(None);
774 self.core.handle(
775 clock_now(),
776 CoreInput::SessionOpened {
777 session: session.clone(),
778 initiator,
779 establish_peer,
780 expected_peer,
781 },
782 )?;
783 self.sessions.insert(
784 session.clone(),
785 SessionIo {
786 outbound,
787 byte_budget: Arc::new(Semaphore::new(OUTBOUND_BYTE_BUDGET)),
788 ws_collect_ceiling,
789 bodies: bodies.clone(),
790 handler,
791 client,
792 cancellation: cancellation.clone(),
793 reservation_pending: Arc::new(AtomicBool::new(false)),
794 establish_peer,
795 deadline,
796 routes,
797 transport_mode: None,
798 pending_mode_writes: VecDeque::new(),
799 operation_lanes: Arc::new(std::sync::Mutex::new(HashMap::new())),
800 operation_slots: Arc::new(Semaphore::new(OPERATION_ACTIVE_MAX)),
801 },
802 );
803 self.outcomes.insert(session.clone(), outcome);
804 spawn_session_deadline(
805 session.clone(),
806 deadline_rx,
807 self.handle.clone(),
808 cancellation.clone(),
809 );
810 spawn_directives(
811 session.clone(),
812 directives,
813 self.handle.clone(),
814 cancellation.clone(),
815 );
816 spawn_reader(
817 session.clone(),
818 reader,
819 self.handle.clone(),
820 cancellation.clone(),
821 bodies,
822 );
823 spawn_writer(
824 session.clone(),
825 writer,
826 outbound_rx,
827 self.handle.clone(),
828 cancellation,
829 );
830 if establish_peer {
831 spawn_establishment_timeout(session, self.handle.clone());
832 }
833 Ok(())
834 }
835
836 async fn directive(&mut self, session: &SessionId, directive: Directive) {
837 match directive {
838 Directive::StartClientOperation {
839 target_path,
840 kind,
841 payload,
842 hops,
843 headers,
844 body,
845 timeout,
846 sender,
847 reply,
848 } => {
849 let input = if kind == unb_core::Kind::Discover {
850 if body.is_some() {
851 Err(unb_core::CoreError::Malformed(
852 "discovery does not accept a streaming body".into(),
853 ))
854 } else {
855 serde_json::from_slice(&payload)
856 .map(unb_core::OperationInput::Discovery)
857 .map_err(|error| unb_core::CoreError::Malformed(error.to_string()))
858 }
859 } else {
860 register_outbound_body(&self.handle, session, payload, body)
861 .map(unb_core::OperationInput::Body)
862 };
863 let input = match input {
864 Ok(input) => input,
865 Err(error) => {
866 let _ = reply.send(Err(error));
867 return;
868 }
869 };
870 let registered_body = match &input {
871 unb_core::OperationInput::Body(body) => body.clone(),
872 unb_core::OperationInput::Discovery(_) => None,
873 };
874 let result = self.core.handle(
875 clock_now(),
876 CoreInput::StartClientOperation {
877 session: session.clone(),
878 target_path,
879 kind,
880 input,
881 hops,
882 headers,
883 timeout,
884 },
885 );
886 if let Err(error) = result {
887 if let Some(body) = ®istered_body {
888 let _ = self.handle.claim_body(session, body.as_str());
889 }
890 let _ = reply.send(Err(error));
891 } else {
892 self.client_starts
893 .insert(session.clone(), PendingClientStart { sender, reply });
894 }
895 }
896 Directive::OpenStream {
897 target_path,
898 kind,
899 payload,
900 hops,
901 headers,
902 body,
903 opened,
904 reply,
905 } => {
906 let result = match register_outbound_body(&self.handle, session, payload, body) {
907 Ok(body) => {
908 let result = self.core.open_stream_body(
909 session,
910 &target_path,
911 kind,
912 body.clone(),
913 hops,
914 headers,
915 );
916 if result.is_err() {
917 if let Some(body) = &body {
918 let _ = self.handle.claim_body(session, body.as_str());
919 }
920 }
921 result
922 }
923 Err(error) => Err(error),
924 };
925 if let (Ok(corr), Some(opened)) = (&result, opened) {
926 opened(corr.clone()).await;
927 }
928 let _ = reply.send(result);
929 }
930 Directive::Send { corr, payload } => {
931 match register_outbound_body(&self.handle, session, payload, None) {
932 Ok(body) => {
933 if self.core.send_body(session, &corr, body.clone()).is_err() {
934 if let Some(body) = &body {
935 let _ = self.handle.claim_body(session, body.as_str());
936 }
937 }
938 }
939 Err(error) => {
940 let _ = self.core.fail(
941 session,
942 &corr,
943 unb_core::ErrorCode::Busy,
944 &error.to_string(),
945 );
946 }
947 }
948 }
949 Directive::Respond {
950 corr,
951 payload,
952 headers,
953 body,
954 } => match register_outbound_body(&self.handle, session, payload, body) {
955 Ok(body) => {
956 if self
957 .core
958 .respond_body(session, &corr, body.clone(), headers)
959 .is_err()
960 {
961 if let Some(body) = &body {
962 let _ = self.handle.claim_body(session, body.as_str());
963 }
964 }
965 }
966 Err(error) => {
967 let _ = self.core.fail(
968 session,
969 &corr,
970 unb_core::ErrorCode::Busy,
971 &error.to_string(),
972 );
973 }
974 },
975 Directive::Fail {
976 corr,
977 code,
978 message,
979 } => {
980 let _ = self.core.fail(session, &corr, code, &message);
981 }
982 Directive::Cancel { corr } => {
983 let _ = self.core.cancel(session, &corr);
984 }
985 Directive::CancelClientOperation { operation } => {
986 let _ = self.core.handle(
987 clock_now(),
988 CoreInput::CancelClientOperation {
989 session: session.clone(),
990 operation,
991 },
992 );
993 }
994 Directive::Control { kind, payload } => {
995 let _ = self.core.control(session, kind, payload);
996 }
997 }
998 }
999
1000 async fn drain(&mut self) {
1001 while let Some(effect) = self.core.poll_effect() {
1002 match effect {
1003 CoreEffect::SendFrame { session, envelope } => {
1004 self.write(&session, envelope, None, false).await
1005 }
1006 CoreEffect::HandshakeEstablished { session, .. } => {
1007 if let Some(bodies) =
1008 self.sessions.get(&session).and_then(|io| io.bodies.clone())
1009 {
1010 let inputs = self.handle.inputs.clone();
1011 let mode_session = session.clone();
1012 n0_future::task::spawn(async move {
1013 let mode = bodies.negotiate().await;
1014 let _ = inputs
1015 .send(ActorInput::TransportModeSelected {
1016 session: mode_session,
1017 mode,
1018 })
1019 .await;
1020 });
1021 }
1022 if self
1023 .sessions
1024 .get(&session)
1025 .is_some_and(|io| !io.establish_peer)
1026 {
1027 if let Some(outcome) = self.outcomes.get(&session) {
1028 let _ = outcome.send(Some(SessionOutcome::Established));
1029 }
1030 }
1031 }
1032 CoreEffect::DeliverClient {
1033 session,
1034 operation,
1035 delivery,
1036 } => {
1037 let body = match &delivery {
1038 CoreClientDelivery::Item(frame) | CoreClientDelivery::Terminal(frame) => {
1039 frame
1040 .body
1041 .as_ref()
1042 .and_then(|body| self.handle.claim_body(&session, body.as_str()))
1043 }
1044 _ => None,
1045 };
1046 let terminal = matches!(
1047 delivery,
1048 CoreClientDelivery::Terminal(_)
1049 | CoreClientDelivery::Cancelled
1050 | CoreClientDelivery::TimedOut
1051 | CoreClientDelivery::SessionClosed
1052 );
1053 let client = self.sessions.get(&session).map(|io| io.client.clone());
1054 if let Some(client) = client {
1055 let delivered = n0_future::time::timeout(
1056 CLIENT_DELIVERY_TIMEOUT,
1057 client.deliver(&operation, delivery, body),
1058 )
1059 .await
1060 .is_ok();
1061 if !delivered {
1062 client.abandon(&operation);
1063 let _ = self.core.handle(
1064 clock_now(),
1065 CoreInput::CancelClientOperation {
1066 session: session.clone(),
1067 operation: operation.clone(),
1068 },
1069 );
1070 }
1071 }
1072 if terminal {
1073 self.handler(&session, HandlerInput::StreamClosed(operation))
1074 .await;
1075 }
1076 }
1077 CoreEffect::RegisterClient { session, operation } => {
1078 let Some(start) = self.client_starts.remove(&session) else {
1079 continue;
1080 };
1081 let client = self.sessions.get(&session).map(|io| io.client.clone());
1082 let result = if let Some(client) = &client {
1083 client
1084 .register(operation.clone(), start.sender)
1085 .map(|()| operation)
1086 .map_err(|error| unb_core::CoreError::UnknownStream(error.to_string()))
1087 } else {
1088 Err(unb_core::CoreError::UnknownSession(session.to_string()))
1089 };
1090 if let Err(Ok(operation)) = start.reply.send(result) {
1091 if let Some(client) = client {
1092 client.abandon(&operation);
1093 }
1094 let _ = self.core.handle(
1095 clock_now(),
1096 CoreInput::CancelClientOperation { session, operation },
1097 );
1098 }
1099 }
1100 CoreEffect::Deliver {
1101 session,
1102 mut envelope,
1103 } => {
1104 if let Some(token) = envelope.body_token.take() {
1105 match self.handle.claim_body(&session, &token) {
1106 Some(WireBody::Bytes(payload)) => {
1107 envelope.payload = payload;
1108 }
1109 Some(body @ WireBody::Stream(_)) => {
1110 let ceiling = self
1111 .sessions
1112 .get(&session)
1113 .map_or(unb_transport::DEFAULT_MAX_FRAME_SIZE, |io| {
1114 io.ws_collect_ceiling
1115 });
1116 match body.collect_to(ceiling).await {
1117 Ok(payload) => envelope.payload = payload,
1118 Err(error) => {
1119 envelope.kind = unb_core::Kind::Error;
1120 envelope.payload =
1121 Envelope::encode_payload(&serde_json::json!({
1122 "code": unb_core::ErrorCode::PayloadTooLarge,
1123 "message": error.to_string(),
1124 }));
1125 }
1126 }
1127 }
1128 None => {}
1129 }
1130 }
1131 self.handler(&session, HandlerInput::Deliver(envelope))
1132 .await
1133 }
1134 CoreEffect::ReleaseBody { session, body } => {
1135 let _ = self.handle.claim_body(&session, body.as_str());
1136 }
1137 CoreEffect::StreamClosed { session, operation } => {
1138 self.handler(&session, HandlerInput::StreamClosed(operation))
1139 .await
1140 }
1141 CoreEffect::CloseTransport {
1142 session,
1143 code,
1144 message,
1145 } => {
1146 let _ = (code, message);
1147 if let Some(io) = self.sessions.get(&session) {
1148 io.cancellation.cancel();
1149 }
1150 }
1151 CoreEffect::ScheduleSessionDeadline { session, deadline } => {
1152 if let Some(io) = self.sessions.get(&session) {
1153 io.deadline.send_replace(Some(deadline));
1154 }
1155 }
1156 CoreEffect::RouteExportAcked { session } => {
1157 if let Some(io) = self.sessions.get(&session) {
1158 io.routes.send_replace(true);
1159 }
1160 }
1161 CoreEffect::ScheduleClientDeadline {
1162 session,
1163 operation,
1164 deadline,
1165 } => spawn_client_timeout(
1166 session.clone(),
1167 operation,
1168 deadline,
1169 self.handle.clone(),
1170 self.sessions
1171 .get(&session)
1172 .map(|io| io.cancellation.clone()),
1173 ),
1174 CoreEffect::Send {
1175 effect,
1176 session,
1177 frame,
1178 } => {
1179 if frame.head.kind == unb_core::Kind::Error {
1180 let _ = self.core.handle(
1181 clock_now(),
1182 CoreInput::SendCompleted {
1183 effect,
1184 result: SendResult::Written,
1185 },
1186 );
1187 self.write(&session, frame.into_envelope(), None, false)
1188 .await;
1189 } else {
1190 self.write(&session, frame.into_envelope(), Some(effect), false)
1191 .await;
1192 }
1193 }
1194 CoreEffect::SendProtocol {
1195 effect,
1196 session,
1197 envelope,
1198 } => {
1199 self.write(&session, envelope, Some(effect), false).await;
1200 }
1201 CoreEffect::ContinueDiscovery { stream } => {
1202 let _ = self
1203 .core
1204 .handle(clock_now(), CoreInput::ContinueDiscovery { stream });
1205 }
1206 CoreEffect::SessionRetired { session, reason } => {
1207 #[cfg(feature = "observability")]
1208 {
1209 metrics::counter!("unb_sessions_retired", "reason" => format!("{reason:?}"))
1210 .increment(1);
1211 tracing::debug!(session = %session, reason = ?reason, "session retired");
1212 }
1213 if let Some(outcome) = self.outcomes.remove(&session) {
1214 let _ = outcome.send(Some(SessionOutcome::Retired(reason)));
1215 }
1216 if let Some(io) = self.sessions.remove(&session) {
1217 io.cancellation.cancel();
1218 }
1219 self.pending_bodies
1220 .lock()
1221 .expect("pending body registry")
1222 .retain(|(owner, _), _| owner != &session);
1223 self.handle
1224 .token_bodies
1225 .lock()
1226 .expect("body token registry")
1227 .retain(|(owner, _), _| owner != &session);
1228 self.execute(CoreEffect::SessionRetired { session, reason });
1229 }
1230 CoreEffect::SessionEstablished { session, peer } => {
1231 let effect = CoreEffect::SessionEstablished {
1232 session: session.clone(),
1233 peer,
1234 };
1235 if let Some(input) = self.executor.execute(effect, self.handle.clone()).await {
1236 let _ = self.core.handle(clock_now(), input);
1237 }
1238 if let Some(outcome) = self.outcomes.get(&session) {
1239 let _ = outcome.send(Some(SessionOutcome::Established));
1240 }
1241 }
1242 CoreEffect::ForwardRelay {
1243 effect,
1244 source,
1245 target,
1246 mut frame,
1247 ..
1248 } => {
1249 if let Some(body_id) = frame.body.take() {
1250 let Some(body) = self.handle.claim_body(&source.session, body_id.as_str())
1251 else {
1252 let _ = self.core.handle(
1253 clock_now(),
1254 CoreInput::RelayForwardCompleted {
1255 effect,
1256 result: SendResult::Refused {
1257 code: unb_core::ErrorCode::Protocol,
1258 message: "relay body capability is unavailable".into(),
1259 },
1260 },
1261 );
1262 continue;
1263 };
1264 match self.handle.register_body(&target.session, body) {
1265 Ok(target_body) => frame.body = Some(target_body),
1266 Err(error) => {
1267 let _ = self.core.handle(
1268 clock_now(),
1269 CoreInput::RelayForwardCompleted {
1270 effect,
1271 result: SendResult::Refused {
1272 code: unb_core::ErrorCode::Busy,
1273 message: error.to_string(),
1274 },
1275 },
1276 );
1277 continue;
1278 }
1279 }
1280 }
1281 self.write(&target.session, frame.into_envelope(), Some(effect), true)
1282 .await;
1283 }
1284 effect @ (CoreEffect::RouteSnapshotApplied { .. }
1285 | CoreEffect::RouteDeltaApplied { .. }
1286 | CoreEffect::RouteSessionWithdrawn { .. }) => {
1287 if let Some(input) = self.executor.execute(effect, self.handle.clone()).await {
1288 let _ = self.core.handle(clock_now(), input);
1289 }
1290 }
1291 effect => self.execute(effect),
1292 }
1293 }
1294 }
1295
1296 fn execute(&self, effect: CoreEffect) {
1297 let executor = self.executor.clone();
1298 let handle = self.handle.clone();
1299 n0_future::task::spawn(async move {
1300 if let Some(input) = executor.execute(effect, handle.clone()).await {
1301 let _ = handle.submit(input).await;
1302 }
1303 });
1304 }
1305
1306 async fn write(
1307 &mut self,
1308 session: &SessionId,
1309 mut envelope: Envelope,
1310 completion: Option<EffectId>,
1311 relay: bool,
1312 ) {
1313 if is_operation_envelope(&envelope) {
1314 let mode = self.sessions.get(session).and_then(|io| io.transport_mode);
1315 match mode {
1316 None if self
1317 .sessions
1318 .get(session)
1319 .is_some_and(|io| io.bodies.is_some()) =>
1320 {
1321 let queued = self.sessions.get_mut(session).is_some_and(|io| {
1322 if io.pending_mode_writes.len() >= PENDING_MODE_WRITES_MAX {
1323 false
1324 } else {
1325 io.pending_mode_writes.push_back(PendingModeWrite {
1326 envelope,
1327 completion,
1328 relay,
1329 });
1330 true
1331 }
1332 });
1333 if !queued {
1334 if let Some(effect) = completion {
1335 self.complete_send(effect, relay, SendResult::ReservationTimedOut);
1336 }
1337 }
1338 return;
1339 }
1340 Some(unb_transport::TransportMode::OperationStreamsV1) => {
1341 self.write_operation(session, envelope, completion, relay)
1342 .await;
1343 return;
1344 }
1345 _ => {}
1346 }
1347 }
1348 if envelope.kind == unb_core::Kind::Cancel {
1349 if let Some(corr) = &envelope.corr {
1350 self.handle.cancel_body_pump(session, corr);
1351 }
1352 }
1353 let body = match (&envelope.body_token, &envelope.corr) {
1354 (Some(token), _) => self.handle.claim_body(session, token),
1355 (None, Some(corr))
1356 if envelope.kind.is_application_request()
1357 || envelope.kind == unb_core::Kind::Response =>
1358 {
1359 let pending = self
1360 .pending_bodies
1361 .lock()
1362 .expect("pending body registry")
1363 .remove(&(session.clone(), corr.clone()));
1364 if let Some(stream) = pending {
1365 envelope.body_token = Some(mint_body_token());
1366 Some(WireBody::Stream(stream))
1367 } else {
1368 None
1369 }
1370 }
1371 _ => None,
1372 };
1373 let Some(io) = self.sessions.get(session) else {
1374 if let Some(effect) = completion {
1375 self.complete_send(effect, relay, SendResult::Closed);
1376 }
1377 return;
1378 };
1379 if let Some(WireBody::Bytes(payload)) = body {
1380 envelope.body_token = None;
1381 envelope.payload = payload;
1382 } else if let Some(WireBody::Stream(stream)) = body {
1383 if let Some(peer) = io.bodies.clone().filter(|bodies| bodies.can_open()) {
1384 let token = envelope.body_token.clone().expect("tokenized body");
1385 let pump_cancellation = io.cancellation.child_token();
1386 if let Some(corr) = &envelope.corr {
1387 self.handle.register_body_pump(
1388 session,
1389 corr,
1390 &token,
1391 pump_cancellation.clone(),
1392 );
1393 }
1394 spawn_body_pump(
1395 peer,
1396 token,
1397 stream,
1398 io.byte_budget.clone(),
1399 pump_cancellation,
1400 self.handle.clone(),
1401 envelope.corr.clone().map(|corr| (session.clone(), corr)),
1402 );
1403 } else {
1404 envelope.body_token = None;
1405 spawn_body_collect(
1406 self.handle.clone(),
1407 session.clone(),
1408 envelope,
1409 stream,
1410 io.ws_collect_ceiling,
1411 completion,
1412 relay,
1413 io.cancellation.clone(),
1414 );
1415 return;
1416 }
1417 }
1418 let envelope = match io
1419 .byte_budget
1420 .clone()
1421 .try_acquire_many_owned(WriteRequest::weight(&envelope))
1422 {
1423 Ok(budget) => {
1424 let request = WriteRequest {
1425 envelope,
1426 completion,
1427 relay,
1428 _budget: budget,
1429 };
1430 match io.outbound.try_send(request) {
1431 Ok(()) => {
1432 if let Some(effect) = completion {
1433 self.complete_send(effect, relay, SendResult::Reserved);
1434 }
1435 return;
1436 }
1437 Err(mpsc::error::TrySendError::Closed(_)) => {
1438 if let Some(effect) = completion {
1439 self.complete_send(effect, relay, SendResult::Closed);
1440 }
1441 return;
1442 }
1443 Err(mpsc::error::TrySendError::Full(request)) => {
1444 #[cfg(feature = "observability")]
1445 {
1446 metrics::counter!("unb_write_queue_full").increment(1);
1447 tracing::warn!(session = %session, "outbound write queue is full");
1448 }
1449 request.envelope
1450 }
1451 }
1452 }
1453 Err(_) => {
1454 #[cfg(feature = "observability")]
1455 {
1456 metrics::counter!("unb_write_budget_exhausted").increment(1);
1457 tracing::warn!(session = %session, "outbound byte budget is exhausted");
1458 }
1459 envelope
1460 }
1461 };
1462 if completion.is_none()
1463 || io
1464 .reservation_pending
1465 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
1466 .is_ok()
1467 {
1468 let timeout = if matches!(
1469 envelope.kind,
1470 unb_core::Kind::Response | unb_core::Kind::Error | unb_core::Kind::Cancel
1471 ) {
1472 TERMINAL_SEND_TIMEOUT
1473 } else {
1474 RELAY_BACKPRESSURE_TIMEOUT
1475 };
1476 reserve_write(
1477 io.outbound.clone(),
1478 io.byte_budget.clone(),
1479 envelope,
1480 completion,
1481 relay,
1482 self.handle.clone(),
1483 io.cancellation.clone(),
1484 completion.map(|_| io.reservation_pending.clone()),
1485 timeout,
1486 );
1487 } else if let Some(effect) = completion {
1488 self.complete_send(effect, relay, SendResult::ReservationTimedOut);
1489 }
1490 }
1491
1492 fn start_operation_acceptor(&self, session: &SessionId) {
1493 let Some(io) = self.sessions.get(session) else {
1494 return;
1495 };
1496 let Some(bodies) = io.bodies.clone() else {
1497 return;
1498 };
1499 spawn_operation_acceptor(
1500 session.clone(),
1501 bodies,
1502 io.operation_lanes.clone(),
1503 io.operation_slots.clone(),
1504 io.byte_budget.clone(),
1505 self.handle.clone(),
1506 io.cancellation.clone(),
1507 );
1508 }
1509
1510 async fn write_operation(
1511 &mut self,
1512 session: &SessionId,
1513 mut envelope: Envelope,
1514 completion: Option<EffectId>,
1515 relay: bool,
1516 ) {
1517 let Some(corr) = envelope.corr.clone() else {
1518 if let Some(effect) = completion {
1519 self.complete_send(
1520 effect,
1521 relay,
1522 SendResult::WriteFailed("missing unb-corr".into()),
1523 );
1524 }
1525 return;
1526 };
1527 let body = match &envelope.body_token {
1528 Some(token) => self.handle.claim_body(session, token),
1529 None if envelope.kind.is_application_request()
1530 || envelope.kind == unb_core::Kind::Response =>
1531 {
1532 self.pending_bodies
1533 .lock()
1534 .expect("pending body registry")
1535 .remove(&(session.clone(), corr.clone()))
1536 .map(WireBody::Stream)
1537 }
1538 None => None,
1539 };
1540 let body = match body {
1541 Some(WireBody::Bytes(payload)) => {
1542 envelope.payload = payload;
1543 None
1544 }
1545 Some(WireBody::Stream(stream)) => Some(stream),
1546 None => None,
1547 };
1548 envelope.body_token = None;
1549 let Some(io) = self.sessions.get(session) else {
1550 if let Some(effect) = completion {
1551 self.complete_send(effect, relay, SendResult::Closed);
1552 }
1553 return;
1554 };
1555 if envelope.kind == unb_core::Kind::Cancel {
1556 let lane = io
1557 .operation_lanes
1558 .lock()
1559 .expect("operation lane registry")
1560 .remove(&corr);
1561 if let Some(control) = lane.and_then(|lane| lane.control) {
1562 let _ = control.reset(unb_transport::operation::RESET_CANCELLED);
1563 let _ = control.stop(unb_transport::operation::RESET_CANCELLED);
1564 }
1565 if let Some(effect) = completion {
1566 self.complete_send(effect, relay, SendResult::Written);
1567 }
1568 return;
1569 }
1570 if envelope.kind.is_application_request() {
1571 spawn_outbound_operation(
1572 session.clone(),
1573 corr,
1574 envelope,
1575 body,
1576 completion,
1577 relay,
1578 io.bodies
1579 .clone()
1580 .expect("operation mode has stream facility"),
1581 io.operation_lanes.clone(),
1582 io.operation_slots.clone(),
1583 io.byte_budget.clone(),
1584 self.handle.clone(),
1585 io.cancellation.clone(),
1586 );
1587 return;
1588 }
1589 let lane = io
1590 .operation_lanes
1591 .lock()
1592 .expect("operation lane registry")
1593 .get(&corr)
1594 .cloned();
1595 let Some(sender) = lane.and_then(|lane| lane.outbound) else {
1596 if let Some(effect) = completion {
1597 self.complete_send(effect, relay, SendResult::Written);
1598 }
1599 return;
1600 };
1601 let budget = match io
1602 .byte_budget
1603 .clone()
1604 .try_acquire_many_owned(WriteRequest::weight(&envelope))
1605 {
1606 Ok(budget) => budget,
1607 Err(_) => {
1608 if let Some(effect) = completion {
1609 self.complete_send(effect, relay, SendResult::ReservationTimedOut);
1610 }
1611 return;
1612 }
1613 };
1614 let request = OperationWriteRequest {
1615 envelope,
1616 completion,
1617 relay,
1618 body,
1619 _budget: budget,
1620 };
1621 match sender.try_send(request) {
1622 Ok(()) => {
1623 if let Some(effect) = completion {
1624 self.complete_send(effect, relay, SendResult::Reserved);
1625 }
1626 }
1627 Err(_) => {
1628 if let Some(effect) = completion {
1629 self.complete_send(effect, relay, SendResult::ReservationTimedOut);
1630 }
1631 }
1632 }
1633 }
1634
1635 fn complete_send(&mut self, effect: EffectId, relay: bool, result: SendResult) {
1636 let _ = self
1637 .core
1638 .handle(clock_now(), send_completion(effect, relay, result));
1639 }
1640
1641 async fn handler(&self, session: &SessionId, event: HandlerInput) {
1642 if let Some(io) = self.sessions.get(session) {
1643 let _ = io.handler.send(event).await;
1644 }
1645 }
1646}
1647
1648enum HandlerInput {
1649 Deliver(Envelope),
1650 StreamClosed(ClientOperationId),
1651}
1652
1653fn handler_channel<H: SessionHandler>(mut handler: H) -> mpsc::Sender<HandlerInput> {
1654 let (events, mut receiver) = mpsc::channel(SESSION_QUEUE);
1655 n0_future::task::spawn(async move {
1656 while let Some(event) = receiver.recv().await {
1657 match event {
1658 HandlerInput::Deliver(envelope) => handler.deliver(envelope).await,
1659 HandlerInput::StreamClosed(operation) => handler.stream_closed(operation).await,
1660 }
1661 }
1662 });
1663 events
1664}
1665
1666fn spawn_directives(
1667 session: SessionId,
1668 mut directives: mpsc::Receiver<Directive>,
1669 handle: ProtocolCoreHandle,
1670 cancellation: CancellationToken,
1671) {
1672 n0_future::task::spawn(async move {
1673 loop {
1674 let directive = tokio::select! {
1675 biased;
1676 () = cancellation.cancelled() => return,
1677 directive = directives.recv() => directive,
1678 };
1679 let Some(directive) = directive else { return };
1680 if handle
1681 .inputs
1682 .send(ActorInput::Directive(session.clone(), directive))
1683 .await
1684 .is_err()
1685 {
1686 return;
1687 }
1688 }
1689 });
1690}
1691
1692fn is_operation_envelope(envelope: &Envelope) -> bool {
1693 envelope.corr.is_some()
1694 && (envelope.kind.is_application_request()
1695 || envelope.kind.is_application_response()
1696 || envelope.kind == unb_core::Kind::Cancel)
1697}
1698
1699fn spawn_operation_acceptor(
1700 session: SessionId,
1701 bodies: Arc<unb_transport::ConnectionStreams>,
1702 lanes: OperationLanes,
1703 operation_slots: Arc<Semaphore>,
1704 byte_budget: Arc<Semaphore>,
1705 handle: ProtocolCoreHandle,
1706 cancellation: CancellationToken,
1707) {
1708 n0_future::task::spawn(async move {
1709 loop {
1710 let operation = tokio::select! {
1711 biased;
1712 () = cancellation.cancelled() => return,
1713 operation = bodies.accept_operation() => operation,
1714 };
1715 let Ok(operation) = operation else {
1716 return;
1717 };
1718 let Ok(operation_slot) = operation_slots.clone().try_acquire_owned() else {
1719 if let Some(control) = operation.control {
1720 let _ = control.reset(unb_transport::operation::RESET_BUSY);
1721 let _ = control.stop(unb_transport::operation::RESET_BUSY);
1722 }
1723 continue;
1724 };
1725 let corr = operation.corr.clone();
1726 let (outbound, receiver) = mpsc::channel(OPERATION_WRITE_QUEUE);
1727 let lane = OperationLane {
1728 outbound: Some(outbound),
1729 control: operation.control.clone(),
1730 };
1731 let admitted = {
1732 let mut lanes = lanes.lock().expect("operation lane registry");
1733 if lanes.contains_key(&corr) {
1734 false
1735 } else {
1736 lanes.insert(corr.clone(), lane);
1737 true
1738 }
1739 };
1740 if !admitted {
1741 if let Some(control) = operation.control {
1742 let _ = control.reset(unb_transport::operation::RESET_PROTOCOL);
1743 }
1744 continue;
1745 }
1746 spawn_accepted_operation(
1747 session.clone(),
1748 operation,
1749 receiver,
1750 lanes.clone(),
1751 byte_budget.clone(),
1752 handle.clone(),
1753 cancellation.child_token(),
1754 operation_slot,
1755 );
1756 }
1757 });
1758}
1759
1760#[allow(clippy::too_many_arguments)]
1761fn spawn_outbound_operation(
1762 session: SessionId,
1763 corr: String,
1764 envelope: Envelope,
1765 body: Option<BodyStream>,
1766 completion: Option<EffectId>,
1767 relay: bool,
1768 bodies: Arc<unb_transport::ConnectionStreams>,
1769 lanes: OperationLanes,
1770 operation_slots: Arc<Semaphore>,
1771 byte_budget: Arc<Semaphore>,
1772 handle: ProtocolCoreHandle,
1773 cancellation: CancellationToken,
1774) {
1775 n0_future::task::spawn(async move {
1776 let open_byte_budget = byte_budget.clone();
1777 let open = async {
1778 let operation_slot = operation_slots
1779 .acquire_owned()
1780 .await
1781 .map_err(|_| unb_transport::TransportError::Closed)?;
1782 let budget = open_byte_budget
1783 .acquire_many_owned(WriteRequest::weight(&envelope))
1784 .await
1785 .map_err(|_| unb_transport::TransportError::Closed)?;
1786 let operation = bodies
1787 .open_operation(&corr)
1788 .await?
1789 .ok_or(unb_transport::TransportError::Closed)?;
1790 Ok::<_, unb_transport::TransportError>((operation, budget, operation_slot))
1791 };
1792 let opened = tokio::select! {
1793 biased;
1794 () = cancellation.cancelled() => return,
1795 opened = n0_future::time::timeout(RELAY_BACKPRESSURE_TIMEOUT, open) => opened,
1796 };
1797 let Ok(Ok((operation, _budget, _operation_slot))) = opened else {
1798 if let Some(effect) = completion {
1799 let _ = handle
1800 .submit(send_completion(
1801 effect,
1802 relay,
1803 SendResult::ReservationTimedOut,
1804 ))
1805 .await;
1806 }
1807 return;
1808 };
1809 lanes.lock().expect("operation lane registry").insert(
1810 corr.clone(),
1811 OperationLane {
1812 outbound: None,
1813 control: operation.control.clone(),
1814 },
1815 );
1816 if let Some(effect) = completion {
1817 if handle
1818 .submit(send_completion(effect, relay, SendResult::Reserved))
1819 .await
1820 .is_err()
1821 {
1822 return;
1823 }
1824 }
1825 let mut writer = unb_transport::operation::OperationRecordWriter::new(operation.write);
1826 let sent = write_operation_envelope(
1827 &mut writer,
1828 envelope,
1829 body,
1830 byte_budget.clone(),
1831 cancellation.clone(),
1832 )
1833 .await;
1834 let sent = match sent {
1835 Ok(()) => writer.finish().await,
1836 Err(error) => Err(error),
1837 };
1838 if let Some(effect) = completion {
1839 let _ = handle
1840 .submit(send_completion(effect, relay, SendResult::Written))
1841 .await;
1842 }
1843 if let Err(error) = sent {
1844 if !operation_peer_stopped_cancelled(&error) {
1845 if let Some(control) = &operation.control {
1846 let _ = control.reset(error.reset_code());
1847 }
1848 let _ = submit_operation_end(
1849 &handle,
1850 &session,
1851 &corr,
1852 OperationDirection::Opening,
1853 operation_outcome(&error),
1854 )
1855 .await;
1856 lanes.lock().expect("operation lane registry").remove(&corr);
1857 return;
1858 }
1859 }
1860 let result = read_operation_direction(
1861 &session,
1862 &corr,
1863 operation.read,
1864 OperationDirection::Return,
1865 &handle,
1866 &cancellation,
1867 )
1868 .await;
1869 if let Err(error) = result {
1870 if let Some(control) = &operation.control {
1871 let _ = control.reset(error.reset_code());
1872 let _ = control.stop(error.reset_code());
1873 }
1874 let _ = submit_operation_end(
1875 &handle,
1876 &session,
1877 &corr,
1878 OperationDirection::Return,
1879 operation_outcome(&error),
1880 )
1881 .await;
1882 }
1883 lanes.lock().expect("operation lane registry").remove(&corr);
1884 });
1885}
1886
1887#[allow(clippy::too_many_arguments)]
1888fn spawn_accepted_operation(
1889 session: SessionId,
1890 operation: unb_transport::operation::OperationIo,
1891 receiver: mpsc::Receiver<OperationWriteRequest>,
1892 lanes: OperationLanes,
1893 byte_budget: Arc<Semaphore>,
1894 handle: ProtocolCoreHandle,
1895 cancellation: CancellationToken,
1896 operation_slot: OwnedSemaphorePermit,
1897) {
1898 let corr = operation.corr.clone();
1899 let control = operation.control.clone();
1900 let read_corr = corr.clone();
1901 let read_lanes = lanes.clone();
1902 let read_handle = handle.clone();
1903 let read_cancellation = cancellation.clone();
1904 let writer_session = session.clone();
1905 n0_future::task::spawn(async move {
1906 let result = read_operation_direction(
1907 &session,
1908 &read_corr,
1909 operation.read,
1910 OperationDirection::Opening,
1911 &read_handle,
1912 &read_cancellation,
1913 )
1914 .await;
1915 if let Err(error) = result {
1916 if operation_body_consumer_closed(&error) {
1917 if let Some(control) = control {
1918 let _ = control.stop(unb_transport::operation::RESET_CANCELLED);
1919 }
1920 return;
1921 }
1922 if let Some(control) = control {
1923 let _ = control.reset(error.reset_code());
1924 let _ = control.stop(error.reset_code());
1925 }
1926 let _ = submit_operation_end(
1927 &read_handle,
1928 &session,
1929 &read_corr,
1930 OperationDirection::Opening,
1931 operation_outcome(&error),
1932 )
1933 .await;
1934 read_lanes
1935 .lock()
1936 .expect("operation lane registry")
1937 .remove(&read_corr);
1938 }
1939 });
1940 spawn_operation_writer(
1941 writer_session,
1942 corr,
1943 operation.write,
1944 operation.control,
1945 receiver,
1946 lanes,
1947 byte_budget,
1948 handle,
1949 cancellation,
1950 operation_slot,
1951 );
1952}
1953
1954#[allow(clippy::too_many_arguments)]
1955fn spawn_operation_writer(
1956 session: SessionId,
1957 corr: String,
1958 write: Box<dyn tokio::io::AsyncWrite + Send + Unpin>,
1959 control: Option<unb_transport::operation::OperationControl>,
1960 mut receiver: mpsc::Receiver<OperationWriteRequest>,
1961 lanes: OperationLanes,
1962 byte_budget: Arc<Semaphore>,
1963 handle: ProtocolCoreHandle,
1964 cancellation: CancellationToken,
1965 _operation_slot: OwnedSemaphorePermit,
1966) {
1967 n0_future::task::spawn(async move {
1968 let mut writer = unb_transport::operation::OperationRecordWriter::new(write);
1969 loop {
1970 let request = tokio::select! {
1971 biased;
1972 () = cancellation.cancelled() => break,
1973 request = receiver.recv() => request,
1974 };
1975 let Some(request) = request else {
1976 break;
1977 };
1978 let terminal = matches!(
1979 request.envelope.kind,
1980 unb_core::Kind::Response | unb_core::Kind::Error
1981 );
1982 let result = write_operation_envelope(
1983 &mut writer,
1984 request.envelope,
1985 request.body,
1986 byte_budget.clone(),
1987 cancellation.clone(),
1988 )
1989 .await;
1990 let result = if result.is_ok() && terminal {
1991 writer.finish().await
1992 } else {
1993 result
1994 };
1995 if let Some(effect) = request.completion {
1996 let _ = handle
1997 .submit(send_completion(effect, request.relay, SendResult::Written))
1998 .await;
1999 }
2000 if result.is_err() || terminal {
2001 if let Err(error) = result {
2002 if let Some(control) = &control {
2003 let _ = control.reset(error.reset_code());
2004 }
2005 let _ = submit_operation_end(
2006 &handle,
2007 &session,
2008 &corr,
2009 OperationDirection::Return,
2010 operation_outcome(&error),
2011 )
2012 .await;
2013 }
2014 break;
2015 }
2016 }
2017 lanes.lock().expect("operation lane registry").remove(&corr);
2018 });
2019}
2020
2021async fn write_operation_envelope<W>(
2022 writer: &mut unb_transport::operation::OperationRecordWriter<W>,
2023 mut envelope: Envelope,
2024 body: Option<BodyStream>,
2025 byte_budget: Arc<Semaphore>,
2026 cancellation: CancellationToken,
2027) -> Result<(), unb_transport::operation::OperationCodecError>
2028where
2029 W: tokio::io::AsyncWrite + Unpin,
2030{
2031 let Some(body) = body else {
2032 return writer
2033 .write_record(
2034 unb_transport::operation::OperationRecordKind::CompleteMessage,
2035 &envelope.encode(),
2036 )
2037 .await;
2038 };
2039 envelope.body_token = None;
2040 envelope.payload = Bytes::new();
2041 writer
2042 .write_record(
2043 unb_transport::operation::OperationRecordKind::Head,
2044 &envelope.encode(),
2045 )
2046 .await?;
2047 let mut body = MeteredBody::new(body, byte_budget, cancellation.clone());
2048 loop {
2049 let chunk = body.next_chunk().await.map_err(|error| {
2050 unb_transport::operation::OperationCodecError::Io(error.to_string())
2051 })?;
2052 let Some(chunk) = chunk else {
2053 if cancellation.is_cancelled() {
2054 return Err(unb_transport::operation::OperationCodecError::Io(
2055 "operation body cancelled".into(),
2056 ));
2057 }
2058 break;
2059 };
2060 for piece in chunk.bytes.chunks(OPERATION_BODY_CHUNK_BYTES) {
2061 writer
2062 .write_record(
2063 unb_transport::operation::OperationRecordKind::BodyChunk,
2064 piece,
2065 )
2066 .await?;
2067 }
2068 }
2069 writer
2070 .write_record(unb_transport::operation::OperationRecordKind::BodyEnd, &[])
2071 .await
2072}
2073
2074#[derive(Clone, Copy)]
2075enum OperationDirection {
2076 Opening,
2077 Return,
2078}
2079
2080async fn read_operation_direction(
2081 session: &SessionId,
2082 corr: &str,
2083 read: Box<dyn tokio::io::AsyncRead + Send + Unpin>,
2084 direction: OperationDirection,
2085 handle: &ProtocolCoreHandle,
2086 cancellation: &CancellationToken,
2087) -> Result<(), unb_transport::operation::OperationCodecError> {
2088 let mut reader = unb_transport::operation::OperationRecordReader::new(read);
2089 let mut records = 0usize;
2090 let mut terminal = false;
2091 let mut body_sender: Option<mpsc::Sender<Result<Bytes, unb_core::CoreError>>> = None;
2092 loop {
2093 let kind = tokio::select! {
2094 biased;
2095 () = cancellation.cancelled() => {
2096 if let Some(sender) = body_sender.take() {
2097 let _ = sender.send(Err(unb_core::CoreError::Malformed(
2098 "operation body cancelled".into(),
2099 ))).await;
2100 }
2101 return Ok(())
2102 },
2103 record = reader.read_record() => record,
2104 };
2105 let (kind, payload) = match kind {
2106 Ok(record) => record,
2107 Err(error) => {
2108 if let Some(sender) = body_sender.take() {
2109 let _ = sender
2110 .send(Err(unb_core::CoreError::Malformed(error.to_string())))
2111 .await;
2112 }
2113 return Err(error);
2114 }
2115 };
2116 match kind {
2117 unb_transport::operation::OperationRecordKind::CompleteMessage => {
2118 if body_sender.is_some() {
2119 return Err(unb_transport::operation::OperationCodecError::Protocol(
2120 "complete message arrived before body end".into(),
2121 ));
2122 }
2123 let envelope = decode_operation_message(payload, corr, direction, records)?;
2124 terminal |= matches!(
2125 envelope.kind,
2126 unb_core::Kind::Response | unb_core::Kind::Error
2127 );
2128 records += 1;
2129 let input = application_core_input(handle, session, envelope).map_err(|error| {
2130 unb_transport::operation::OperationCodecError::Protocol(error.to_string())
2131 })?;
2132 handle.submit(input).await.map_err(|error| {
2133 unb_transport::operation::OperationCodecError::Io(error.to_string())
2134 })?;
2135 }
2136 unb_transport::operation::OperationRecordKind::Head => {
2137 if body_sender.is_some() {
2138 return Err(unb_transport::operation::OperationCodecError::Protocol(
2139 "operation head arrived before body end".into(),
2140 ));
2141 }
2142 let mut envelope = decode_operation_message(payload, corr, direction, records)?;
2143 let (token, sender) = register_operation_body(handle, session)?;
2144 envelope.body_token = Some(token);
2145 terminal |= matches!(
2146 envelope.kind,
2147 unb_core::Kind::Response | unb_core::Kind::Error
2148 );
2149 records += 1;
2150 body_sender = Some(sender);
2151 let input = application_core_input(handle, session, envelope).map_err(|error| {
2152 unb_transport::operation::OperationCodecError::Protocol(error.to_string())
2153 })?;
2154 handle.submit(input).await.map_err(|error| {
2155 unb_transport::operation::OperationCodecError::Io(error.to_string())
2156 })?;
2157 }
2158 unb_transport::operation::OperationRecordKind::BodyChunk => {
2159 let Some(sender) = &body_sender else {
2160 return Err(unb_transport::operation::OperationCodecError::Protocol(
2161 "body chunk arrived without a head".into(),
2162 ));
2163 };
2164 sender.send(Ok(payload)).await.map_err(|_| {
2165 unb_transport::operation::OperationCodecError::Io(
2166 "operation body consumer closed".into(),
2167 )
2168 })?;
2169 }
2170 unb_transport::operation::OperationRecordKind::BodyEnd => {
2171 let Some(sender) = body_sender.take() else {
2172 return Err(unb_transport::operation::OperationCodecError::Protocol(
2173 "body end arrived without a head".into(),
2174 ));
2175 };
2176 drop(sender);
2177 }
2178 unb_transport::operation::OperationRecordKind::DirectionEnd => {
2179 if body_sender.is_some() {
2180 return Err(unb_transport::operation::OperationCodecError::Truncated(
2181 "direction ended before body end".into(),
2182 ));
2183 }
2184 if records == 0 || matches!(direction, OperationDirection::Return) && !terminal {
2185 return Err(unb_transport::operation::OperationCodecError::Protocol(
2186 "operation direction ended before its required message".into(),
2187 ));
2188 }
2189 reader.expect_fin().await?;
2190 submit_operation_end(
2191 handle,
2192 session,
2193 corr,
2194 direction,
2195 unb_core::OperationStreamOutcome::Clean,
2196 )
2197 .await
2198 .map_err(|error| {
2199 unb_transport::operation::OperationCodecError::Io(error.to_string())
2200 })?;
2201 return Ok(());
2202 }
2203 }
2204 }
2205}
2206
2207fn register_operation_body(
2208 handle: &ProtocolCoreHandle,
2209 session: &SessionId,
2210) -> Result<
2211 (String, mpsc::Sender<Result<Bytes, unb_core::CoreError>>),
2212 unb_transport::operation::OperationCodecError,
2213> {
2214 let token = mint_body_token();
2215 let (sender, receiver) = mpsc::channel(8);
2216 let stream: BodyStream = Box::pin(futures_util::stream::unfold(
2217 receiver,
2218 |mut receiver| async move { receiver.recv().await.map(|chunk| (chunk, receiver)) },
2219 ));
2220 let mut registry = handle.token_bodies.lock().expect("body token registry");
2221 let owned = registry
2222 .keys()
2223 .filter(|(owner, _)| owner == session)
2224 .count();
2225 if owned >= SESSION_BODY_TOKENS_MAX {
2226 return Err(unb_transport::operation::OperationCodecError::Protocol(
2227 "session operation body limit reached".into(),
2228 ));
2229 }
2230 registry.insert((session.clone(), token.clone()), WireBody::Stream(stream));
2231 Ok((token, sender))
2232}
2233
2234fn operation_outcome(
2235 error: &unb_transport::operation::OperationCodecError,
2236) -> unb_core::OperationStreamOutcome {
2237 match error {
2238 unb_transport::operation::OperationCodecError::Protocol(message) => {
2239 unb_core::OperationStreamOutcome::Protocol(message.clone())
2240 }
2241 unb_transport::operation::OperationCodecError::Truncated(_) => {
2242 unb_core::OperationStreamOutcome::Truncated
2243 }
2244 unb_transport::operation::OperationCodecError::Oversized { size, max } => {
2245 unb_core::OperationStreamOutcome::Protocol(format!(
2246 "operation record of {size} bytes exceeds max {max}"
2247 ))
2248 }
2249 unb_transport::operation::OperationCodecError::Io(message) => {
2250 unb_core::OperationStreamOutcome::Transport(message.clone())
2251 }
2252 }
2253}
2254
2255fn operation_body_consumer_closed(error: &unb_transport::operation::OperationCodecError) -> bool {
2256 matches!(
2257 error,
2258 unb_transport::operation::OperationCodecError::Io(message)
2259 if message == "operation body consumer closed"
2260 )
2261}
2262
2263fn operation_peer_stopped_cancelled(error: &unb_transport::operation::OperationCodecError) -> bool {
2264 matches!(
2265 error,
2266 unb_transport::operation::OperationCodecError::Io(message)
2267 if message.contains(&unb_transport::operation::RESET_CANCELLED.to_string())
2268 )
2269}
2270
2271async fn submit_operation_end(
2272 handle: &ProtocolCoreHandle,
2273 session: &SessionId,
2274 corr: &str,
2275 direction: OperationDirection,
2276 outcome: unb_core::OperationStreamOutcome,
2277) -> Result<(), WsError> {
2278 handle
2279 .submit(CoreInput::OperationStreamEnded {
2280 session: session.clone(),
2281 corr: unb_core::CorrelationId::from(corr.to_owned()),
2282 direction: match direction {
2283 OperationDirection::Opening => unb_core::OperationStreamDirection::Opening,
2284 OperationDirection::Return => unb_core::OperationStreamDirection::Return,
2285 },
2286 outcome,
2287 })
2288 .await
2289}
2290
2291fn decode_operation_message(
2292 payload: Bytes,
2293 corr: &str,
2294 direction: OperationDirection,
2295 records: usize,
2296) -> Result<Envelope, unb_transport::operation::OperationCodecError> {
2297 let envelope = Envelope::decode(payload).map_err(|error| {
2298 unb_transport::operation::OperationCodecError::Protocol(error.to_string())
2299 })?;
2300 if envelope.corr.as_deref() != Some(corr) {
2301 return Err(unb_transport::operation::OperationCodecError::Protocol(
2302 "operation record changed unb-corr".into(),
2303 ));
2304 }
2305 match direction {
2306 OperationDirection::Opening if records != 0 || !envelope.kind.is_application_request() => {
2307 Err(unb_transport::operation::OperationCodecError::Protocol(
2308 "opening direction requires exactly one application opener".into(),
2309 ))
2310 }
2311 OperationDirection::Return if !envelope.kind.is_application_response() => {
2312 Err(unb_transport::operation::OperationCodecError::Protocol(
2313 "return direction requires application responses".into(),
2314 ))
2315 }
2316 _ => Ok(envelope),
2317 }
2318}
2319
2320fn spawn_body_pump(
2321 peer: std::sync::Arc<unb_transport::ConnectionStreams>,
2322 token: String,
2323 stream: BodyStream,
2324 budget: Arc<Semaphore>,
2325 cancellation: CancellationToken,
2326 handle: ProtocolCoreHandle,
2327 registration: Option<(SessionId, String)>,
2328) {
2329 n0_future::task::spawn(async move {
2330 let pump = async {
2331 let Ok(mut writer) = peer.open_body(&token).await else {
2332 return;
2333 };
2334 let mut metered = MeteredBody::new(stream, budget, cancellation.clone());
2335 loop {
2336 match metered.next_chunk().await {
2337 Ok(Some(chunk)) => {
2338 if writer.write_chunk(&chunk.bytes).await.is_err() {
2339 return;
2340 }
2341 }
2342 Ok(None) => {
2343 if cancellation.is_cancelled() {
2344 writer.abort().await;
2345 } else {
2346 let _ = writer.finish().await;
2347 }
2348 return;
2349 }
2350 Err(_) => {
2351 writer.abort().await;
2352 return;
2353 }
2354 }
2355 }
2356 };
2357 pump.await;
2358 if let Some((session, corr)) = registration {
2359 handle.release_body_pump(&session, &corr, &token);
2360 }
2361 });
2362}
2363
2364#[allow(clippy::too_many_arguments)]
2365fn spawn_body_collect(
2366 handle: ProtocolCoreHandle,
2367 session: SessionId,
2368 mut envelope: Envelope,
2369 stream: BodyStream,
2370 ceiling: usize,
2371 completion: Option<EffectId>,
2372 relay: bool,
2373 cancellation: CancellationToken,
2374) {
2375 n0_future::task::spawn(async move {
2376 let collected = tokio::select! {
2377 biased;
2378 () = cancellation.cancelled() => {
2379 if let Some(effect) = completion {
2380 let _ = handle
2381 .submit(send_completion(effect, relay, SendResult::Cancelled))
2382 .await;
2383 }
2384 return;
2385 }
2386 collected = WireBody::Stream(stream).collect_to(ceiling) => collected,
2387 };
2388 match collected {
2389 Ok(payload) => {
2390 envelope.payload = payload;
2391 let _ = handle
2392 .inputs
2393 .send(ActorInput::Write {
2394 session,
2395 envelope,
2396 completion,
2397 relay,
2398 })
2399 .await;
2400 }
2401 Err(error) => {
2402 let (code, message) = match error {
2403 unb_core::CoreError::BodyTooLarge(_) => (
2404 unb_core::ErrorCode::PayloadTooLarge,
2405 "streaming body exceeds this route's WS collect ceiling; raise the \
2406 ceiling or reach this node over WebTransport"
2407 .to_string(),
2408 ),
2409 other => (unb_core::ErrorCode::Protocol, other.to_string()),
2410 };
2411 if let Some(effect) = completion {
2412 let refused = SendResult::Refused { code, message };
2413 let _ = handle.submit(send_completion(effect, relay, refused)).await;
2414 } else if envelope.kind.is_application_request() {
2415 if let Some(corr) = envelope.corr.clone() {
2416 let outcome = if code == unb_core::ErrorCode::PayloadTooLarge {
2417 unb_core::OperationStreamOutcome::PayloadTooLarge(message)
2418 } else {
2419 unb_core::OperationStreamOutcome::Protocol(message)
2420 };
2421 let _ = handle
2422 .submit(CoreInput::OperationStreamEnded {
2423 session,
2424 corr: unb_core::CorrelationId::from(corr),
2425 direction: unb_core::OperationStreamDirection::Opening,
2426 outcome,
2427 })
2428 .await;
2429 }
2430 } else if envelope.kind == unb_core::Kind::Response {
2431 envelope.kind = unb_core::Kind::Error;
2432 envelope.payload = Envelope::encode_payload(&serde_json::json!({
2433 "code": code,
2434 "message": message,
2435 }));
2436 let _ = handle
2437 .inputs
2438 .send(ActorInput::Write {
2439 session,
2440 envelope,
2441 completion: None,
2442 relay: false,
2443 })
2444 .await;
2445 }
2446 }
2447 }
2448 });
2449}
2450
2451fn spawn_reader(
2452 session: SessionId,
2453 mut reader: PipeReader,
2454 handle: ProtocolCoreHandle,
2455 cancellation: CancellationToken,
2456 bodies: crate::transport::SessionStreams,
2457) {
2458 n0_future::task::spawn(async move {
2459 loop {
2460 let received = tokio::select! {
2461 biased;
2462 () = cancellation.cancelled() => {
2463 let _ = handle.submit(CoreInput::SessionClosed { session }).await;
2464 return;
2465 },
2466 received = reader.recv() => received,
2467 };
2468 let envelope = match received {
2469 Ok(Some((envelope, _body))) => {
2470 if let Some(token) = &envelope.body_token {
2471 if !(envelope.kind.is_application_request()
2472 || envelope.kind == unb_core::Kind::Response)
2473 {
2474 let _ = handle.submit(CoreInput::SessionFailed { session }).await;
2475 return;
2476 }
2477 {
2478 let Some(peer) = bodies.clone() else {
2479 let _ = handle.submit(CoreInput::SessionFailed { session }).await;
2480 return;
2481 };
2482 let lazy_token = token.clone();
2483 let accepted = futures_util::stream::once(async move {
2484 peer.accept_body(&lazy_token, RELAY_BACKPRESSURE_TIMEOUT)
2485 .await
2486 });
2487 let stream: BodyStream =
2488 Box::pin(futures_util::StreamExt::flat_map(accepted, |outcome| {
2489 match outcome {
2490 Ok(reader) => futures_util::StreamExt::boxed(
2491 futures_util::StreamExt::map(reader, |chunk| {
2492 chunk.map_err(|error| {
2493 unb_core::CoreError::Malformed(
2494 error.to_string(),
2495 )
2496 })
2497 }),
2498 ),
2499 Err(error) => futures_util::StreamExt::boxed(
2500 futures_util::stream::once(async move {
2501 Err(unb_core::CoreError::Malformed(
2502 error.to_string(),
2503 ))
2504 }),
2505 ),
2506 }
2507 }));
2508 let rejected = {
2509 let mut registry =
2510 handle.token_bodies.lock().expect("body token registry");
2511 let key = (session.clone(), token.clone());
2512 let owned = registry
2513 .keys()
2514 .filter(|(owner, _)| owner == &session)
2515 .count();
2516 if registry.contains_key(&key) || owned >= SESSION_BODY_TOKENS_MAX {
2517 true
2518 } else {
2519 registry.insert(key, WireBody::Stream(stream));
2520 false
2521 }
2522 };
2523 if rejected {
2524 let _ = handle.submit(CoreInput::SessionFailed { session }).await;
2525 return;
2526 }
2527 }
2528 }
2529 envelope
2530 }
2531 Ok(None) => {
2532 let _ = handle.submit(CoreInput::SessionClosed { session }).await;
2533 return;
2534 }
2535 Err(_) => {
2536 #[cfg(feature = "observability")]
2537 {
2538 metrics::counter!("unb_transport_failures").increment(1);
2539 tracing::warn!(session = %session, "transport read failed");
2540 }
2541 let _ = handle.submit(CoreInput::SessionFailed { session }).await;
2542 return;
2543 }
2544 };
2545 if envelope.kind == unb_core::Kind::Cancel {
2546 if let Some(corr) = &envelope.corr {
2547 handle.cancel_body_pump(&session, corr);
2548 }
2549 }
2550 let input = match application_core_input(&handle, &session, envelope) {
2551 Ok(input) => input,
2552 Err(_) => {
2553 let _ = handle.submit(CoreInput::SessionFailed { session }).await;
2554 return;
2555 }
2556 };
2557 if handle.submit(input).await.is_err() {
2558 return;
2559 }
2560 }
2561 });
2562}
2563
2564fn spawn_writer(
2565 session: SessionId,
2566 mut writer: PipeWriter,
2567 mut outbound: mpsc::Receiver<WriteRequest>,
2568 handle: ProtocolCoreHandle,
2569 cancellation: CancellationToken,
2570) {
2571 n0_future::task::spawn(async move {
2572 loop {
2573 let request = tokio::select! {
2574 biased;
2575 () = cancellation.cancelled() => {
2576 let farewell = async {
2577 while let Ok(request) = outbound.try_recv() {
2578 if writer.feed(request.envelope).await.is_err() {
2579 break;
2580 }
2581 }
2582 let _ = writer.flush().await;
2583 let _ = writer.close().await;
2584 };
2585 let _ = n0_future::time::timeout(TERMINAL_SEND_TIMEOUT, farewell).await;
2586 return;
2587 },
2588 request = outbound.recv() => request,
2589 };
2590 let Some(request) = request else {
2591 let _ = handle
2592 .submit(CoreInput::SessionClosed {
2593 session: session.clone(),
2594 })
2595 .await;
2596 return;
2597 };
2598 let mut batch = Vec::with_capacity(WRITE_BATCH);
2599 batch.push(request);
2600 while batch.len() < WRITE_BATCH {
2601 match outbound.try_recv() {
2602 Ok(request) => batch.push(request),
2603 Err(_) => break,
2604 }
2605 }
2606 let completions = batch
2607 .iter()
2608 .filter_map(|request| request.completion.map(|effect| (effect, request.relay)))
2609 .collect::<Vec<_>>();
2610 let result = async {
2611 for request in batch {
2612 writer.feed(request.envelope).await?;
2613 }
2614 writer.flush().await
2615 }
2616 .await;
2617 let send_result = result.as_ref().map_or_else(
2618 |error| SendResult::WriteFailed(error.to_string()),
2619 |_| SendResult::Written,
2620 );
2621 let completions = completions
2622 .into_iter()
2623 .map(|(effect, relay)| send_completion(effect, relay, send_result.clone()))
2624 .collect::<Vec<_>>();
2625 if !completions.is_empty()
2626 && handle
2627 .inputs
2628 .send(ActorInput::CoreBatch(completions))
2629 .await
2630 .is_err()
2631 {
2632 return;
2633 }
2634 if result.is_err() {
2635 let _ = handle
2636 .submit(CoreInput::SessionClosed {
2637 session: session.clone(),
2638 })
2639 .await;
2640 cancellation.cancel();
2641 let _ = writer.close().await;
2642 return;
2643 }
2644 }
2645 });
2646}
2647
2648#[allow(clippy::too_many_arguments)]
2649fn reserve_write(
2650 sender: mpsc::Sender<WriteRequest>,
2651 byte_budget: Arc<Semaphore>,
2652 envelope: Envelope,
2653 completion: Option<EffectId>,
2654 relay: bool,
2655 handle: ProtocolCoreHandle,
2656 cancellation: CancellationToken,
2657 reservation_pending: Option<Arc<AtomicBool>>,
2658 timeout: std::time::Duration,
2659) {
2660 n0_future::task::spawn(async move {
2661 let effect = completion;
2662 let acquire = async {
2663 let budget = byte_budget
2664 .acquire_many_owned(WriteRequest::weight(&envelope))
2665 .await
2666 .map_err(|_| ())?;
2667 let slot = sender.reserve_owned().await.map_err(|_| ())?;
2668 Ok::<_, ()>((budget, slot))
2669 };
2670 let reserved = tokio::select! {
2671 biased;
2672 () = cancellation.cancelled() => {
2673 if let Some(pending) = &reservation_pending {
2674 pending.store(false, Ordering::Release);
2675 }
2676 if let Some(effect) = effect {
2677 let _ = handle.submit(send_completion(effect, relay, SendResult::Cancelled)).await;
2678 }
2679 return;
2680 }
2681 reserved = n0_future::time::timeout(timeout, acquire) => reserved,
2682 };
2683 if let Some(pending) = &reservation_pending {
2684 pending.store(false, Ordering::Release);
2685 }
2686 match reserved {
2687 Ok(Ok((budget, slot))) => {
2688 if let Some(effect) = effect {
2689 if handle
2690 .submit(send_completion(effect, relay, SendResult::Reserved))
2691 .await
2692 .is_err()
2693 {
2694 return;
2695 }
2696 }
2697 slot.send(WriteRequest {
2698 envelope,
2699 completion,
2700 relay,
2701 _budget: budget,
2702 });
2703 }
2704 Ok(Err(())) => {
2705 if let Some(effect) = effect {
2706 let _ = handle
2707 .submit(send_completion(effect, relay, SendResult::Closed))
2708 .await;
2709 }
2710 }
2711 Err(_) => {
2712 if let Some(effect) = effect {
2713 let _ = handle
2714 .submit(send_completion(
2715 effect,
2716 relay,
2717 SendResult::ReservationTimedOut,
2718 ))
2719 .await;
2720 }
2721 }
2722 }
2723 });
2724}
2725
2726fn send_completion(effect: EffectId, relay: bool, result: SendResult) -> CoreInput {
2727 if relay {
2728 CoreInput::RelayForwardCompleted { effect, result }
2729 } else {
2730 CoreInput::SendCompleted { effect, result }
2731 }
2732}
2733
2734fn spawn_session_deadline(
2735 session: SessionId,
2736 mut deadlines: tokio::sync::watch::Receiver<Option<Instant>>,
2737 handle: ProtocolCoreHandle,
2738 cancellation: CancellationToken,
2739) {
2740 n0_future::task::spawn(async move {
2741 loop {
2742 let deadline = *deadlines.borrow_and_update();
2743 match deadline {
2744 Some(deadline) if clock_now() < deadline => {
2745 tokio::select! {
2746 biased;
2747 () = cancellation.cancelled() => return,
2748 () = sleep_to(deadline) => {}
2749 }
2750 }
2751 Some(_) => {
2752 if handle
2753 .submit(CoreInput::SessionTimeout {
2754 session: session.clone(),
2755 })
2756 .await
2757 .is_err()
2758 {
2759 return;
2760 }
2761 tokio::select! {
2762 biased;
2763 () = cancellation.cancelled() => return,
2764 changed = deadlines.changed() => {
2765 if changed.is_err() {
2766 return;
2767 }
2768 }
2769 }
2770 }
2771 None => {
2772 tokio::select! {
2773 biased;
2774 () = cancellation.cancelled() => return,
2775 changed = deadlines.changed() => {
2776 if changed.is_err() {
2777 return;
2778 }
2779 }
2780 }
2781 }
2782 }
2783 }
2784 });
2785}
2786
2787fn spawn_client_timeout(
2788 session: SessionId,
2789 operation: ClientOperationId,
2790 deadline: Instant,
2791 handle: ProtocolCoreHandle,
2792 cancellation: Option<CancellationToken>,
2793) {
2794 let Some(cancellation) = cancellation else {
2795 return;
2796 };
2797 n0_future::task::spawn(async move {
2798 tokio::select! {
2799 biased;
2800 () = cancellation.cancelled() => {}
2801 () = sleep_to(deadline) => {
2802 let _ = handle.submit(CoreInput::ClientOperationTimeout { session, operation }).await;
2803 }
2804 }
2805 });
2806}
2807
2808fn spawn_establishment_timeout(session: SessionId, handle: ProtocolCoreHandle) {
2809 n0_future::task::spawn(async move {
2810 n0_future::time::sleep(ESTABLISHMENT_TIMEOUT).await;
2811 let _ = handle
2812 .submit(CoreInput::EstablishmentTimeout { session })
2813 .await;
2814 });
2815}
2816
2817#[cfg(test)]
2818mod tests {
2819 use super::*;
2820 use bytes::Bytes;
2821 use std::sync::atomic::{AtomicBool, Ordering};
2822 use unb_core::{DiscoverPlan, Kind, RetirementReason, PROTOCOL_VERSION};
2823
2824 struct RecordingExecutor {
2825 effects: mpsc::Sender<CoreEffect>,
2826 }
2827
2828 impl EffectExecutor for RecordingExecutor {
2829 fn execute(&self, effect: CoreEffect, _handle: ProtocolCoreHandle) -> EffectFuture {
2830 let effects = self.effects.clone();
2831 Box::pin(async move {
2832 let _ = effects.send(effect).await;
2833 None
2834 })
2835 }
2836 }
2837
2838 struct PublicationExecutor {
2839 published: Arc<AtomicBool>,
2840 effects: mpsc::Sender<bool>,
2841 }
2842
2843 struct ClosingHandler {
2844 delivered: mpsc::Sender<Envelope>,
2845 closed: mpsc::Sender<ClientOperationId>,
2846 }
2847
2848 impl SessionHandler for ClosingHandler {
2849 async fn deliver(&mut self, envelope: Envelope) {
2850 let _ = self.delivered.send(envelope).await;
2851 }
2852
2853 async fn stream_closed(&mut self, operation: ClientOperationId) {
2854 let _ = self.closed.send(operation).await;
2855 }
2856 }
2857
2858 impl EffectExecutor for PublicationExecutor {
2859 fn execute(&self, effect: CoreEffect, _handle: ProtocolCoreHandle) -> EffectFuture {
2860 let published = self.published.clone();
2861 let effects = self.effects.clone();
2862 Box::pin(async move {
2863 if matches!(effect, CoreEffect::SessionRetired { .. }) {
2864 let _ = effects.send(published.load(Ordering::SeqCst)).await;
2865 }
2866 None
2867 })
2868 }
2869 }
2870
2871 fn local_pipe() -> (Pipe, mpsc::Sender<Envelope>, mpsc::Receiver<Envelope>) {
2872 let (incoming, rx) = mpsc::channel(16);
2873 let (tx, outgoing) = mpsc::channel(16);
2874 (
2875 Pipe::Local {
2876 rx,
2877 tx,
2878 initiator: true,
2879 },
2880 incoming,
2881 outgoing,
2882 )
2883 }
2884
2885 fn frame(kind: Kind, corr: Option<String>, payload: serde_json::Value) -> Envelope {
2886 Envelope {
2887 v: PROTOCOL_VERSION,
2888 id: format!("{kind:?}"),
2889 target: String::new(),
2890 subject: String::new(),
2891 kind,
2892 corr,
2893 seq: None,
2894 hops: None,
2895 body_token: None,
2896 payload: Envelope::encode_payload(&payload),
2897 path: Vec::new(),
2898 headers: Default::default(),
2899 }
2900 }
2901
2902 async fn establish(incoming: &mpsc::Sender<Envelope>, outgoing: &mut mpsc::Receiver<Envelope>) {
2903 assert_eq!(outgoing.recv().await.unwrap().kind, Kind::Hello);
2904 incoming
2905 .send(frame(
2906 Kind::Welcome,
2907 None,
2908 serde_json::json!({ "version": 1 }),
2909 ))
2910 .await
2911 .unwrap();
2912 }
2913
2914 fn request(target_path: &str, body: &'static [u8]) -> http::Request<Bytes> {
2915 http::Request::post(target_path)
2916 .body(Bytes::from_static(body))
2917 .unwrap()
2918 }
2919
2920 async fn attach_client(handle: &ProtocolCoreHandle, name: &str, pipe: Pipe) -> Arc<Wire> {
2921 attach_client_with_ceiling(handle, name, pipe, unb_transport::DEFAULT_MAX_FRAME_SIZE).await
2922 }
2923
2924 async fn attach_client_with_ceiling(
2925 handle: &ProtocolCoreHandle,
2926 name: &str,
2927 pipe: Pipe,
2928 ws_collect_ceiling: usize,
2929 ) -> Arc<Wire> {
2930 let session_cancellation = handle.cancellation.child_token();
2931 let (wire, directives, outcome, routes) =
2932 Wire::standalone_channel(session_cancellation.clone());
2933 let (reply, response) = oneshot::channel();
2934 handle
2935 .inputs
2936 .send(ActorInput::Attach {
2937 session: SessionId::from(name),
2938 transport: pipe,
2939 expected_peer: None,
2940 directives,
2941 handler: handler_channel(wire.observation_sender()),
2942 client: wire.client_session(),
2943 cancellation: session_cancellation,
2944 establish_peer: false,
2945 outcome,
2946 routes,
2947 ws_collect_ceiling,
2948 reply,
2949 })
2950 .await
2951 .unwrap();
2952 response.await.unwrap().unwrap();
2953 wire
2954 }
2955
2956 fn spawn_core() -> ProtocolCoreHandle {
2957 ProtocolCoreHandle::spawn(
2958 ProtocolCore::new("node"),
2959 Arc::new(RecordingExecutor {
2960 effects: mpsc::channel(1).0,
2961 }),
2962 CancellationToken::new(),
2963 &tokio::runtime::Handle::current(),
2964 )
2965 }
2966
2967 fn stalled_pipe() -> (Pipe, mpsc::Sender<Envelope>, mpsc::Receiver<Envelope>) {
2968 let (incoming, rx) = mpsc::channel(16);
2969 let (tx, outgoing) = mpsc::channel(1);
2970 (
2971 Pipe::Local {
2972 rx,
2973 tx,
2974 initiator: true,
2975 },
2976 incoming,
2977 outgoing,
2978 )
2979 }
2980
2981 fn chunk_stream(chunks: Vec<Result<Bytes, unb_core::CoreError>>) -> BodyStream {
2982 Box::pin(futures_util::stream::iter(chunks))
2983 }
2984
2985 #[tokio::test(start_paused = true)]
2986 async fn a_metered_body_stall_past_the_deadline_is_busy_shaped() {
2987 let budget = Arc::new(Semaphore::new(4));
2988 let hold = budget.clone().try_acquire_many_owned(4).unwrap();
2989 let mut body = MeteredBody::new(
2990 chunk_stream(vec![Ok(Bytes::from_static(b"x"))]),
2991 budget,
2992 CancellationToken::new(),
2993 );
2994 let outcome = body.next_chunk().await;
2995 assert!(
2996 matches!(
2997 outcome,
2998 Err(WsError::Core(unb_core::CoreError::BodyStalled))
2999 ),
3000 "budget starvation past the deadline must be BUSY-shaped: {outcome:?}"
3001 );
3002 drop(hold);
3003 }
3004
3005 #[tokio::test]
3006 async fn a_window_saturated_body_leaves_a_sibling_pump_undisturbed() {
3007 let budget = Arc::new(Semaphore::new(OUTBOUND_BYTE_BUDGET));
3008 let mut saturated = MeteredBody::new(
3009 chunk_stream(vec![
3010 Ok(Bytes::from(vec![0u8; BODY_WINDOW_BYTES])),
3011 Ok(Bytes::from_static(b"tail")),
3012 ]),
3013 budget.clone(),
3014 CancellationToken::new(),
3015 );
3016 let held = saturated.next_chunk().await.unwrap().unwrap();
3017 assert!(
3018 tokio::time::timeout(std::time::Duration::from_millis(50), saturated.next_chunk())
3019 .await
3020 .is_err(),
3021 "a full per-body window must block the next chunk"
3022 );
3023 let mut sibling = MeteredBody::new(
3024 chunk_stream(vec![Ok(Bytes::from_static(b"y"))]),
3025 budget,
3026 CancellationToken::new(),
3027 );
3028 let chunk = tokio::time::timeout(std::time::Duration::from_secs(1), sibling.next_chunk())
3029 .await
3030 .expect("a sibling body must progress while another window is saturated")
3031 .unwrap()
3032 .unwrap();
3033 assert_eq!(chunk.bytes, Bytes::from_static(b"y"));
3034 drop(held);
3035 let tail = tokio::time::timeout(std::time::Duration::from_secs(1), saturated.next_chunk())
3036 .await
3037 .expect("draining the window must unblock the body")
3038 .unwrap()
3039 .unwrap();
3040 assert_eq!(tail.bytes, Bytes::from_static(b"tail"));
3041 }
3042
3043 #[test]
3044 fn body_tokens_are_unique_and_carry_entropy() {
3045 let first = mint_body_token();
3046 let second = mint_body_token();
3047 assert_ne!(first, second);
3048 assert!(
3049 first.len() >= 33,
3050 "a guessable serial token invites body substitution: {first}"
3051 );
3052 }
3053
3054 #[tokio::test]
3055 async fn an_oversized_source_chunk_is_rejected_before_retention() {
3056 let budget = Arc::new(Semaphore::new(OUTBOUND_BYTE_BUDGET));
3057 let mut body = MeteredBody::new(
3058 chunk_stream(vec![Ok(Bytes::from(vec![
3059 0u8;
3060 unb_transport::DEFAULT_MAX_FRAME_SIZE
3061 + 1
3062 ]))]),
3063 budget.clone(),
3064 CancellationToken::new(),
3065 );
3066 let outcome = body.next_chunk().await;
3067 assert!(
3068 matches!(
3069 outcome,
3070 Err(WsError::Core(unb_core::CoreError::BodyTooLarge(_)))
3071 ),
3072 "a chunk beyond the frame cap must be refused before retention: {outcome:?}"
3073 );
3074 assert_eq!(
3075 budget.available_permits(),
3076 OUTBOUND_BYTE_BUDGET,
3077 "a refused chunk must not consume budget"
3078 );
3079 }
3080
3081 #[tokio::test]
3082 async fn concurrent_bodies_stay_within_the_session_budget() {
3083 let budget = Arc::new(Semaphore::new(100));
3084 let mut first = MeteredBody::new(
3085 chunk_stream(vec![Ok(Bytes::from(vec![0u8; 60]))]),
3086 budget.clone(),
3087 CancellationToken::new(),
3088 );
3089 let mut second = MeteredBody::new(
3090 chunk_stream(vec![Ok(Bytes::from(vec![0u8; 60]))]),
3091 budget.clone(),
3092 CancellationToken::new(),
3093 );
3094 let held = first.next_chunk().await.unwrap().unwrap();
3095 assert_eq!(budget.available_permits(), 40);
3096 assert!(
3097 tokio::time::timeout(std::time::Duration::from_millis(50), second.next_chunk())
3098 .await
3099 .is_err(),
3100 "combined queued bytes must not exceed the session budget"
3101 );
3102 drop(held);
3103 let chunk = tokio::time::timeout(std::time::Duration::from_secs(1), second.next_chunk())
3104 .await
3105 .expect("released budget must admit the waiting body")
3106 .unwrap()
3107 .unwrap();
3108 assert_eq!(chunk.bytes.len(), 60);
3109 }
3110
3111 #[tokio::test(start_paused = true)]
3112 async fn a_steady_body_streams_past_the_terminal_deadline_without_timing_out() {
3113 let budget = Arc::new(Semaphore::new(OUTBOUND_BYTE_BUDGET));
3114 let chunks: Vec<Result<Bytes, unb_core::CoreError>> =
3115 (0..8).map(|_| Ok(Bytes::from_static(b"chunk"))).collect();
3116 let mut body = MeteredBody::new(chunk_stream(chunks), budget, CancellationToken::new());
3117 for _ in 0..8 {
3118 let chunk = body
3119 .next_chunk()
3120 .await
3121 .expect("a steadily progressing body never times out")
3122 .expect("chunks remain");
3123 drop(chunk);
3124 tokio::time::advance(std::time::Duration::from_secs(6)).await;
3125 }
3126 assert!(body.next_chunk().await.unwrap().is_none());
3127 }
3128
3129 #[tokio::test]
3130 async fn pipe_recv_yields_whole_message_envelopes_with_no_body_stream() {
3131 let mut envelope = frame(
3132 Kind::Request,
3133 Some("c1".into()),
3134 serde_json::json!({"n": 1}),
3135 );
3136 envelope.target = "server".into();
3137 envelope.subject = "echo".into();
3138 let encoded = envelope.encode();
3139 let pipe = Pipe::Piped {
3140 pipe: Box::pin(ScriptedPipe {
3141 frames: std::collections::VecDeque::from([Ok(encoded.clone())]),
3142 }),
3143 initiator: true,
3144 };
3145 let (mut reader, _writer) = pipe.split();
3146 let (received, body) = reader.recv().await.unwrap().unwrap();
3147 assert!(body.is_none(), "whole-message carriers collect the body");
3148 assert_eq!(received, Envelope::decode(encoded).unwrap());
3149
3150 let (incoming, rx) = mpsc::channel(4);
3151 let (tx, _outgoing) = mpsc::channel(4);
3152 let pipe = Pipe::Local {
3153 rx,
3154 tx,
3155 initiator: true,
3156 };
3157 let (mut reader, _writer) = pipe.split();
3158 incoming.send(envelope.clone()).await.unwrap();
3159 let (received, body) = reader.recv().await.unwrap().unwrap();
3160 assert!(body.is_none());
3161 assert_eq!(received, envelope);
3162 }
3163
3164 #[test]
3165 fn write_weight_counts_head_and_payload_and_clamps_to_the_whole_budget() {
3166 let mut envelope = frame(Kind::Request, None, serde_json::Value::Null);
3167 envelope.id = String::new();
3168 envelope.target = "server".into();
3169 envelope.subject = "echo".into();
3170 envelope.payload = Bytes::new();
3171 assert_eq!(WriteRequest::weight(&envelope), 10);
3172 envelope
3173 .headers
3174 .insert("origin".into(), serde_json::Value::String("x".repeat(64)));
3175 assert_eq!(WriteRequest::weight(&envelope), 80);
3176 envelope.payload = Bytes::from(vec![0u8; OUTBOUND_BYTE_BUDGET + 1]);
3177 assert_eq!(WriteRequest::weight(&envelope), OUTBOUND_BYTE_BUDGET as u32);
3178 }
3179
3180 #[tokio::test]
3181 async fn a_byte_saturated_outbound_ends_with_busy_and_recovers_on_drain() {
3182 let handle = spawn_core();
3183 let (pipe, incoming, mut outgoing) = stalled_pipe();
3184 let wire = attach_client(&handle, "stalled", pipe).await;
3185 establish(&incoming, &mut outgoing).await;
3186 let client = wire.client_session();
3187 let payload = Bytes::from(vec![0u8; 16 * 1024 * 1024]);
3188
3189 let mut flood = Vec::new();
3190 for _ in 0..6 {
3191 flood.push(
3192 client
3193 .start(
3194 "/server/flood",
3195 Kind::Request,
3196 payload.clone(),
3197 None,
3198 Default::default(),
3199 )
3200 .await
3201 .unwrap(),
3202 );
3203 }
3204 let mut rejected = client
3205 .start(
3206 "/server/flood",
3207 Kind::Request,
3208 payload.clone(),
3209 None,
3210 Default::default(),
3211 )
3212 .await
3213 .unwrap();
3214
3215 let refused = rejected.operation().as_str().to_owned();
3216 let refusal = tokio::time::timeout(std::time::Duration::from_secs(2), rejected.next())
3217 .await
3218 .expect("an over-budget write must be refused promptly, not silently queued");
3219 assert!(
3220 refusal.is_err(),
3221 "queued outbound bytes past the budget must refuse the write: {refusal:?}"
3222 );
3223
3224 let mut saw_busy = false;
3225 loop {
3226 match tokio::time::timeout(std::time::Duration::from_millis(300), outgoing.recv()).await
3227 {
3228 Ok(Some(envelope)) => {
3229 if envelope.kind == Kind::Error
3230 && envelope.corr.as_deref() == Some(refused.as_str())
3231 {
3232 assert_eq!(envelope.payload_json()["code"], "BUSY");
3233 saw_busy = true;
3234 }
3235 }
3236 Ok(None) => panic!("the stalled pipe closed while draining"),
3237 Err(_) => break,
3238 }
3239 }
3240 assert!(saw_busy, "the refused write must surface as a BUSY error");
3241
3242 let probe = client
3243 .start(
3244 "/server/probe",
3245 Kind::Request,
3246 payload.clone(),
3247 None,
3248 Default::default(),
3249 )
3250 .await
3251 .unwrap();
3252 let opening = tokio::time::timeout(std::time::Duration::from_secs(5), async {
3253 loop {
3254 let envelope = outgoing.recv().await.unwrap();
3255 if envelope.subject == "probe" {
3256 break envelope;
3257 }
3258 }
3259 })
3260 .await
3261 .expect("draining the writer must release budget for later writes");
3262 incoming
3263 .send(frame(
3264 Kind::Response,
3265 opening.corr,
3266 serde_json::json!({ "ok": true }),
3267 ))
3268 .await
3269 .unwrap();
3270 let mut probe = probe;
3271 let response = tokio::time::timeout(std::time::Duration::from_secs(5), probe.next())
3272 .await
3273 .expect("a post-drain write must complete end to end")
3274 .unwrap()
3275 .unwrap();
3276 assert_eq!(response.payload_json(), serde_json::json!({ "ok": true }));
3277 handle.shutdown();
3278 }
3279
3280 #[tokio::test]
3281 async fn a_byte_saturated_session_leaves_a_sibling_session_undisturbed() {
3282 let handle = spawn_core();
3283 let (saturated_pipe, stalled_incoming, mut stalled_outgoing) = stalled_pipe();
3284 let stalled = attach_client(&handle, "stalled", saturated_pipe).await;
3285 establish(&stalled_incoming, &mut stalled_outgoing).await;
3286 let (live_pipe, live_incoming, mut live_outgoing) = local_pipe();
3287 let live = attach_client(&handle, "live", live_pipe).await;
3288 establish(&live_incoming, &mut live_outgoing).await;
3289
3290 let payload = Bytes::from(vec![0u8; 16 * 1024 * 1024]);
3291 let mut flood = Vec::new();
3292 for _ in 0..6 {
3293 flood.push(
3294 stalled
3295 .client_session()
3296 .start(
3297 "/server/flood",
3298 Kind::Request,
3299 payload.clone(),
3300 None,
3301 Default::default(),
3302 )
3303 .await
3304 .unwrap(),
3305 );
3306 }
3307
3308 let mut sibling = live
3309 .client_session()
3310 .start(
3311 "/server/sibling",
3312 Kind::Request,
3313 Bytes::new(),
3314 None,
3315 Default::default(),
3316 )
3317 .await
3318 .unwrap();
3319 let opening = tokio::time::timeout(std::time::Duration::from_secs(1), live_outgoing.recv())
3320 .await
3321 .expect("a sibling session must keep writing while another is byte-saturated")
3322 .unwrap();
3323 assert_eq!(opening.subject, "sibling");
3324 live_incoming
3325 .send(frame(
3326 Kind::Response,
3327 opening.corr,
3328 serde_json::json!({ "ok": true }),
3329 ))
3330 .await
3331 .unwrap();
3332 let response = tokio::time::timeout(std::time::Duration::from_secs(2), sibling.next())
3333 .await
3334 .expect("a sibling response must be delivered while another session is saturated")
3335 .unwrap()
3336 .unwrap();
3337 assert_eq!(response.payload_json(), serde_json::json!({ "ok": true }));
3338 handle.shutdown();
3339 }
3340
3341 #[tokio::test]
3342 async fn terminal_client_delivery_closes_handler_stream_once() {
3343 let cancellation = CancellationToken::new();
3344 let handle = ProtocolCoreHandle::spawn(
3345 ProtocolCore::new("node"),
3346 Arc::new(RecordingExecutor {
3347 effects: mpsc::channel(1).0,
3348 }),
3349 cancellation,
3350 &tokio::runtime::Handle::current(),
3351 );
3352 let (pipe, incoming, mut outgoing) = local_pipe();
3353 let (delivered, _deliveries) = mpsc::channel(4);
3354 let (closed, mut closures) = mpsc::channel(4);
3355 let session_cancellation = handle.cancellation.child_token();
3356 let (wire, directives, outcome, routes) =
3357 Wire::standalone_channel(session_cancellation.clone());
3358 let (reply, response) = oneshot::channel();
3359 handle
3360 .inputs
3361 .send(ActorInput::Attach {
3362 session: SessionId::from("session"),
3363 transport: pipe,
3364 expected_peer: None,
3365 directives,
3366 handler: handler_channel(ClosingHandler { delivered, closed }),
3367 client: wire.client_session(),
3368 cancellation: session_cancellation,
3369 establish_peer: false,
3370 outcome,
3371 routes,
3372 ws_collect_ceiling: unb_transport::DEFAULT_MAX_FRAME_SIZE,
3373 reply,
3374 })
3375 .await
3376 .unwrap();
3377 response.await.unwrap().unwrap();
3378 establish(&incoming, &mut outgoing).await;
3379 let mut stream = wire
3380 .client_session()
3381 .start(
3382 "/server/echo",
3383 Kind::Request,
3384 Bytes::new(),
3385 None,
3386 Default::default(),
3387 )
3388 .await
3389 .unwrap();
3390 let opening = outgoing.recv().await.unwrap();
3391
3392 incoming
3393 .send(frame(
3394 Kind::Response,
3395 opening.corr,
3396 serde_json::json!({ "ok": true }),
3397 ))
3398 .await
3399 .unwrap();
3400
3401 assert!(stream.next().await.unwrap().is_some());
3402 assert_eq!(closures.recv().await, Some(stream.operation().clone()));
3403 assert!(
3404 tokio::time::timeout(std::time::Duration::from_millis(25), closures.recv())
3405 .await
3406 .is_err()
3407 );
3408 handle.shutdown();
3409 }
3410
3411 #[tokio::test]
3412 async fn generic_terminal_delivery_closes_handler_stream_once() {
3413 let cancellation = CancellationToken::new();
3414 let handle = ProtocolCoreHandle::spawn(
3415 ProtocolCore::new("node"),
3416 Arc::new(RecordingExecutor {
3417 effects: mpsc::channel(1).0,
3418 }),
3419 cancellation,
3420 &tokio::runtime::Handle::current(),
3421 );
3422 let (pipe, incoming, mut outgoing) = local_pipe();
3423 let (delivered, mut deliveries) = mpsc::channel(4);
3424 let (closed, mut closures) = mpsc::channel(4);
3425 let session_cancellation = handle.cancellation.child_token();
3426 let (wire, directives, outcome, routes) =
3427 Wire::standalone_channel(session_cancellation.clone());
3428 let (reply, response) = oneshot::channel();
3429 handle
3430 .inputs
3431 .send(ActorInput::Attach {
3432 session: SessionId::from("session"),
3433 transport: pipe,
3434 expected_peer: None,
3435 directives,
3436 handler: handler_channel(ClosingHandler { delivered, closed }),
3437 client: wire.client_session(),
3438 cancellation: session_cancellation,
3439 establish_peer: false,
3440 outcome,
3441 routes,
3442 ws_collect_ceiling: unb_transport::DEFAULT_MAX_FRAME_SIZE,
3443 reply,
3444 })
3445 .await
3446 .unwrap();
3447 response.await.unwrap().unwrap();
3448 establish(&incoming, &mut outgoing).await;
3449 let corr = wire
3450 .open_stream("/server/echo", Kind::Request, serde_json::Value::Null)
3451 .await
3452 .unwrap();
3453 let opening = outgoing.recv().await.unwrap();
3454 assert_eq!(opening.corr.as_deref(), Some(corr.as_str()));
3455
3456 incoming
3457 .send(frame(
3458 Kind::Response,
3459 Some(corr.clone()),
3460 serde_json::json!({ "ok": true }),
3461 ))
3462 .await
3463 .unwrap();
3464
3465 assert_eq!(
3466 deliveries.recv().await.unwrap().corr.as_deref(),
3467 Some(corr.as_str())
3468 );
3469 assert_eq!(closures.recv().await, Some(ClientOperationId::from(corr)));
3470 assert!(
3471 tokio::time::timeout(std::time::Duration::from_millis(25), closures.recv())
3472 .await
3473 .is_err()
3474 );
3475 handle.shutdown();
3476 }
3477
3478 #[tokio::test]
3479 async fn immediate_loopback_response_finds_registered_client_operation() {
3480 let (pipe, incoming, mut outgoing) = local_pipe();
3481 let wire = Wire::open(pipe);
3482 establish(&incoming, &mut outgoing).await;
3483 let loopback = tokio::spawn(async move {
3484 let opening = outgoing.recv().await.unwrap();
3485 let corr = opening.corr.unwrap();
3486 incoming
3487 .send(frame(
3488 Kind::Response,
3489 Some(corr),
3490 serde_json::json!({ "ok": true }),
3491 ))
3492 .await
3493 .unwrap();
3494 });
3495
3496 let mut operation = wire
3497 .client_session()
3498 .start(
3499 "/server/echo",
3500 Kind::Request,
3501 Bytes::new(),
3502 None,
3503 Default::default(),
3504 )
3505 .await
3506 .unwrap();
3507
3508 loopback.await.unwrap();
3509 assert_eq!(
3510 operation
3511 .try_next()
3512 .unwrap()
3513 .unwrap()
3514 .unwrap()
3515 .payload_json(),
3516 serde_json::json!({ "ok": true })
3517 );
3518 assert_eq!(operation.try_next().unwrap(), Some(None));
3519 }
3520
3521 #[tokio::test]
3522 async fn opening_send_failure_cleans_up_client_operation() {
3523 let (pipe, incoming, mut outgoing) = local_pipe();
3524 let wire = Wire::open(pipe);
3525 establish(&incoming, &mut outgoing).await;
3526 drop(outgoing);
3527
3528 let mut operation = wire
3529 .client_session()
3530 .start(
3531 "/server/echo",
3532 Kind::Request,
3533 Bytes::new(),
3534 None,
3535 Default::default(),
3536 )
3537 .await
3538 .unwrap();
3539
3540 assert!(matches!(
3541 operation.next().await,
3542 Err(crate::ClientError::SessionClosed(_))
3543 ));
3544 }
3545
3546 #[tokio::test]
3547 async fn fetch_projects_response_and_ignores_late_terminal_frame() {
3548 let (pipe, incoming, mut outgoing) = local_pipe();
3549 let wire = Wire::open(pipe);
3550 establish(&incoming, &mut outgoing).await;
3551 let responder = tokio::spawn(async move {
3552 let opening = outgoing.recv().await.unwrap();
3553 let corr = opening.corr.unwrap();
3554 let mut response = frame(
3555 Kind::Response,
3556 Some(corr.clone()),
3557 serde_json::json!({ "ok": true }),
3558 );
3559 response.headers.insert("x-source".into(), "owner".into());
3560 incoming.send(response.clone()).await.unwrap();
3561 incoming.send(response).await.unwrap();
3562 });
3563
3564 let response = wire
3565 .client_session()
3566 .fetch(
3567 request("/server/echo", b"request"),
3568 std::time::Duration::from_secs(1),
3569 )
3570 .await
3571 .unwrap();
3572
3573 assert_eq!(response.status(), http::StatusCode::OK);
3574 assert_eq!(response.headers()["x-source"], "owner");
3575 assert_eq!(response.body(), &Bytes::from_static(br#"{"ok":true}"#));
3576 responder.await.unwrap();
3577 }
3578
3579 #[tokio::test]
3580 async fn subscribe_and_discovery_are_isolated() {
3581 let (pipe, incoming, mut outgoing) = local_pipe();
3582 let wire = Wire::open(pipe);
3583 establish(&incoming, &mut outgoing).await;
3584 let client = wire.client_session();
3585 let mut subscription = client
3586 .subscribe(request("/server/events", b""), None)
3587 .await
3588 .unwrap();
3589 let mut discovery = client
3590 .discover("/server", DiscoverPlan::decode(&Bytes::new()).unwrap())
3591 .await
3592 .unwrap();
3593 let first = outgoing.recv().await.unwrap();
3594 let second = outgoing.recv().await.unwrap();
3595 let (subscribe_corr, discover_corr) = if first.kind == Kind::Subscribe {
3596 (first.corr.unwrap(), second.corr.unwrap())
3597 } else {
3598 (second.corr.unwrap(), first.corr.unwrap())
3599 };
3600
3601 incoming
3602 .send(frame(
3603 Kind::Event,
3604 Some(discover_corr.clone()),
3605 serde_json::json!({ "discover": 1 }),
3606 ))
3607 .await
3608 .unwrap();
3609 incoming
3610 .send(frame(
3611 Kind::Event,
3612 Some(subscribe_corr.clone()),
3613 serde_json::json!({ "event": 1 }),
3614 ))
3615 .await
3616 .unwrap();
3617 incoming
3618 .send(frame(
3619 Kind::Response,
3620 Some(discover_corr),
3621 serde_json::json!({ "done": true }),
3622 ))
3623 .await
3624 .unwrap();
3625
3626 assert_eq!(
3627 subscription.next().await.unwrap().unwrap().payload_json()["event"],
3628 1
3629 );
3630 assert_eq!(
3631 discovery.next().await.unwrap().unwrap().payload_json()["discover"],
3632 1
3633 );
3634 assert_eq!(
3635 discovery.next().await.unwrap().unwrap().payload_json()["done"],
3636 true
3637 );
3638 }
3639
3640 #[tokio::test]
3641 async fn dropping_a_saturated_operation_frees_its_session_for_idle_delivery() {
3642 let (pipe, incoming, mut outgoing) = local_pipe();
3643 let wire = Wire::open(pipe);
3644 establish(&incoming, &mut outgoing).await;
3645 let client = wire.client_session();
3646 let busy = client
3647 .subscribe(request("/server/busy", b""), None)
3648 .await
3649 .unwrap();
3650 let mut idle = client
3651 .subscribe(request("/server/idle", b""), None)
3652 .await
3653 .unwrap();
3654 let busy_open = outgoing.recv().await.unwrap();
3655 let idle_open = outgoing.recv().await.unwrap();
3656
3657 let burst = tokio::spawn(async move {
3658 for sequence in 0..257 {
3659 incoming
3660 .send(frame(
3661 Kind::Event,
3662 busy_open.corr.clone(),
3663 serde_json::json!({ "sequence": sequence }),
3664 ))
3665 .await
3666 .unwrap();
3667 }
3668 incoming
3669 .send(frame(
3670 Kind::Response,
3671 idle_open.corr,
3672 serde_json::json!({ "done": true }),
3673 ))
3674 .await
3675 .unwrap();
3676 incoming
3677 });
3678 tokio::time::sleep(std::time::Duration::from_millis(25)).await;
3679
3680 drop(busy);
3681
3682 let _incoming = tokio::time::timeout(std::time::Duration::from_secs(5), burst)
3683 .await
3684 .expect("dropping the saturated stream must unblock its session")
3685 .unwrap();
3686 assert_eq!(
3687 idle.next().await.unwrap().unwrap().payload_json()["done"],
3688 true
3689 );
3690 }
3691
3692 #[tokio::test]
3693 async fn out_of_order_unary_responses_reach_their_operations() {
3694 let (pipe, incoming, mut outgoing) = local_pipe();
3695 let wire = Wire::open(pipe);
3696 establish(&incoming, &mut outgoing).await;
3697 let client = wire.client_session();
3698 let mut first = client
3699 .start(
3700 "/server/first",
3701 Kind::Request,
3702 Bytes::new(),
3703 None,
3704 Default::default(),
3705 )
3706 .await
3707 .unwrap();
3708 let mut second = client
3709 .start(
3710 "/server/second",
3711 Kind::Request,
3712 Bytes::new(),
3713 None,
3714 Default::default(),
3715 )
3716 .await
3717 .unwrap();
3718 let first_open = outgoing.recv().await.unwrap();
3719 let second_open = outgoing.recv().await.unwrap();
3720
3721 incoming
3722 .send(frame(
3723 Kind::Response,
3724 second_open.corr,
3725 serde_json::json!({ "response": "second" }),
3726 ))
3727 .await
3728 .unwrap();
3729 incoming
3730 .send(frame(
3731 Kind::Response,
3732 first_open.corr,
3733 serde_json::json!({ "response": "first" }),
3734 ))
3735 .await
3736 .unwrap();
3737
3738 assert_eq!(
3739 first.next().await.unwrap().unwrap().payload_json()["response"],
3740 "first"
3741 );
3742 assert_eq!(
3743 second.next().await.unwrap().unwrap().payload_json()["response"],
3744 "second"
3745 );
3746 }
3747
3748 #[tokio::test]
3749 async fn mixed_unary_subscription_and_discovery_are_isolated() {
3750 let (pipe, incoming, mut outgoing) = local_pipe();
3751 let wire = Wire::open(pipe);
3752 establish(&incoming, &mut outgoing).await;
3753 let client = wire.client_session();
3754 let mut unary = client
3755 .start(
3756 "/server/unary",
3757 Kind::Request,
3758 Bytes::new(),
3759 None,
3760 Default::default(),
3761 )
3762 .await
3763 .unwrap();
3764 let mut subscription = client
3765 .subscribe(request("/server/events", b""), None)
3766 .await
3767 .unwrap();
3768 let mut discovery = client
3769 .discover("/server", DiscoverPlan::decode(&Bytes::new()).unwrap())
3770 .await
3771 .unwrap();
3772 let mut unary_corr = None;
3773 let mut subscription_corr = None;
3774 let mut discovery_corr = None;
3775 for _ in 0..3 {
3776 let opening = outgoing.recv().await.unwrap();
3777 match opening.kind {
3778 Kind::Request => unary_corr = opening.corr,
3779 Kind::Subscribe => subscription_corr = opening.corr,
3780 Kind::Discover => discovery_corr = opening.corr,
3781 kind => panic!("unexpected opening kind {kind:?}"),
3782 }
3783 }
3784
3785 for (kind, corr, value) in [
3786 (Kind::Event, discovery_corr, "discover"),
3787 (Kind::Response, unary_corr, "unary"),
3788 (Kind::Event, subscription_corr, "subscription"),
3789 ] {
3790 incoming
3791 .send(frame(kind, corr, serde_json::json!({ "owner": value })))
3792 .await
3793 .unwrap();
3794 }
3795
3796 assert_eq!(
3797 unary.next().await.unwrap().unwrap().payload_json()["owner"],
3798 "unary"
3799 );
3800 assert_eq!(
3801 subscription.next().await.unwrap().unwrap().payload_json()["owner"],
3802 "subscription"
3803 );
3804 assert_eq!(
3805 discovery.next().await.unwrap().unwrap().payload_json()["owner"],
3806 "discover"
3807 );
3808 }
3809
3810 #[tokio::test]
3811 async fn cancelling_one_operation_does_not_complete_another() {
3812 let (pipe, incoming, mut outgoing) = local_pipe();
3813 let wire = Wire::open(pipe);
3814 establish(&incoming, &mut outgoing).await;
3815 let client = wire.client_session();
3816 let cancelled = client
3817 .subscribe(request("/server/cancelled", b""), None)
3818 .await
3819 .unwrap();
3820 let mut live = client
3821 .subscribe(request("/server/live", b""), None)
3822 .await
3823 .unwrap();
3824 let cancelled_open = outgoing.recv().await.unwrap();
3825 let live_open = outgoing.recv().await.unwrap();
3826
3827 drop(cancelled);
3828 let cancel = outgoing.recv().await.unwrap();
3829 assert_eq!(cancel.kind, Kind::Cancel);
3830 assert_eq!(cancel.corr, cancelled_open.corr);
3831 incoming
3832 .send(frame(
3833 Kind::Event,
3834 live_open.corr,
3835 serde_json::json!({ "live": true }),
3836 ))
3837 .await
3838 .unwrap();
3839 assert_eq!(
3840 live.next().await.unwrap().unwrap().payload_json()["live"],
3841 true
3842 );
3843 }
3844
3845 #[tokio::test]
3846 async fn stream_drop_sends_exactly_one_cancel() {
3847 let (pipe, incoming, mut outgoing) = local_pipe();
3848 let wire = Wire::open(pipe);
3849 establish(&incoming, &mut outgoing).await;
3850 let stream = wire
3851 .client_session()
3852 .subscribe(request("/server/events", b""), None)
3853 .await
3854 .unwrap();
3855 let opening = outgoing.recv().await.unwrap();
3856 let corr = opening.corr.unwrap();
3857
3858 drop(stream);
3859
3860 let cancel = tokio::time::timeout(std::time::Duration::from_secs(1), outgoing.recv())
3861 .await
3862 .unwrap()
3863 .unwrap();
3864 assert_eq!(cancel.kind, Kind::Cancel);
3865 assert_eq!(cancel.corr.as_deref(), Some(corr.as_str()));
3866 assert!(
3867 tokio::time::timeout(std::time::Duration::from_millis(25), outgoing.recv())
3868 .await
3869 .is_err()
3870 );
3871 }
3872
3873 #[tokio::test]
3874 async fn operation_timeout_is_typed_and_cancels_once() {
3875 let (pipe, incoming, mut outgoing) = local_pipe();
3876 let wire = Wire::open(pipe);
3877 establish(&incoming, &mut outgoing).await;
3878 let mut stream = wire
3879 .client_session()
3880 .subscribe(
3881 request("/server/events", b""),
3882 Some(std::time::Duration::from_millis(10)),
3883 )
3884 .await
3885 .unwrap();
3886 let opening = outgoing.recv().await.unwrap();
3887
3888 assert!(matches!(
3889 stream.next().await,
3890 Err(crate::ClientError::Timeout(_))
3891 ));
3892 let cancel = outgoing.recv().await.unwrap();
3893 assert_eq!(cancel.kind, Kind::Cancel);
3894 assert_eq!(cancel.corr, opening.corr);
3895 drop(stream);
3896 assert!(
3897 tokio::time::timeout(std::time::Duration::from_millis(25), outgoing.recv())
3898 .await
3899 .is_err()
3900 );
3901 }
3902
3903 #[tokio::test]
3904 async fn session_close_completes_every_operation_once() {
3905 let (pipe, incoming, mut outgoing) = local_pipe();
3906 let wire = Wire::open(pipe);
3907 establish(&incoming, &mut outgoing).await;
3908 let client = wire.client_session();
3909 let mut first = client
3910 .subscribe(request("/server/first", b""), None)
3911 .await
3912 .unwrap();
3913 let mut second = client
3914 .subscribe(request("/server/second", b""), None)
3915 .await
3916 .unwrap();
3917 outgoing.recv().await.unwrap();
3918 outgoing.recv().await.unwrap();
3919
3920 drop(incoming);
3921
3922 assert!(matches!(
3923 first.next().await,
3924 Err(crate::ClientError::SessionClosed(_))
3925 ));
3926 assert!(matches!(
3927 second.next().await,
3928 Err(crate::ClientError::SessionClosed(_))
3929 ));
3930 assert_eq!(first.next().await.unwrap(), None);
3931 assert_eq!(second.next().await.unwrap(), None);
3932 }
3933
3934 #[tokio::test]
3935 async fn a_session_the_core_rejects_closes_its_transport() {
3936 let (pipe, incoming, mut outgoing) = local_pipe();
3937 let _wire = Wire::open(pipe);
3938 assert_eq!(outgoing.recv().await.unwrap().kind, Kind::Hello);
3939 incoming
3940 .send(frame(
3941 Kind::Welcome,
3942 None,
3943 serde_json::json!({ "version": 999 }),
3944 ))
3945 .await
3946 .unwrap();
3947 let mut saw_error = false;
3948 let closed = loop {
3949 match tokio::time::timeout(std::time::Duration::from_millis(500), outgoing.recv()).await
3950 {
3951 Ok(Some(frame)) => {
3952 if frame.kind == Kind::Error {
3953 saw_error = true;
3954 }
3955 }
3956 Ok(None) => break true,
3957 Err(_) => break false,
3958 }
3959 };
3960 assert!(
3961 saw_error,
3962 "the peer must receive the close-reason error frame before the transport closes"
3963 );
3964 assert!(
3965 closed,
3966 "core deciding a session invalid must close the transport, not leave it open"
3967 );
3968 }
3969
3970 struct ScriptedPipe {
3971 frames: std::collections::VecDeque<Result<Bytes, unb_transport::TransportError>>,
3972 }
3973
3974 impl futures_util::Stream for ScriptedPipe {
3975 type Item = Result<Bytes, unb_transport::TransportError>;
3976
3977 fn poll_next(
3978 mut self: Pin<&mut Self>,
3979 _cx: &mut std::task::Context<'_>,
3980 ) -> std::task::Poll<Option<Self::Item>> {
3981 std::task::Poll::Ready(self.frames.pop_front())
3982 }
3983 }
3984
3985 impl futures_util::Sink<Bytes> for ScriptedPipe {
3986 type Error = unb_transport::TransportError;
3987
3988 fn poll_ready(
3989 self: Pin<&mut Self>,
3990 _cx: &mut std::task::Context<'_>,
3991 ) -> std::task::Poll<Result<(), Self::Error>> {
3992 std::task::Poll::Ready(Ok(()))
3993 }
3994
3995 fn start_send(self: Pin<&mut Self>, _item: Bytes) -> Result<(), Self::Error> {
3996 Ok(())
3997 }
3998
3999 fn poll_flush(
4000 self: Pin<&mut Self>,
4001 _cx: &mut std::task::Context<'_>,
4002 ) -> std::task::Poll<Result<(), Self::Error>> {
4003 std::task::Poll::Ready(Ok(()))
4004 }
4005
4006 fn poll_close(
4007 self: Pin<&mut Self>,
4008 _cx: &mut std::task::Context<'_>,
4009 ) -> std::task::Poll<Result<(), Self::Error>> {
4010 std::task::Poll::Ready(Ok(()))
4011 }
4012 }
4013
4014 #[tokio::test]
4015 async fn a_malformed_frame_retires_the_session_with_a_typed_reason() {
4016 let cancellation = CancellationToken::new();
4017 let (effect_tx, mut effects) = mpsc::channel(64);
4018 let handle = ProtocolCoreHandle::spawn(
4019 ProtocolCore::new("node"),
4020 Arc::new(RecordingExecutor { effects: effect_tx }),
4021 cancellation,
4022 &tokio::runtime::Handle::current(),
4023 );
4024 let pipe = Pipe::Piped {
4025 pipe: Box::pin(ScriptedPipe {
4026 frames: std::collections::VecDeque::from([Ok(Bytes::from_static(
4027 b"\xff\xff\xff\xffgarbage",
4028 ))]),
4029 }),
4030 initiator: true,
4031 };
4032 let (events, _events_rx) = mpsc::channel(16);
4033 handle
4034 .attach(SessionId::from("session"), pipe, None, events)
4035 .await
4036 .unwrap();
4037 let reason = tokio::time::timeout(std::time::Duration::from_secs(2), async {
4038 loop {
4039 if let CoreEffect::SessionRetired { reason, .. } = effects.recv().await.unwrap() {
4040 break reason;
4041 }
4042 }
4043 })
4044 .await
4045 .expect("a malformed frame must retire the session");
4046 assert_eq!(reason, RetirementReason::TransportFailed);
4047 handle.shutdown();
4048 }
4049
4050 #[tokio::test]
4051 async fn a_transport_read_error_retires_the_session_with_a_typed_reason() {
4052 let cancellation = CancellationToken::new();
4053 let (effect_tx, mut effects) = mpsc::channel(64);
4054 let handle = ProtocolCoreHandle::spawn(
4055 ProtocolCore::new("node"),
4056 Arc::new(RecordingExecutor { effects: effect_tx }),
4057 cancellation,
4058 &tokio::runtime::Handle::current(),
4059 );
4060 let pipe = Pipe::Piped {
4061 pipe: Box::pin(ScriptedPipe {
4062 frames: std::collections::VecDeque::from([Err(unb_transport::TransportError::Io(
4063 "connection reset".into(),
4064 ))]),
4065 }),
4066 initiator: true,
4067 };
4068 let (events, _events_rx) = mpsc::channel(16);
4069 handle
4070 .attach(SessionId::from("session"), pipe, None, events)
4071 .await
4072 .unwrap();
4073 let reason = tokio::time::timeout(std::time::Duration::from_secs(2), async {
4074 loop {
4075 if let CoreEffect::SessionRetired { reason, .. } = effects.recv().await.unwrap() {
4076 break reason;
4077 }
4078 }
4079 })
4080 .await
4081 .expect("a transport read error must retire the session");
4082 assert_eq!(reason, RetirementReason::TransportFailed);
4083 handle.shutdown();
4084 }
4085
4086 #[tokio::test]
4087 async fn routes_two_sessions_and_submits_transport_closure() {
4088 let cancellation = CancellationToken::new();
4089 let (effect_tx, mut effects) = mpsc::channel(16);
4090 let handle = ProtocolCoreHandle::spawn(
4091 ProtocolCore::new("node"),
4092 Arc::new(RecordingExecutor { effects: effect_tx }),
4093 cancellation,
4094 &tokio::runtime::Handle::current(),
4095 );
4096 let (first_pipe, first_incoming, mut first_outgoing) = local_pipe();
4097 let (second_pipe, _second_incoming, mut second_outgoing) = local_pipe();
4098 let (first_events, _first_events_rx) = mpsc::channel(16);
4099 let (second_events, _second_events_rx) = mpsc::channel(16);
4100
4101 let first = handle
4102 .attach(SessionId::from("first"), first_pipe, None, first_events)
4103 .await
4104 .unwrap();
4105 let second = handle
4106 .attach(SessionId::from("second"), second_pipe, None, second_events)
4107 .await
4108 .unwrap();
4109
4110 assert!(first_outgoing.recv().await.is_some());
4111 assert!(second_outgoing.recv().await.is_some());
4112 assert!(Arc::ptr_eq(&first, &first));
4113 assert!(!Arc::ptr_eq(&first, &second));
4114
4115 drop(first_incoming);
4116 let retired = loop {
4117 let effect = effects.recv().await.unwrap();
4118 if matches!(
4119 effect,
4120 CoreEffect::SessionRetired {
4121 ref session,
4122 reason: RetirementReason::SessionClosed,
4123 } if session == &SessionId::from("first")
4124 ) {
4125 break effect;
4126 }
4127 };
4128 assert!(matches!(
4129 retired,
4130 CoreEffect::SessionRetired {
4131 reason: RetirementReason::SessionClosed,
4132 ..
4133 }
4134 ));
4135
4136 second.shutdown();
4137 handle.shutdown();
4138 }
4139
4140 #[tokio::test]
4141 async fn install_publishes_after_acceptance_and_before_effect_execution() {
4142 let cancellation = CancellationToken::new();
4143 let published = Arc::new(AtomicBool::new(false));
4144 let (effect_tx, mut effects) = mpsc::channel(1);
4145 let handle = ProtocolCoreHandle::spawn(
4146 ProtocolCore::new("node"),
4147 Arc::new(PublicationExecutor {
4148 published: published.clone(),
4149 effects: effect_tx,
4150 }),
4151 cancellation,
4152 &tokio::runtime::Handle::current(),
4153 );
4154 let (pipe, _incoming, mut outgoing) = local_pipe();
4155 let (events, _events_rx) = mpsc::channel(16);
4156 handle
4157 .attach(SessionId::from("session"), pipe, None, events)
4158 .await
4159 .unwrap();
4160 assert!(outgoing.recv().await.is_some());
4161
4162 handle
4163 .install(
4164 CoreInput::SessionClosed {
4165 session: SessionId::from("session"),
4166 },
4167 {
4168 let published = published.clone();
4169 move || published.store(true, Ordering::SeqCst)
4170 },
4171 )
4172 .await
4173 .unwrap();
4174
4175 assert!(effects.recv().await.unwrap());
4176 handle.shutdown();
4177 }
4178
4179 #[tokio::test]
4180 async fn install_failure_does_not_publish() {
4181 let cancellation = CancellationToken::new();
4182 let (effect_tx, _effects) = mpsc::channel(1);
4183 let handle = ProtocolCoreHandle::spawn(
4184 ProtocolCore::new("node"),
4185 Arc::new(RecordingExecutor { effects: effect_tx }),
4186 cancellation,
4187 &tokio::runtime::Handle::current(),
4188 );
4189 let input = CoreInput::SessionOpened {
4190 session: SessionId::from("duplicate"),
4191 initiator: true,
4192 establish_peer: false,
4193 expected_peer: None,
4194 };
4195 handle.submit(input.clone()).await.unwrap();
4196 let published = Arc::new(AtomicBool::new(false));
4197
4198 let result = handle
4199 .install(input, {
4200 let published = published.clone();
4201 move || published.store(true, Ordering::SeqCst)
4202 })
4203 .await;
4204
4205 assert!(matches!(result, Err(WsError::Core(_))));
4206 assert!(!published.load(Ordering::SeqCst));
4207 handle.shutdown();
4208 }
4209
4210 #[test]
4211 fn operation_records_cannot_change_their_bound_correlation() {
4212 let response = frame(
4213 Kind::Response,
4214 Some("s2".into()),
4215 serde_json::json!({"ok": true}),
4216 );
4217 let error =
4218 decode_operation_message(response.encode(), "s1", OperationDirection::Return, 0)
4219 .unwrap_err();
4220 assert!(matches!(
4221 error,
4222 unb_transport::operation::OperationCodecError::Protocol(_)
4223 ));
4224 assert_eq!(error.reset_code(), unb_transport::operation::RESET_PROTOCOL);
4225 }
4226
4227 #[tokio::test]
4228 async fn streaming_operation_body_uses_head_chunks_and_no_wire_body_token() {
4229 let mut envelope = frame(Kind::Response, Some("s1".into()), serde_json::json!(null));
4230 envelope.body_token = Some("legacy-token-must-not-leak".into());
4231 let body: BodyStream = Box::pin(futures_util::stream::iter([
4232 Ok(Bytes::from_static(b"first")),
4233 Ok(Bytes::from_static(b"second")),
4234 ]));
4235 let (write, read) = tokio::io::duplex(4096);
4236 let sender = async move {
4237 let mut writer = unb_transport::operation::OperationRecordWriter::new(write);
4238 write_operation_envelope(
4239 &mut writer,
4240 envelope,
4241 Some(body),
4242 Arc::new(Semaphore::new(OUTBOUND_BYTE_BUDGET)),
4243 CancellationToken::new(),
4244 )
4245 .await
4246 .unwrap();
4247 writer.finish().await.unwrap();
4248 };
4249 let receiver = async move {
4250 let mut reader = unb_transport::operation::OperationRecordReader::new(read);
4251 let (kind, payload) = reader.read_record().await.unwrap();
4252 assert_eq!(kind, unb_transport::operation::OperationRecordKind::Head);
4253 let head = Envelope::decode(payload).unwrap();
4254 assert_eq!(head.corr.as_deref(), Some("s1"));
4255 assert!(head.body_token.is_none());
4256 assert!(head.payload.is_empty());
4257 let mut collected = Vec::new();
4258 loop {
4259 let (kind, payload) = reader.read_record().await.unwrap();
4260 match kind {
4261 unb_transport::operation::OperationRecordKind::BodyChunk => {
4262 collected.extend_from_slice(&payload)
4263 }
4264 unb_transport::operation::OperationRecordKind::BodyEnd => break,
4265 other => panic!("unexpected body record {other:?}"),
4266 }
4267 }
4268 assert_eq!(collected, b"firstsecond");
4269 let (kind, payload) = reader.read_record().await.unwrap();
4270 assert_eq!(
4271 kind,
4272 unb_transport::operation::OperationRecordKind::DirectionEnd
4273 );
4274 assert!(payload.is_empty());
4275 reader.expect_fin().await.unwrap();
4276 };
4277 tokio::join!(sender, receiver);
4278 }
4279
4280 #[tokio::test]
4281 async fn cancelling_a_client_operation_releases_a_late_response_body() {
4282 let handle = spawn_core();
4283 let (pipe, incoming, mut outgoing) = local_pipe();
4284 let wire = attach_client(&handle, "cancel-body", pipe).await;
4285 establish(&incoming, &mut outgoing).await;
4286
4287 let call = wire
4288 .client_session()
4289 .start(
4290 "/server/echo",
4291 Kind::Request,
4292 Bytes::from_static(b"request"),
4293 None,
4294 Default::default(),
4295 )
4296 .await
4297 .unwrap();
4298 let corr = call.operation().as_str().to_owned();
4299 assert_eq!(
4300 outgoing.recv().await.unwrap().corr.as_deref(),
4301 Some(corr.as_str())
4302 );
4303 drop(call);
4304 let cancel = outgoing.recv().await.unwrap();
4305 assert_eq!(cancel.kind, Kind::Cancel);
4306 assert_eq!(cancel.corr.as_deref(), Some(corr.as_str()));
4307
4308 incoming
4309 .send(frame(
4310 Kind::Response,
4311 Some(corr),
4312 serde_json::json!({ "late": true }),
4313 ))
4314 .await
4315 .unwrap();
4316 tokio::time::timeout(std::time::Duration::from_secs(1), async {
4317 loop {
4318 let retained = handle
4319 .token_bodies
4320 .lock()
4321 .expect("body token registry")
4322 .keys()
4323 .any(|(owner, _)| owner.as_str() == "cancel-body");
4324 if !retained {
4325 break;
4326 }
4327 tokio::task::yield_now().await;
4328 }
4329 })
4330 .await
4331 .expect("late response body must be released");
4332 handle.shutdown();
4333 }
4334
4335 #[tokio::test]
4336 async fn session_retirement_releases_unclaimed_bodies() {
4337 let handle = spawn_core();
4338 let (pipe, incoming, mut outgoing) = local_pipe();
4339 let _wire = attach_client(&handle, "retired-body", pipe).await;
4340 establish(&incoming, &mut outgoing).await;
4341 handle
4342 .register_body(
4343 &SessionId::from("retired-body"),
4344 WireBody::Bytes(Bytes::from_static(b"retained")),
4345 )
4346 .unwrap();
4347 assert!(handle
4348 .token_bodies
4349 .lock()
4350 .expect("body token registry")
4351 .keys()
4352 .any(|(owner, _)| owner.as_str() == "retired-body"));
4353
4354 drop(incoming);
4355 tokio::time::timeout(std::time::Duration::from_secs(1), async {
4356 loop {
4357 let retained = handle
4358 .token_bodies
4359 .lock()
4360 .expect("body token registry")
4361 .keys()
4362 .any(|(owner, _)| owner.as_str() == "retired-body");
4363 if !retained {
4364 break;
4365 }
4366 tokio::task::yield_now().await;
4367 }
4368 })
4369 .await
4370 .expect("session retirement must release body capabilities");
4371 handle.shutdown();
4372 }
4373}