1use std::process::Stdio;
12use std::sync::atomic::{AtomicI64, AtomicUsize, Ordering};
13use std::sync::{Arc, Weak};
14
15use scc::HashMap as SccHashMap;
16use tokio::io::{AsyncReadExt, AsyncWrite, AsyncWriteExt};
17use tokio::process::{Child, ChildStdout, Command};
18use tokio::sync::{broadcast, mpsc, oneshot};
19
20use crate::wire::{self, WireFrameCodec};
21use crate::TransportError;
22
23const EVENT_CHANNEL_CAPACITY: usize = 4096;
25
26const REQUEST_FRAME_QUEUE_CAPACITY: usize = 4096;
28
29const CONTROL_FRAME_QUEUE_CAPACITY: usize = 1024;
31
32const PENDING_REQUEST_LIMIT: usize = 4096;
34
35const SIDECAR_BIN_ENV: &str = "AGENTOS_SIDECAR_BIN";
38
39const SIDECAR_SILENCE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
45
46pub type WireSidecarCallback = Arc<
48 dyn Fn(
49 wire::SidecarRequestPayload,
50 wire::OwnershipScope,
51 ) -> futures::future::BoxFuture<
52 'static,
53 Result<wire::SidecarResponsePayload, TransportError>,
54 > + Send
55 + Sync,
56>;
57
58pub struct SidecarTransport {
61 child: parking_lot::Mutex<Option<Child>>,
63 pending: SccHashMap<wire::RequestId, oneshot::Sender<wire::ResponsePayload>>,
65 pending_request_lock: parking_lot::Mutex<()>,
66 request_counter: AtomicI64,
68 max_frame_bytes: AtomicUsize,
70 event_tx: broadcast::Sender<(wire::OwnershipScope, wire::EventPayload)>,
72 callbacks: SccHashMap<&'static str, WireSidecarCallback>,
74 request_writer_tx: mpsc::Sender<Vec<u8>>,
76 control_writer_tx: mpsc::Sender<Vec<u8>>,
78 last_inbound_at: parking_lot::Mutex<std::time::Instant>,
80}
81
82impl SidecarTransport {
83 pub async fn spawn(binary_path: Option<String>) -> Result<Arc<Self>, TransportError> {
88 let bin = resolve_sidecar_binary_path(binary_path);
89 let mut child = Command::new(&bin)
90 .stdin(Stdio::piped())
91 .stdout(Stdio::piped())
92 .stderr(Stdio::inherit())
93 .kill_on_drop(true)
94 .spawn()
95 .map_err(|error| {
96 TransportError::Sidecar(format!("failed to spawn sidecar '{bin}': {error}"))
97 })?;
98
99 let stdin = child
100 .stdin
101 .take()
102 .ok_or_else(|| TransportError::Sidecar("sidecar stdin was not piped".to_string()))?;
103 let stdout = child
104 .stdout
105 .take()
106 .ok_or_else(|| TransportError::Sidecar("sidecar stdout was not piped".to_string()))?;
107
108 let (request_writer_tx, request_writer_rx) = mpsc::channel(REQUEST_FRAME_QUEUE_CAPACITY);
109 let (control_writer_tx, control_writer_rx) = mpsc::channel(CONTROL_FRAME_QUEUE_CAPACITY);
110 let (event_tx, _) = broadcast::channel(EVENT_CHANNEL_CAPACITY);
111
112 let transport = Arc::new(Self {
113 child: parking_lot::Mutex::new(Some(child)),
114 pending: SccHashMap::new(),
115 pending_request_lock: parking_lot::Mutex::new(()),
116 request_counter: AtomicI64::new(1),
117 max_frame_bytes: AtomicUsize::new(wire::DEFAULT_MAX_FRAME_BYTES),
118 event_tx,
119 callbacks: SccHashMap::new(),
120 request_writer_tx,
121 control_writer_tx,
122 last_inbound_at: parking_lot::Mutex::new(std::time::Instant::now()),
123 });
124
125 tokio::spawn(run_writer(stdin, control_writer_rx, request_writer_rx));
126 tokio::spawn(run_reader(Arc::downgrade(&transport), stdout));
127 tokio::spawn(run_silence_watchdog(
128 Arc::downgrade(&transport),
129 SIDECAR_SILENCE_TIMEOUT,
130 ));
131
132 Ok(transport)
133 }
134
135 pub fn next_request_id(&self) -> wire::RequestId {
137 self.request_counter.fetch_add(1, Ordering::SeqCst)
138 }
139
140 pub async fn request_wire(
142 &self,
143 ownership: wire::OwnershipScope,
144 payload: wire::RequestPayload,
145 ) -> Result<wire::ResponsePayload, TransportError> {
146 self.request_wire_with_frame_limit(ownership, payload, None)
147 .await
148 }
149
150 pub async fn request_wire_bounded(
152 &self,
153 ownership: wire::OwnershipScope,
154 payload: wire::RequestPayload,
155 max_frame_bytes: usize,
156 ) -> Result<wire::ResponsePayload, TransportError> {
157 self.request_wire_with_frame_limit(ownership, payload, Some(max_frame_bytes))
158 .await
159 }
160
161 async fn request_wire_with_frame_limit(
162 &self,
163 ownership: wire::OwnershipScope,
164 payload: wire::RequestPayload,
165 max_frame_bytes: Option<usize>,
166 ) -> Result<wire::ResponsePayload, TransportError> {
167 let request_id = self.next_request_id();
168 let frame = wire::ProtocolFrame::RequestFrame(wire::RequestFrame {
169 schema: wire::protocol_schema(),
170 request_id,
171 ownership,
172 payload,
173 });
174 let bytes = self.encode_wire_frame(&frame, max_frame_bytes)?;
175
176 let (tx, rx) = oneshot::channel();
177 self.register_pending_request(request_id, tx)?;
178 let _pending_guard = PendingRequestGuard::new(self, request_id);
179
180 if self.request_writer_tx.send(bytes).await.is_err() {
181 self.pending.remove(&request_id);
182 return Err(TransportError::Sidecar(
183 "sidecar transport closed".to_string(),
184 ));
185 }
186
187 rx.await
188 .map_err(|_| TransportError::Sidecar("sidecar transport disconnected".to_string()))
189 }
190
191 pub fn subscribe_wire_events(
193 &self,
194 ) -> broadcast::Receiver<(wire::OwnershipScope, wire::EventPayload)> {
195 self.event_tx.subscribe()
196 }
197
198 pub fn register_wire_callback(&self, key: &'static str, callback: WireSidecarCallback) {
201 let _ = self.callbacks.insert(key, callback);
202 }
203
204 pub fn max_frame_bytes(&self) -> usize {
206 self.max_frame_bytes.load(Ordering::Relaxed)
207 }
208
209 pub fn set_max_frame_bytes(&self, max_frame_bytes: usize) {
211 self.max_frame_bytes
212 .store(max_frame_bytes, Ordering::SeqCst);
213 }
214
215 pub fn kill_child(&self) {
217 if let Some(mut child) = self.child.lock().take() {
218 let _ = child.start_kill();
219 }
220 }
221
222 fn encode_wire_frame(
223 &self,
224 frame: &wire::ProtocolFrame,
225 max_frame_bytes: Option<usize>,
226 ) -> Result<Vec<u8>, TransportError> {
227 let transport_limit = self.max_frame_bytes.load(Ordering::Relaxed);
228 let max_frame_bytes = max_frame_bytes
229 .map(|limit| limit.min(transport_limit))
230 .unwrap_or(transport_limit);
231 let codec = WireFrameCodec::new(max_frame_bytes);
232 Ok(codec.encode(frame)?)
233 }
234
235 async fn handle_wire_frame(self: &Arc<Self>, frame: wire::ProtocolFrame) {
238 match frame {
239 wire::ProtocolFrame::ResponseFrame(response) => {
240 match self.pending.remove(&response.request_id) {
241 Some((_, tx)) => {
242 let _ = tx.send(response.payload);
243 }
244 None => {
245 tracing::warn!(
246 request_id = response.request_id,
247 "response for unknown request id"
248 )
249 }
250 }
251 }
252 wire::ProtocolFrame::EventFrame(event) => {
253 if matches!(
258 &event.payload,
259 wire::EventPayload::StructuredEvent(structured)
260 if structured.name == "heartbeat"
261 ) {
262 return;
263 }
264 let _ = self.event_tx.send((event.ownership, event.payload));
265 }
266 wire::ProtocolFrame::SidecarRequestFrame(request) => {
267 self.dispatch_sidecar_request(request).await
268 }
269 wire::ProtocolFrame::SidecarResponseFrame(_) | wire::ProtocolFrame::RequestFrame(_) => {
270 tracing::warn!("unexpected inbound frame on host transport")
271 }
272 }
273 }
274
275 async fn dispatch_sidecar_request(self: &Arc<Self>, frame: wire::SidecarRequestFrame) {
280 let key = sidecar_request_key(&frame.payload);
281 let callback = self.callbacks.read(&key, |_, value| value.clone());
282 match callback {
283 Some(callback) => {
284 let transport = Arc::downgrade(self);
285 tokio::spawn(async move {
286 match callback(frame.payload, frame.ownership.clone()).await {
287 Ok(payload) => {
288 let response = wire::ProtocolFrame::SidecarResponseFrame(
289 wire::SidecarResponseFrame {
290 schema: wire::protocol_schema(),
291 request_id: frame.request_id,
292 ownership: frame.ownership,
293 payload,
294 },
295 );
296 let Some(transport) = transport.upgrade() else {
298 return;
299 };
300 if let Ok(bytes) = transport.encode_wire_frame(&response, None) {
301 let _ = transport.control_writer_tx.send(bytes).await;
302 }
303 }
304 Err(error) => tracing::warn!(?error, key, "sidecar callback failed"),
305 }
306 });
307 }
308 None => tracing::warn!(key, "no callback registered for sidecar request"),
309 }
310 }
311
312 fn fail_all_pending(&self) {
314 self.pending.clear();
315 }
316
317 fn register_pending_request(
318 &self,
319 request_id: wire::RequestId,
320 tx: oneshot::Sender<wire::ResponsePayload>,
321 ) -> Result<(), TransportError> {
322 let _guard = self.pending_request_lock.lock();
323 if pending_request_count(self) >= PENDING_REQUEST_LIMIT {
324 return Err(TransportError::Sidecar(format!(
325 "sidecar pending request limit exceeded: at most {PENDING_REQUEST_LIMIT} requests can be in flight"
326 )));
327 }
328 let _ = self.pending.insert(request_id, tx);
329 Ok(())
330 }
331}
332
333struct PendingRequestGuard<'a> {
334 transport: &'a SidecarTransport,
335 request_id: wire::RequestId,
336}
337
338impl<'a> PendingRequestGuard<'a> {
339 fn new(transport: &'a SidecarTransport, request_id: wire::RequestId) -> Self {
340 Self {
341 transport,
342 request_id,
343 }
344 }
345}
346
347impl Drop for PendingRequestGuard<'_> {
348 fn drop(&mut self) {
349 let _ = self.transport.pending.remove(&self.request_id);
350 }
351}
352
353fn pending_request_count(transport: &SidecarTransport) -> usize {
354 let mut count = 0;
355 transport.pending.scan(|_, _| {
356 count += 1;
357 });
358 count
359}
360
361fn sidecar_request_key(payload: &wire::SidecarRequestPayload) -> &'static str {
363 match payload {
364 wire::SidecarRequestPayload::HostCallbackRequest(_) => "host_callback",
365 wire::SidecarRequestPayload::JsBridgeCallRequest(_) => "js_bridge_call",
366 wire::SidecarRequestPayload::ExtEnvelope(_) => "ext",
367 }
368}
369
370async fn run_writer<W>(
373 mut stdin: W,
374 mut control_rx: mpsc::Receiver<Vec<u8>>,
375 mut request_rx: mpsc::Receiver<Vec<u8>>,
376) where
377 W: AsyncWrite + Unpin,
378{
379 let mut prefer_control = true;
380 loop {
381 let (bytes, wrote_control) = if prefer_control {
382 tokio::select! {
383 biased;
384 bytes = control_rx.recv() => match bytes {
385 Some(bytes) => (bytes, true),
386 None => match request_rx.recv().await {
387 Some(bytes) => (bytes, false),
388 None => break,
389 },
390 },
391 bytes = request_rx.recv() => match bytes {
392 Some(bytes) => (bytes, false),
393 None => match control_rx.recv().await {
394 Some(bytes) => (bytes, true),
395 None => break,
396 },
397 },
398 }
399 } else {
400 tokio::select! {
401 biased;
402 bytes = request_rx.recv() => match bytes {
403 Some(bytes) => (bytes, false),
404 None => match control_rx.recv().await {
405 Some(bytes) => (bytes, true),
406 None => break,
407 },
408 },
409 bytes = control_rx.recv() => match bytes {
410 Some(bytes) => (bytes, true),
411 None => match request_rx.recv().await {
412 Some(bytes) => (bytes, false),
413 None => break,
414 },
415 },
416 }
417 };
418 if stdin.write_all(&bytes).await.is_err() {
419 break;
420 }
421 if stdin.flush().await.is_err() {
422 break;
423 }
424 prefer_control = !wrote_control;
425 }
426}
427
428async fn run_reader(transport: Weak<SidecarTransport>, mut stdout: ChildStdout) {
432 loop {
433 let mut length_buf = [0u8; 4];
434 if stdout.read_exact(&mut length_buf).await.is_err() {
435 break;
436 }
437 let length = u32::from_be_bytes(length_buf) as usize;
438
439 let Some(transport) = transport.upgrade() else {
440 break;
441 };
442 let max_frame_bytes = transport.max_frame_bytes.load(Ordering::Relaxed);
443 if frame_length_exceeds_limit(length, max_frame_bytes) {
444 tracing::warn!(
445 size = length,
446 max = max_frame_bytes,
447 "sidecar frame exceeds negotiated limit"
448 );
449 break;
450 }
451
452 let mut frame_bytes = vec![0u8; 4 + length];
453 frame_bytes[..4].copy_from_slice(&length_buf);
454 if stdout.read_exact(&mut frame_bytes[4..]).await.is_err() {
455 break;
456 }
457 *transport.last_inbound_at.lock() = std::time::Instant::now();
460
461 let codec = WireFrameCodec::new(max_frame_bytes);
462 match codec.decode(&frame_bytes) {
463 Ok(frame) => transport.handle_wire_frame(frame).await,
464 Err(error) => tracing::warn!(?error, "failed to decode sidecar frame"),
465 }
466 }
467
468 if let Some(transport) = transport.upgrade() {
469 transport.fail_all_pending();
470 }
471}
472
473fn frame_length_exceeds_limit(length: usize, max_frame_bytes: usize) -> bool {
474 length > max_frame_bytes
475}
476
477async fn run_silence_watchdog(transport: Weak<SidecarTransport>, timeout: std::time::Duration) {
481 let check_interval = (timeout / 4).min(std::time::Duration::from_secs(1));
482 loop {
483 tokio::time::sleep(check_interval).await;
484 let Some(transport) = transport.upgrade() else {
485 return;
486 };
487 let silence = transport.last_inbound_at.lock().elapsed();
488 if silence < timeout {
489 continue;
490 }
491 tracing::error!(
492 silence_ms = silence.as_millis() as u64,
493 "sidecar unresponsive: no protocol frames or heartbeats; killing sidecar",
494 );
495 transport.kill_child();
496 transport.fail_all_pending();
497 return;
498 }
499}
500
501fn resolve_sidecar_binary_path(binary_path: Option<String>) -> String {
502 binary_path
503 .or_else(|| std::env::var(SIDECAR_BIN_ENV).ok())
504 .unwrap_or_else(|| "agentos-native-sidecar".to_string())
505}
506
507#[cfg(test)]
508mod tests {
509 use super::*;
510 use std::sync::Mutex;
511
512 static ENV_LOCK: Mutex<()> = Mutex::new(());
513
514 fn test_transport() -> SidecarTransport {
515 let (request_writer_tx, _request_writer_rx) = mpsc::channel(REQUEST_FRAME_QUEUE_CAPACITY);
516 let (control_writer_tx, _control_writer_rx) = mpsc::channel(CONTROL_FRAME_QUEUE_CAPACITY);
517 let (event_tx, _) = broadcast::channel(EVENT_CHANNEL_CAPACITY);
518 SidecarTransport {
519 child: parking_lot::Mutex::new(None),
520 pending: SccHashMap::new(),
521 pending_request_lock: parking_lot::Mutex::new(()),
522 request_counter: AtomicI64::new(1),
523 max_frame_bytes: AtomicUsize::new(wire::DEFAULT_MAX_FRAME_BYTES),
524 event_tx,
525 callbacks: SccHashMap::new(),
526 request_writer_tx,
527 control_writer_tx,
528 last_inbound_at: parking_lot::Mutex::new(std::time::Instant::now()),
529 }
530 }
531
532 #[test]
533 fn binary_path_prefers_explicit_path_over_env() {
534 let _guard = ENV_LOCK.lock().expect("env lock");
535 let previous = std::env::var(SIDECAR_BIN_ENV).ok();
536 std::env::set_var(SIDECAR_BIN_ENV, "/tmp/from-env");
537
538 assert_eq!(
539 resolve_sidecar_binary_path(Some("/tmp/from-config".to_string())),
540 "/tmp/from-config"
541 );
542
543 restore_env(SIDECAR_BIN_ENV, previous);
544 }
545
546 #[test]
547 fn binary_path_uses_secure_exec_env_fallback() {
548 let _guard = ENV_LOCK.lock().expect("env lock");
549 let previous = std::env::var(SIDECAR_BIN_ENV).ok();
550 std::env::set_var(SIDECAR_BIN_ENV, "/tmp/agentos-native-sidecar");
551
552 assert_eq!(
553 resolve_sidecar_binary_path(None),
554 "/tmp/agentos-native-sidecar"
555 );
556
557 restore_env(SIDECAR_BIN_ENV, previous);
558 }
559
560 #[test]
561 fn binary_path_defaults_to_agentos_native_sidecar() {
562 let _guard = ENV_LOCK.lock().expect("env lock");
563 let previous = std::env::var(SIDECAR_BIN_ENV).ok();
564 std::env::remove_var(SIDECAR_BIN_ENV);
565
566 assert_eq!(resolve_sidecar_binary_path(None), "agentos-native-sidecar");
567
568 restore_env(SIDECAR_BIN_ENV, previous);
569 }
570
571 fn restore_env(key: &str, value: Option<String>) {
572 match value {
573 Some(value) => std::env::set_var(key, value),
574 None => std::env::remove_var(key),
575 }
576 }
577
578 #[test]
579 fn frame_length_limit_rejects_oversized_declared_length() {
580 assert!(!frame_length_exceeds_limit(1024, 1024));
581 assert!(frame_length_exceeds_limit(1025, 1024));
582 }
583
584 #[test]
585 fn transport_encodes_requests_with_generated_wire_codec() {
586 let transport = test_transport();
587 let frame = wire::ProtocolFrame::RequestFrame(wire::RequestFrame {
588 schema: wire::protocol_schema(),
589 request_id: 7,
590 ownership: wire::OwnershipScope::ConnectionOwnership(wire::ConnectionOwnership {
591 connection_id: "conn-1".to_string(),
592 }),
593 payload: wire::RequestPayload::AuthenticateRequest(wire::AuthenticateRequest {
594 client_name: "transport-test".to_string(),
595 auth_token: "token".to_string(),
596 protocol_version: wire::PROTOCOL_VERSION,
597 bridge_version: 1,
598 }),
599 });
600
601 let encoded = transport
602 .encode_wire_frame(&frame, None)
603 .expect("encode transport frame");
604 let decoded = WireFrameCodec::default()
605 .decode(&encoded)
606 .expect("decode generated wire frame");
607
608 assert!(matches!(
609 decoded,
610 wire::ProtocolFrame::RequestFrame(wire::RequestFrame {
611 payload: wire::RequestPayload::AuthenticateRequest(_),
612 ..
613 })
614 ));
615 }
616
617 #[tokio::test]
618 async fn transport_fans_out_generated_wire_events() {
619 let transport = Arc::new(test_transport());
620 let mut wire_events = transport.subscribe_wire_events();
621
622 transport
623 .handle_wire_frame(wire::ProtocolFrame::EventFrame(wire::EventFrame {
624 schema: wire::protocol_schema(),
625 ownership: wire::OwnershipScope::VmOwnership(wire::VmOwnership {
626 connection_id: "conn-1".to_string(),
627 session_id: "session-1".to_string(),
628 vm_id: "vm-1".to_string(),
629 }),
630 payload: wire::EventPayload::ProcessOutputEvent(wire::ProcessOutputEvent {
631 process_id: "proc-1".to_string(),
632 channel: wire::StreamChannel::Stdout,
633 chunk: b"hello".to_vec(),
634 }),
635 }))
636 .await;
637
638 let (ownership, payload) = wire_events.recv().await.expect("wire event");
639 assert!(matches!(
640 ownership,
641 wire::OwnershipScope::VmOwnership(wire::VmOwnership {
642 connection_id,
643 session_id,
644 vm_id,
645 }) if connection_id == "conn-1" && session_id == "session-1" && vm_id == "vm-1"
646 ));
647 assert!(matches!(
648 payload,
649 wire::EventPayload::ProcessOutputEvent(wire::ProcessOutputEvent {
650 process_id,
651 channel: wire::StreamChannel::Stdout,
652 chunk,
653 }) if process_id == "proc-1" && chunk == b"hello".to_vec()
654 ));
655 }
656
657 #[tokio::test]
658 async fn silence_watchdog_fails_pending_requests_after_sustained_silence() {
659 let transport = Arc::new(test_transport());
660 let (tx, rx) = oneshot::channel();
661 transport
662 .register_pending_request(1, tx)
663 .expect("register pending request");
664
665 tokio::spawn(run_silence_watchdog(
666 Arc::downgrade(&transport),
667 std::time::Duration::from_millis(40),
668 ));
669
670 rx.await
673 .expect_err("watchdog should drop the pending sender");
674 assert_eq!(pending_request_count(&transport), 0);
675 }
676
677 #[tokio::test]
678 async fn silence_watchdog_stays_quiet_while_frames_arrive() {
679 let transport = Arc::new(test_transport());
680 let (tx, mut rx) = oneshot::channel();
681 transport
682 .register_pending_request(1, tx)
683 .expect("register pending request");
684
685 tokio::spawn(run_silence_watchdog(
686 Arc::downgrade(&transport),
687 std::time::Duration::from_millis(120),
688 ));
689
690 for _ in 0..6 {
693 tokio::time::sleep(std::time::Duration::from_millis(40)).await;
694 *transport.last_inbound_at.lock() = std::time::Instant::now();
695 assert!(
696 rx.try_recv().is_err(),
697 "pending request must remain registered while frames arrive"
698 );
699 }
700 assert_eq!(pending_request_count(&transport), 1);
701 }
702
703 #[tokio::test]
704 async fn heartbeat_events_are_swallowed_before_the_event_fanout() {
705 let transport = Arc::new(test_transport());
706 let mut wire_events = transport.subscribe_wire_events();
707
708 transport
709 .handle_wire_frame(wire::ProtocolFrame::EventFrame(wire::EventFrame {
710 schema: wire::protocol_schema(),
711 ownership: wire::OwnershipScope::ConnectionOwnership(wire::ConnectionOwnership {
712 connection_id: "sidecar-transport".to_string(),
713 }),
714 payload: wire::EventPayload::StructuredEvent(wire::StructuredEvent {
715 name: "heartbeat".to_string(),
716 detail: std::collections::HashMap::new(),
717 }),
718 }))
719 .await;
720 transport
723 .handle_wire_frame(wire::ProtocolFrame::EventFrame(wire::EventFrame {
724 schema: wire::protocol_schema(),
725 ownership: wire::OwnershipScope::ConnectionOwnership(wire::ConnectionOwnership {
726 connection_id: "conn-1".to_string(),
727 }),
728 payload: wire::EventPayload::StructuredEvent(wire::StructuredEvent {
729 name: "limit_warning".to_string(),
730 detail: std::collections::HashMap::new(),
731 }),
732 }))
733 .await;
734
735 let (_, payload) = wire_events.recv().await.expect("structured event");
736 assert!(matches!(
737 payload,
738 wire::EventPayload::StructuredEvent(wire::StructuredEvent { name, .. })
739 if name == "limit_warning"
740 ));
741 assert!(
742 wire_events.try_recv().is_err(),
743 "heartbeat must not fan out"
744 );
745 }
746
747 #[test]
748 fn pending_request_guard_removes_registered_slot_on_drop() {
749 let transport = test_transport();
750 let (tx, _rx) = oneshot::channel();
751 transport
752 .register_pending_request(1, tx)
753 .expect("register pending request");
754
755 {
756 let _guard = PendingRequestGuard::new(&transport, 1);
757 assert_eq!(pending_request_count(&transport), 1);
758 }
759
760 assert_eq!(pending_request_count(&transport), 0);
761 }
762
763 #[test]
764 fn pending_request_limit_rejects_full_transport() {
765 let transport = test_transport();
766 for request_id in 1..=PENDING_REQUEST_LIMIT as wire::RequestId {
767 let (tx, _rx) = oneshot::channel();
768 transport
769 .register_pending_request(request_id, tx)
770 .expect("register pending request");
771 }
772 let (tx, _rx) = oneshot::channel();
773 let error = transport
774 .register_pending_request((PENDING_REQUEST_LIMIT + 1) as wire::RequestId, tx)
775 .expect_err("full pending map should reject");
776
777 assert!(
778 error
779 .to_string()
780 .contains("sidecar pending request limit exceeded"),
781 "unexpected error: {error}"
782 );
783 }
784
785 #[tokio::test]
786 async fn writer_prioritizes_control_frames_over_request_backlog() {
787 let (client, mut server) = tokio::io::duplex(64);
788 let (control_tx, control_rx) = mpsc::channel(CONTROL_FRAME_QUEUE_CAPACITY);
789 let (request_tx, request_rx) = mpsc::channel(REQUEST_FRAME_QUEUE_CAPACITY);
790 request_tx
791 .send(vec![b'r'])
792 .await
793 .expect("send request frame");
794 control_tx
795 .send(vec![b'c'])
796 .await
797 .expect("send control frame");
798 drop(control_tx);
799 drop(request_tx);
800
801 let writer = tokio::spawn(run_writer(client, control_rx, request_rx));
802 let mut first = [0u8; 1];
803 server
804 .read_exact(&mut first)
805 .await
806 .expect("read first byte");
807 writer.await.expect("writer task");
808
809 assert_eq!(first, [b'c']);
810 }
811
812 #[tokio::test]
813 async fn writer_alternates_when_control_and_request_are_ready() {
814 let (client, mut server) = tokio::io::duplex(64);
815 let (control_tx, control_rx) = mpsc::channel(CONTROL_FRAME_QUEUE_CAPACITY);
816 let (request_tx, request_rx) = mpsc::channel(REQUEST_FRAME_QUEUE_CAPACITY);
817 control_tx.send(vec![b'c']).await.expect("control one");
818 control_tx.send(vec![b'C']).await.expect("control two");
819 request_tx.send(vec![b'r']).await.expect("request one");
820 request_tx.send(vec![b'R']).await.expect("request two");
821 drop(control_tx);
822 drop(request_tx);
823
824 let writer = tokio::spawn(run_writer(client, control_rx, request_rx));
825 let mut output = [0u8; 4];
826 server.read_exact(&mut output).await.expect("read output");
827 writer.await.expect("writer task");
828
829 assert_eq!(output, [b'c', b'r', b'C', b'R']);
830 }
831}