1use std::{
2 collections::HashMap,
3 error::Error,
4 fmt,
5 future::Future,
6 io,
7 path::{Path, PathBuf},
8 pin::Pin,
9 sync::{
10 atomic::{AtomicBool, AtomicU64, Ordering},
11 Arc, Mutex, MutexGuard,
12 },
13 task::{Context, Poll},
14 time::Duration,
15};
16
17use subc_control::{
18 CatalogEntry, ClientControlRequest, ClientControlResponse, ConsumerIdentity, PollKind,
19};
20use subc_protocol::{
21 AdmissionClass, BindIdentity, ErrorBody, Flags, Frame, FrameBuildError, FrameType, Priority,
22 RouteTarget, SUBC_LAUNCH_NONCE_ENV, SUBC_MODULE_ID_ENV,
23};
24
25use crate::RouteHandle;
26use subc_transport::{
27 authenticate_client, connection_file, read_frame, write_frame, AuthError, ConnectionFileError,
28 FrameIoError,
29};
30use tokio::{
31 io::{AsyncWrite, AsyncWriteExt, BufWriter},
32 net::{tcp::OwnedReadHalf, TcpStream},
33 sync::{mpsc, oneshot, Notify, OwnedSemaphorePermit, Semaphore},
34 task::JoinHandle,
35 time::{sleep, timeout_at, Instant},
36};
37use tokio_util::sync::CancellationToken;
38
39const DEFAULT_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(2);
40const DEFAULT_CALL_TIMEOUT: Duration = Duration::from_secs(30);
41const DEFAULT_ROUTE_RETRY_DEADLINE: Duration = Duration::from_secs(30);
47const DEFAULT_RESTORED_DEBOUNCE: Duration = Duration::from_millis(250);
48const EGRESS_BUFFER: usize = 128;
49const DEFAULT_ROUTE_WINDOW: usize = 1024;
50const DEFAULT_SUBSCRIPTION_EVENT_BUFFER: usize = 128;
51const DEFAULT_PUSH_EVENT_BUFFER: usize = 128;
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub struct RetryBackoff {
56 pub base: Duration,
57 pub cap: Duration,
58 pub max_attempts: usize,
60}
61
62impl Default for RetryBackoff {
63 fn default() -> Self {
64 Self {
65 base: Duration::from_millis(100),
66 cap: Duration::from_secs(2),
67 max_attempts: 6,
68 }
69 }
70}
71
72impl RetryBackoff {
73 fn delay_after_attempt(self, attempt: usize) -> Duration {
74 let mut delay = self.base;
75 for _ in 1..attempt {
76 delay = (delay * 2).min(self.cap);
77 }
78 delay
79 }
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct ConsumerOptions {
85 pub handshake_timeout: Duration,
86 pub call_timeout: Duration,
88 pub reconnect_backoff: RetryBackoff,
89 pub restored_debounce: Duration,
90}
91
92impl Default for ConsumerOptions {
93 fn default() -> Self {
94 Self {
95 handshake_timeout: DEFAULT_HANDSHAKE_TIMEOUT,
96 call_timeout: DEFAULT_CALL_TIMEOUT,
97 reconnect_backoff: RetryBackoff::default(),
98 restored_debounce: DEFAULT_RESTORED_DEBOUNCE,
99 }
100 }
101}
102
103#[derive(Debug, Clone)]
105pub struct CloseRouteOptions {
106 pub drain: bool,
110 pub drain_timeout: Duration,
112 pub consumer_identity: Option<ConsumerIdentity>,
116 pub consumer_capabilities: Option<Vec<String>>,
121}
122
123impl Default for CloseRouteOptions {
124 fn default() -> Self {
125 Self {
126 drain: false,
127 drain_timeout: DEFAULT_CALL_TIMEOUT,
128 consumer_identity: None,
129 consumer_capabilities: None,
130 }
131 }
132}
133
134#[derive(Debug, Clone, PartialEq, Eq)]
136pub struct CallOptions {
137 pub timeout: Duration,
139 pub priority: Priority,
140 pub admission_class: AdmissionClass,
142 pub route_retry: RetryBackoff,
143 pub route_retry_deadline: Duration,
145 pub consumer_identity: Option<ConsumerIdentity>,
147 pub consumer_capabilities: Option<Vec<String>>,
152}
153
154impl Default for CallOptions {
155 fn default() -> Self {
156 Self {
157 timeout: DEFAULT_CALL_TIMEOUT,
158 priority: Priority::Interactive,
159 admission_class: AdmissionClass::Normal,
160 route_retry: RetryBackoff::default(),
161 route_retry_deadline: DEFAULT_ROUTE_RETRY_DEADLINE,
162 consumer_identity: None,
163 consumer_capabilities: None,
164 }
165 }
166}
167
168#[derive(Debug, Clone, PartialEq, Eq)]
170pub struct SubscribeOptions {
171 pub priority: Priority,
172 pub admission_class: AdmissionClass,
174 pub event_buffer: usize,
178 pub route_retry: RetryBackoff,
179 pub route_retry_deadline: Duration,
181 pub route_open_timeout: Duration,
184 pub consumer_identity: Option<ConsumerIdentity>,
186 pub consumer_capabilities: Option<Vec<String>>,
191}
192
193impl Default for SubscribeOptions {
194 fn default() -> Self {
195 Self {
196 priority: Priority::Interactive,
197 admission_class: AdmissionClass::Normal,
198 event_buffer: DEFAULT_SUBSCRIPTION_EVENT_BUFFER,
199 route_retry: RetryBackoff::default(),
200 route_retry_deadline: DEFAULT_ROUTE_RETRY_DEADLINE,
201 route_open_timeout: DEFAULT_CALL_TIMEOUT,
202 consumer_identity: None,
203 consumer_capabilities: None,
204 }
205 }
206}
207
208#[derive(Debug, Clone, PartialEq, Eq)]
211pub enum ConnectionState {
212 Dropped,
213 Restored { epoch: u64 },
214}
215
216#[derive(Debug, Clone, PartialEq, Eq)]
218pub struct RoutePollResult {
219 pub handle: RouteHandle,
220 pub status: Option<String>,
221 pub live: Option<bool>,
222}
223
224#[derive(Debug, Clone, serde::Deserialize, PartialEq)]
229pub struct CatalogList {
230 pub generation: u64,
231 #[serde(default)]
232 pub modules: Vec<CatalogEntry>,
233 #[serde(default)]
234 pub subc_ops: Vec<String>,
235}
236
237#[derive(Debug, Clone, PartialEq, Eq)]
239pub struct PushEvent {
240 pub handle: RouteHandle,
242 pub body: Vec<u8>,
244}
245
246pub struct SubcConsumer {
248 shared: Arc<Shared>,
249}
250
251pub struct Subscription {
259 events: mpsc::Receiver<Vec<u8>>,
260 closed: SubscriptionClosed,
261 cancel: SubscriptionCancel,
262}
263
264impl Subscription {
265 pub fn events(&mut self) -> &mut mpsc::Receiver<Vec<u8>> {
267 &mut self.events
268 }
269
270 pub fn closed(&mut self) -> &mut SubscriptionClosed {
277 &mut self.closed
278 }
279
280 pub fn unsubscribe(&self) -> Result<(), CallError> {
287 self.cancel.unsubscribe()
288 }
289}
290
291impl Drop for Subscription {
292 fn drop(&mut self) {
293 let _ = self.cancel.unsubscribe();
294 }
295}
296
297pub struct SubscriptionClosed {
299 rx: oneshot::Receiver<Result<(), CallError>>,
300}
301
302impl Unpin for SubscriptionClosed {}
303
304impl Future for SubscriptionClosed {
305 type Output = Result<(), CallError>;
306
307 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
308 let this = self.get_mut();
309 match Pin::new(&mut this.rx).poll(cx) {
310 Poll::Ready(Ok(result)) => Poll::Ready(result),
311 Poll::Ready(Err(_)) => Poll::Ready(Err(CallError::outcome_unknown(
312 "subscription closed result channel dropped",
313 ))),
314 Poll::Pending => Poll::Pending,
315 }
316 }
317}
318
319struct SubscriptionCancel {
320 shared: Arc<Shared>,
321 key: PendingKey,
322 priority: Priority,
323 cancelled: AtomicBool,
324}
325
326impl SubscriptionCancel {
327 fn new(shared: Arc<Shared>, key: PendingKey, priority: Priority) -> Self {
328 Self {
329 shared,
330 key,
331 priority,
332 cancelled: AtomicBool::new(false),
333 }
334 }
335
336 fn unsubscribe(&self) -> Result<(), CallError> {
337 let handle = RouteHandle::new(self.key.channel, self.key.epoch, self.key.generation);
338 self.shared.validate_current_handle(handle)?;
339 if self.cancelled.swap(true, Ordering::AcqRel) {
340 return Ok(());
341 }
342 self.shared
343 .unsubscribe_subscription(self.key, self.priority)
344 }
345}
346
347impl SubcConsumer {
348 pub async fn connect(
350 connection_file: &Path,
351 opts: ConsumerOptions,
352 ) -> Result<Self, ConsumerError> {
353 let opened = open_connection(connection_file, opts.handshake_timeout).await?;
354 let shared = Arc::new(Shared::new(connection_file.to_path_buf(), opts));
355 shared.install_initial(opened)?;
356 Ok(Self { shared })
357 }
358
359 pub async fn open_route(
361 &self,
362 target: RouteTarget,
363 identity: BindIdentity,
364 opts: CallOptions,
365 ) -> Result<RouteHandle, CallError> {
366 let deadline = Instant::now() + opts.timeout;
367 let consumer_identity = route_open_consumer_identity(&opts);
368 let consumer_capabilities = route_open_consumer_capabilities(&opts);
369 let key = RouteKey::new(
370 &target,
371 &identity,
372 consumer_identity.as_ref(),
373 consumer_capabilities.as_deref(),
374 );
375 let params = RouteOpenParams {
376 target: &target,
377 identity: &identity,
378 consumer_identity: &consumer_identity,
379 consumer_capabilities: &consumer_capabilities,
380 };
381 self.shared
382 .ensure_route(&key, ¶ms, &opts, deadline)
383 .await
384 .map(|route| route.handle)
385 }
386
387 pub async fn open_route_with_admission_facts(
393 &self,
394 target: RouteTarget,
395 identity: BindIdentity,
396 facts: serde_json::Value,
397 ) -> Result<RouteHandle, CallError> {
398 let deadline = Instant::now() + self.shared.opts.call_timeout;
399 let opts = CallOptions::default();
400 let body = serde_json::to_vec(&ClientControlRequest::RouteOpen {
401 target,
402 identity,
403 consumer_identity: route_open_consumer_identity(&opts),
404 consumer_capabilities: None,
405 admission_facts: Some(facts),
406 })
407 .map_err(|err| CallError::not_sent(format!("failed to encode route.open: {err}")))?;
408
409 let terminal = self.shared.control_call(body, deadline, true).await?;
410 let TerminalFrame::Response {
411 generation, body, ..
412 } = terminal
413 else {
414 return Err(CallError::not_sent(
415 "route.open returned a non-response frame",
416 ));
417 };
418 let ClientControlResponse::RouteOpen {
419 route_channel,
420 route_epoch,
421 } = serde_json::from_slice(&body).map_err(|err| {
422 CallError::not_sent(format!("failed to decode route.open response: {err}"))
423 })?
424 else {
425 return Err(CallError::not_sent(
426 "route.open returned an unexpected control response",
427 ));
428 };
429 let route = RouteState {
430 handle: RouteHandle::new(route_channel, route_epoch, generation),
431 sem: Arc::new(Semaphore::new(DEFAULT_ROUTE_WINDOW)),
432 };
433 self.shared.install_one_shot_route(route.clone())?;
434 Ok(route.handle)
435 }
436
437 pub async fn catalog_list(&self) -> Result<CatalogList, CallError> {
439 let deadline = Instant::now() + self.shared.opts.call_timeout;
440 let body = serde_json::to_vec(&serde_json::json!({
441 "op": subc_control::ops::CATALOG_LIST,
442 }))
443 .map_err(|err| CallError::not_sent(format!("failed to encode catalog.list: {err}")))?;
444
445 loop {
446 match self
447 .shared
448 .control_call(body.clone(), deadline, false)
449 .await
450 {
451 Ok(TerminalFrame::Response { body, .. }) => {
452 let response =
453 serde_json::from_slice::<ClientControlResponse>(&body).map_err(|err| {
454 CallError::not_sent(format!(
455 "failed to decode catalog.list response: {err}"
456 ))
457 })?;
458 let ClientControlResponse::CatalogList {
459 generation,
460 modules,
461 subc_ops,
462 } = response
463 else {
464 return Err(CallError::not_sent(
465 "catalog.list returned an unexpected control response",
466 ));
467 };
468 return Ok(CatalogList {
469 generation,
470 modules,
471 subc_ops,
472 });
473 }
474 Ok(TerminalFrame::Error { body }) => return Err(CallError::Module(body)),
475 Ok(TerminalFrame::StreamEnd) => {
476 return Err(CallError::not_sent("catalog.list returned StreamEnd"));
477 }
478 Err(err)
479 if is_retryable_catalog_transport_error(&err) && Instant::now() < deadline =>
480 {
481 continue;
482 }
483 Err(err) => return Err(err),
484 }
485 }
486 }
487
488 pub async fn request(
490 &self,
491 handle: &RouteHandle,
492 body: Vec<u8>,
493 opts: CallOptions,
494 ) -> Result<Vec<u8>, CallError> {
495 let deadline = Instant::now() + opts.timeout;
496 let route = self.shared.route_state(*handle)?;
497 let permit = match timeout_at(deadline, Arc::clone(&route.sem).acquire_owned()).await {
498 Ok(Ok(permit)) => permit,
499 Ok(Err(_)) => return Err(CallError::StaleRouteHandle(*handle)),
500 Err(_) => {
501 return Err(CallError::not_sent(
502 "call deadline elapsed waiting for route flow-control",
503 ))
504 }
505 };
506 let result = self
507 .shared
508 .send_request(RequestSend {
509 expected_handle: Some(*handle),
510 channel: handle.channel,
511 epoch: handle.epoch,
512 body,
513 priority: opts.priority,
514 admission_class: opts.admission_class,
515 deadline,
516 retain_late_route_open: false,
517 })
518 .await;
519 drop(permit);
520 match result? {
521 TerminalFrame::Response { body, .. } => Ok(body),
522 TerminalFrame::StreamEnd => Ok(Vec::new()),
523 TerminalFrame::Error { body } => Err(CallError::Module(body)),
524 }
525 }
526
527 pub async fn subscribe_route(
529 &self,
530 handle: &RouteHandle,
531 body: Vec<u8>,
532 opts: SubscribeOptions,
533 ) -> Result<Subscription, CallError> {
534 let deadline = Instant::now() + opts.route_open_timeout;
535 let route = self.shared.route_state(*handle)?;
536 let permit = match timeout_at(deadline, Arc::clone(&route.sem).acquire_owned()).await {
537 Ok(Ok(permit)) => permit,
538 Ok(Err(_)) => return Err(CallError::StaleRouteHandle(*handle)),
539 Err(_) => {
540 return Err(CallError::not_sent(
541 "subscription deadline elapsed waiting for route flow-control",
542 ))
543 }
544 };
545 self.shared
546 .send_subscription(SubscriptionSend {
547 expected_handle: Some(*handle),
548 channel: handle.channel,
549 epoch: handle.epoch,
550 body,
551 priority: opts.priority,
552 admission_class: opts.admission_class,
553 event_buffer: opts.event_buffer,
554 deadline,
555 permit,
556 })
557 .await
558 }
559
560 pub async fn poll_route(
562 &self,
563 handle: &RouteHandle,
564 kind: PollKind,
565 timeout: Duration,
566 ) -> Result<RoutePollResult, CallError> {
567 let deadline = Instant::now() + timeout;
568 let body = serde_json::to_vec(&ClientControlRequest::RoutePoll {
569 route_channel: handle.channel,
570 route_epoch: handle.epoch,
571 kind,
572 })
573 .map_err(|err| CallError::not_sent(format!("failed to encode route.poll: {err}")))?;
574 let terminal = self
575 .shared
576 .send_request(RequestSend {
577 expected_handle: Some(*handle),
578 channel: 0,
579 epoch: 0,
580 body,
581 priority: Priority::Interactive,
582 admission_class: AdmissionClass::Normal,
583 deadline,
584 retain_late_route_open: false,
585 })
586 .await?;
587 let TerminalFrame::Response { body, .. } = terminal else {
588 return Err(CallError::not_sent(
589 "route.poll returned a non-response frame",
590 ));
591 };
592 let ClientControlResponse::RoutePoll {
593 route_channel,
594 route_epoch,
595 status,
596 live,
597 } = serde_json::from_slice(&body)
598 .map_err(|err| CallError::not_sent(format!("failed to decode route.poll: {err}")))?
599 else {
600 return Err(CallError::not_sent(
601 "route.poll returned an unexpected control response",
602 ));
603 };
604 if route_channel != handle.channel || route_epoch != handle.epoch {
605 return Err(CallError::not_sent(
606 "route.poll response echoed a different route handle",
607 ));
608 }
609 self.shared.validate_current_handle(*handle)?;
610 Ok(RoutePollResult {
611 handle: *handle,
612 status,
613 live,
614 })
615 }
616
617 pub fn dropped_route_frames(&self) -> u64 {
619 self.shared.lock_inner().dropped_route_frames
620 }
621
622 pub fn push_events(
628 &self,
629 handle: &RouteHandle,
630 ) -> Result<mpsc::Receiver<PushEvent>, CallError> {
631 self.shared.register_push_events(*handle)
632 }
633
634 pub fn pushes_dropped_no_receiver(&self) -> u64 {
640 self.shared
641 .pushes_dropped_no_receiver
642 .load(Ordering::Relaxed)
643 }
644
645 pub async fn call(
648 &self,
649 target: RouteTarget,
650 identity: BindIdentity,
651 body: Vec<u8>,
652 opts: CallOptions,
653 ) -> Result<Vec<u8>, CallError> {
654 let call_deadline = Instant::now() + opts.timeout;
655 let mut retried_unknown_channel = false;
656 let consumer_identity = route_open_consumer_identity(&opts);
657 let consumer_capabilities = route_open_consumer_capabilities(&opts);
658 let route_key = RouteKey::new(
659 &target,
660 &identity,
661 consumer_identity.as_ref(),
662 consumer_capabilities.as_deref(),
663 );
664
665 let route_open = RouteOpenParams {
666 target: &target,
667 identity: &identity,
668 consumer_identity: &consumer_identity,
669 consumer_capabilities: &consumer_capabilities,
670 };
671
672 loop {
673 let route = self
674 .shared
675 .ensure_route(&route_key, &route_open, &opts, call_deadline)
676 .await?;
677 let permit =
678 match timeout_at(call_deadline, Arc::clone(&route.sem).acquire_owned()).await {
679 Ok(Ok(permit)) => permit,
680 Ok(Err(_)) => {
681 return Err(CallError::not_sent("route flow-control semaphore closed"));
682 }
683 Err(_) => {
684 return Err(CallError::not_sent(
685 "call deadline elapsed waiting for route flow-control",
686 ));
687 }
688 };
689
690 if !self.shared.route_is_current(&route_key, &route) {
691 drop(permit);
692 self.shared
693 .sleep_until_retry(call_deadline, opts.route_retry.base)
694 .await?;
695 continue;
696 }
697
698 let response = self
699 .shared
700 .send_request(RequestSend {
701 expected_handle: Some(route.handle),
702 channel: route.handle.channel,
703 epoch: route.handle.epoch,
704 body: body.clone(),
705 priority: opts.priority,
706 admission_class: opts.admission_class,
707 deadline: call_deadline,
708 retain_late_route_open: false,
709 })
710 .await;
711 drop(permit);
712
713 match response {
714 Ok(TerminalFrame::Response { body, .. }) => return Ok(body),
715 Ok(TerminalFrame::StreamEnd) => return Ok(Vec::new()),
716 Ok(TerminalFrame::Error { body, .. })
723 if body.code == "unknown_channel"
724 && !retried_unknown_channel
725 && Instant::now() < call_deadline =>
726 {
727 retried_unknown_channel = true;
728 self.shared.invalidate_route(&route_key, Some(route.handle));
729 continue;
730 }
731 Ok(TerminalFrame::Error { body, .. }) => return Err(CallError::Module(body)),
732 Err(err) if err.is_not_sent() && Instant::now() < call_deadline => {
733 self.shared.invalidate_route(&route_key, Some(route.handle));
734 self.shared.ensure_connected_for_call(call_deadline).await?;
735 continue;
736 }
737 Err(err) => return Err(err),
738 }
739 }
740 }
741
742 pub async fn subscribe(
753 &self,
754 target: RouteTarget,
755 identity: BindIdentity,
756 body: Vec<u8>,
757 opts: SubscribeOptions,
758 ) -> Result<Subscription, CallError> {
759 let open_deadline = Instant::now() + opts.route_open_timeout;
760 let route_opts = CallOptions {
761 timeout: opts.route_open_timeout,
762 priority: opts.priority,
763 admission_class: opts.admission_class,
764 route_retry: opts.route_retry,
765 route_retry_deadline: opts.route_retry_deadline,
766 consumer_identity: opts.consumer_identity.clone(),
767 consumer_capabilities: opts.consumer_capabilities.clone(),
768 };
769 let consumer_identity = route_open_consumer_identity(&route_opts);
770 let consumer_capabilities = route_open_consumer_capabilities(&route_opts);
771 let route_key = RouteKey::new(
772 &target,
773 &identity,
774 consumer_identity.as_ref(),
775 consumer_capabilities.as_deref(),
776 );
777
778 let route_open = RouteOpenParams {
779 target: &target,
780 identity: &identity,
781 consumer_identity: &consumer_identity,
782 consumer_capabilities: &consumer_capabilities,
783 };
784
785 loop {
786 let route = self
787 .shared
788 .ensure_route(&route_key, &route_open, &route_opts, open_deadline)
789 .await?;
790 let permit =
791 match timeout_at(open_deadline, Arc::clone(&route.sem).acquire_owned()).await {
792 Ok(Ok(permit)) => permit,
793 Ok(Err(_)) => {
794 return Err(CallError::not_sent("route flow-control semaphore closed"));
795 }
796 Err(_) => {
797 return Err(CallError::not_sent(
798 "subscription open deadline elapsed waiting for route flow-control",
799 ));
800 }
801 };
802
803 if !self.shared.route_is_current(&route_key, &route) {
804 drop(permit);
805 self.shared
806 .sleep_until_retry(open_deadline, opts.route_retry.base)
807 .await?;
808 continue;
809 }
810
811 match self
812 .shared
813 .send_subscription(SubscriptionSend {
814 expected_handle: Some(route.handle),
815 channel: route.handle.channel,
816 epoch: route.handle.epoch,
817 body: body.clone(),
818 priority: opts.priority,
819 admission_class: opts.admission_class,
820 event_buffer: opts.event_buffer,
821 deadline: open_deadline,
822 permit,
823 })
824 .await
825 {
826 Ok(subscription) => return Ok(subscription),
827 Err(err) if err.is_not_sent() && Instant::now() < open_deadline => {
828 self.shared.invalidate_route(&route_key, Some(route.handle));
829 self.shared.ensure_connected_for_call(open_deadline).await?;
830 continue;
831 }
832 Err(err) => return Err(err),
833 }
834 }
835 }
836
837 pub async fn close_route(
849 &self,
850 target: RouteTarget,
851 identity: BindIdentity,
852 opts: CloseRouteOptions,
853 ) {
854 let consumer_identity = close_route_consumer_identity(&opts);
855 let consumer_capabilities = close_route_consumer_capabilities(&opts);
856 let key = RouteKey::new(
857 &target,
858 &identity,
859 consumer_identity.as_ref(),
860 consumer_capabilities.as_deref(),
861 );
862 self.shared.close_route(&key, &opts).await;
863 }
864
865 pub async fn close_handle(
867 &self,
868 handle: &RouteHandle,
869 opts: CloseRouteOptions,
870 ) -> Result<(), CallError> {
871 self.shared.close_handle(*handle, &opts).await
872 }
873
874 pub fn current_epoch(&self) -> u64 {
876 self.shared.lock_inner().epoch
877 }
878
879 pub fn on_connection_state(&self, cb: impl Fn(ConnectionState) + Send + 'static) {
881 self.shared
882 .lock_inner()
883 .callbacks
884 .push(Arc::new(Mutex::new(Box::new(cb))));
885 }
886
887 pub async fn close(&self) {
889 self.shared.close_sync("consumer closed");
890 tokio::task::yield_now().await;
891 }
892}
893
894impl Drop for SubcConsumer {
895 fn drop(&mut self) {
896 self.shared.close_sync("consumer dropped");
897 }
898}
899
900#[derive(Debug)]
902pub enum ConsumerError {
903 ConnectionFile {
904 path: PathBuf,
905 source: ConnectionFileError,
906 },
907 NoEndpoint {
908 path: PathBuf,
909 },
910 Connect {
911 path: PathBuf,
912 endpoint: String,
913 source: io::Error,
914 },
915 Auth {
916 path: PathBuf,
917 endpoint: String,
918 source: AuthError,
919 },
920 Closed,
921}
922
923impl fmt::Display for ConsumerError {
924 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
925 match self {
926 Self::ConnectionFile { path, source } => write!(
927 f,
928 "failed to read subc connection file '{}': {source}",
929 path.display()
930 ),
931 Self::NoEndpoint { path } => {
932 write!(
933 f,
934 "subc connection file '{}' has no endpoints",
935 path.display()
936 )
937 }
938 Self::Connect {
939 path,
940 endpoint,
941 source,
942 } => write!(
943 f,
944 "failed to connect to subc endpoint {endpoint} from '{}': {source}",
945 path.display()
946 ),
947 Self::Auth {
948 path,
949 endpoint,
950 source,
951 } => write!(
952 f,
953 "failed to authenticate to subc endpoint {endpoint} from '{}': {source}",
954 path.display()
955 ),
956 Self::Closed => write!(f, "consumer closed"),
957 }
958 }
959}
960
961impl Error for ConsumerError {
962 fn source(&self) -> Option<&(dyn Error + 'static)> {
963 match self {
964 Self::ConnectionFile { source, .. } => Some(source),
965 Self::Connect { source, .. } => Some(source),
966 Self::Auth { source, .. } => Some(source),
967 Self::NoEndpoint { .. } | Self::Closed => None,
968 }
969 }
970}
971
972#[derive(Debug)]
974pub enum CallError {
975 NotSent(Box<dyn Error + Send + Sync>),
977 OutcomeUnknown(Box<dyn Error + Send + Sync>),
979 Module(ErrorBody),
982 SubscriptionBackpressure(Box<dyn Error + Send + Sync>),
987 StaleRouteHandle(RouteHandle),
989}
990
991impl CallError {
992 fn not_sent(reason: impl Into<String>) -> Self {
993 Self::NotSent(Box::new(SimpleError(reason.into())))
994 }
995
996 fn outcome_unknown(reason: impl Into<String>) -> Self {
997 Self::OutcomeUnknown(Box::new(SimpleError(reason.into())))
998 }
999
1000 fn is_not_sent(&self) -> bool {
1001 matches!(self, Self::NotSent(_))
1002 }
1003
1004 fn subscription_backpressure(reason: impl Into<String>) -> Self {
1005 Self::SubscriptionBackpressure(Box::new(SimpleError(reason.into())))
1006 }
1007}
1008
1009impl fmt::Display for CallError {
1010 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1011 match self {
1012 Self::NotSent(err) => write!(f, "request not sent: {err}"),
1013 Self::OutcomeUnknown(err) => write!(f, "request outcome unknown: {err}"),
1014 Self::Module(body) => write!(f, "module error {}: {}", body.code, body.message),
1015 Self::SubscriptionBackpressure(err) => {
1016 write!(f, "subscription event channel backpressure: {err}")
1017 }
1018 Self::StaleRouteHandle(handle) => write!(f, "stale route handle: {handle:?}"),
1019 }
1020 }
1021}
1022
1023impl Error for CallError {
1024 fn source(&self) -> Option<&(dyn Error + 'static)> {
1025 match self {
1026 Self::NotSent(err)
1027 | Self::OutcomeUnknown(err)
1028 | Self::SubscriptionBackpressure(err) => Some(err.as_ref()),
1029 Self::Module(_) | Self::StaleRouteHandle(_) => None,
1030 }
1031 }
1032}
1033
1034#[derive(Debug)]
1035struct SimpleError(String);
1036
1037impl fmt::Display for SimpleError {
1038 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1039 f.write_str(&self.0)
1040 }
1041}
1042
1043impl Error for SimpleError {}
1044
1045type Callback = Arc<Mutex<Box<dyn Fn(ConnectionState) + Send + 'static>>>;
1046
1047type OpeningWaiter = oneshot::Sender<Result<RouteState, SharedCallFailure>>;
1048
1049struct Opening {
1055 waiters: Vec<OpeningWaiter>,
1056 closed: bool,
1057}
1058
1059struct Shared {
1060 connection_file: PathBuf,
1061 opts: ConsumerOptions,
1062 inner: Mutex<Inner>,
1063 notify: Notify,
1064 close_token: CancellationToken,
1065 pushes_dropped_no_receiver: AtomicU64,
1066}
1067
1068struct Inner {
1069 generation: u64,
1070 epoch: u64,
1071 next_corr: Option<u64>,
1072 writer: Option<mpsc::Sender<WriteCommand>>,
1073 pending: HashMap<PendingKey, PendingEntry>,
1074 routes: HashMap<RouteKey, RouteState>,
1075 route_by_channel: HashMap<u16, RouteKey>,
1076 one_shot_routes: HashMap<u16, RouteState>,
1077 route_epochs: HashMap<u16, RouteHandle>,
1078 push_event_receivers: HashMap<RouteHandle, mpsc::Sender<PushEvent>>,
1079 dropped_route_frames: u64,
1080 openings: HashMap<RouteKey, Opening>,
1081 callbacks: Vec<Callback>,
1082 closed: bool,
1083 reconnect: ReconnectState,
1084 restored_token: u64,
1085 reader_task: Option<JoinHandle<()>>,
1086 writer_task: Option<JoinHandle<()>>,
1087}
1088
1089impl Inner {
1090 fn cache_route(&mut self, key: RouteKey, route: RouteState) -> RouteState {
1091 let cached = self.routes.entry(key.clone()).or_insert(route).clone();
1092 let previous = self
1093 .route_by_channel
1094 .insert(cached.handle.channel, key.clone());
1095 debug_assert!(previous.as_ref().is_none_or(|previous| previous == &key));
1096 self.route_epochs
1097 .insert(cached.handle.channel, cached.handle);
1098 cached
1099 }
1100
1101 fn remove_route(&mut self, key: &RouteKey) -> Option<RouteState> {
1102 let route = self.routes.remove(key)?;
1103 let indexed = self.route_by_channel.remove(&route.handle.channel);
1104 debug_assert_eq!(indexed.as_ref(), Some(key));
1105 Some(route)
1106 }
1107
1108 fn remove_route_by_handle(&mut self, handle: RouteHandle) -> Option<RouteState> {
1109 if let Some(key) = self.route_by_channel.get(&handle.channel).cloned() {
1110 let matches = self
1111 .routes
1112 .get(&key)
1113 .is_some_and(|route| route.handle == handle);
1114 debug_assert!(matches);
1115 if matches {
1116 return self.remove_route(&key);
1117 }
1118 }
1119 self.one_shot_routes
1120 .get(&handle.channel)
1121 .is_some_and(|route| route.handle == handle)
1122 .then(|| self.one_shot_routes.remove(&handle.channel))
1123 .flatten()
1124 }
1125
1126 fn drain_routes(&mut self) -> Vec<RouteState> {
1127 self.route_by_channel.clear();
1128 self.routes
1129 .drain()
1130 .map(|(_, route)| route)
1131 .chain(self.one_shot_routes.drain().map(|(_, route)| route))
1132 .collect()
1133 }
1134
1135 fn close_routes(&mut self) {
1136 self.push_event_receivers.clear();
1137 for route in self.drain_routes() {
1138 route.sem.close();
1139 }
1140 }
1141}
1142
1143impl Shared {
1144 fn new(connection_file: PathBuf, opts: ConsumerOptions) -> Self {
1145 Self {
1146 connection_file,
1147 opts,
1148 inner: Mutex::new(Inner {
1149 generation: 1,
1150 epoch: 1,
1151 next_corr: Some(1),
1152 writer: None,
1153 pending: HashMap::new(),
1154 routes: HashMap::new(),
1155 route_by_channel: HashMap::new(),
1156 one_shot_routes: HashMap::new(),
1157 route_epochs: HashMap::new(),
1158 push_event_receivers: HashMap::new(),
1159 dropped_route_frames: 0,
1160 openings: HashMap::new(),
1161 callbacks: Vec::new(),
1162 closed: false,
1163 reconnect: ReconnectState::Idle,
1164 restored_token: 0,
1165 reader_task: None,
1166 writer_task: None,
1167 }),
1168 notify: Notify::new(),
1169 close_token: CancellationToken::new(),
1170 pushes_dropped_no_receiver: AtomicU64::new(0),
1171 }
1172 }
1173
1174 fn lock_inner(&self) -> MutexGuard<'_, Inner> {
1175 self.inner
1176 .lock()
1177 .unwrap_or_else(|poisoned| poisoned.into_inner())
1178 }
1179
1180 fn install_initial(self: &Arc<Self>, opened: OpenedConnection) -> Result<(), ConsumerError> {
1181 self.install_connection(opened, InstallKind::Initial)
1182 .map(|_| ())
1183 }
1184
1185 fn install_reconnected(
1186 self: &Arc<Self>,
1187 opened: OpenedConnection,
1188 ) -> Result<(u64, u64), ConsumerError> {
1189 let generation_epoch = self.install_connection(opened, InstallKind::Reconnect)?;
1190 self.notify.notify_waiters();
1191 Ok(generation_epoch)
1192 }
1193
1194 fn install_connection(
1195 self: &Arc<Self>,
1196 opened: OpenedConnection,
1197 kind: InstallKind,
1198 ) -> Result<(u64, u64), ConsumerError> {
1199 if self.close_token.is_cancelled() {
1200 return Err(ConsumerError::Closed);
1201 }
1202
1203 let (reader, writer) = opened.stream.into_split();
1204 let (tx, rx) = mpsc::channel(EGRESS_BUFFER);
1205 let (generation, epoch, old_reader, old_writer) = {
1206 let mut inner = self.lock_inner();
1207 if inner.closed {
1208 return Err(ConsumerError::Closed);
1209 }
1210 let (generation, epoch) = match kind {
1211 InstallKind::Initial => (inner.generation, inner.epoch),
1212 InstallKind::Reconnect => {
1213 inner.generation = inner
1214 .generation
1215 .checked_add(1)
1216 .ok_or(ConsumerError::Closed)?;
1217 inner.epoch = inner.epoch.checked_add(1).ok_or(ConsumerError::Closed)?;
1218 (inner.generation, inner.epoch)
1219 }
1220 };
1221 inner.close_routes();
1222 inner.route_epochs.clear();
1223 inner.next_corr = Some(1);
1224 inner.writer = Some(tx);
1225 (
1226 generation,
1227 epoch,
1228 inner.reader_task.take(),
1229 inner.writer_task.take(),
1230 )
1231 };
1232
1233 if let Some(handle) = old_reader {
1234 handle.abort();
1235 }
1236 if let Some(handle) = old_writer {
1237 handle.abort();
1238 }
1239
1240 let reader_shared = Arc::clone(self);
1241 let reader_task = tokio::spawn(async move {
1242 reader_loop(reader_shared, reader, generation).await;
1243 });
1244 let writer_shared = Arc::clone(self);
1245 let writer_task = tokio::spawn(async move {
1246 writer_loop(writer_shared, writer, rx, generation).await;
1247 });
1248
1249 {
1250 let mut inner = self.lock_inner();
1251 if inner.closed || inner.generation != generation {
1252 reader_task.abort();
1253 writer_task.abort();
1254 return Err(ConsumerError::Closed);
1255 }
1256 inner.reader_task = Some(reader_task);
1257 inner.writer_task = Some(writer_task);
1258 }
1259
1260 Ok((generation, epoch))
1261 }
1262
1263 async fn ensure_connected_for_call(
1264 self: &Arc<Self>,
1265 deadline: Instant,
1266 ) -> Result<(), CallError> {
1267 loop {
1268 if Instant::now() >= deadline {
1269 return Err(CallError::not_sent(
1270 "call deadline elapsed waiting for reconnection",
1271 ));
1272 }
1273 let action = {
1274 let mut inner = self.lock_inner();
1275 if inner.closed {
1276 return Err(CallError::not_sent("consumer closed"));
1277 }
1278 if inner.writer.is_some() {
1279 return Ok(());
1280 }
1281 let generation = inner.generation;
1282 let reconnect_is_live = match &inner.reconnect {
1283 ReconnectState::Idle => false,
1284 ReconnectState::Inline { generation: active } => *active == generation,
1285 ReconnectState::Background {
1286 generation: active,
1287 task,
1288 } => *active == generation && !task.is_finished(),
1289 };
1290 if reconnect_is_live {
1291 EnsureAction::Wait
1292 } else {
1293 let stale_task =
1294 match std::mem::replace(&mut inner.reconnect, ReconnectState::Idle) {
1295 ReconnectState::Background { task, .. } => Some(task),
1296 ReconnectState::Idle | ReconnectState::Inline { .. } => None,
1297 };
1298 inner.reconnect = ReconnectState::Inline { generation };
1299 EnsureAction::Lead {
1300 generation,
1301 stale_task,
1302 }
1303 }
1304 };
1305
1306 match action {
1307 EnsureAction::Wait => {
1308 timeout_at(deadline, self.notify.notified())
1309 .await
1310 .map_err(|_| {
1311 CallError::not_sent("call deadline elapsed waiting for reconnection")
1312 })?;
1313 }
1314 EnsureAction::Lead {
1315 generation,
1316 stale_task,
1317 } => {
1318 if let Some(handle) = stale_task {
1319 handle.abort();
1320 }
1321 let mut guard = InlineReconnectGuard::new(Arc::clone(self), generation);
1322 let result = timeout_at(deadline, self.reconnect_with_retry(generation)).await;
1323 guard.finish();
1324 return match result {
1325 Ok(Ok(())) => Ok(()),
1326 Ok(Err(err)) => Err(CallError::not_sent(err.to_string())),
1327 Err(_) => Err(CallError::not_sent(
1328 "call deadline elapsed waiting for reconnection",
1329 )),
1330 };
1331 }
1332 }
1333 }
1334 }
1335
1336 fn spawn_reconnect(self: &Arc<Self>, generation: u64) -> bool {
1337 let stale_task = {
1341 let mut inner = self.lock_inner();
1342 if inner.closed || inner.writer.is_some() || inner.generation != generation {
1343 return false;
1344 }
1345 let should_spawn = match &inner.reconnect {
1346 ReconnectState::Idle => true,
1347 ReconnectState::Inline { generation: active } => *active < generation,
1348 ReconnectState::Background {
1349 generation: active,
1350 task,
1351 } => *active < generation || (*active == generation && task.is_finished()),
1352 };
1353 if !should_spawn {
1354 return false;
1355 }
1356
1357 let stale_task = match std::mem::replace(&mut inner.reconnect, ReconnectState::Idle) {
1358 ReconnectState::Background { task, .. } => Some(task),
1359 ReconnectState::Idle | ReconnectState::Inline { .. } => None,
1360 };
1361 let shared = Arc::clone(self);
1362 let handle = tokio::spawn(async move {
1363 let _result = shared.reconnect_with_retry(generation).await;
1364 shared.finish_background_reconnect(generation);
1365 });
1366 inner.reconnect = ReconnectState::Background {
1367 generation,
1368 task: handle,
1369 };
1370 stale_task
1371 };
1372 if let Some(handle) = stale_task {
1373 handle.abort();
1374 }
1375 true
1376 }
1377
1378 async fn reconnect_with_retry(
1379 self: &Arc<Self>,
1380 reconnect_generation: u64,
1381 ) -> Result<(), ConsumerError> {
1382 let mut last_error: Option<ConsumerError> = None;
1383 for attempt in 1..=self.opts.reconnect_backoff.max_attempts {
1384 if self.close_token.is_cancelled() {
1385 return Err(ConsumerError::Closed);
1386 }
1387 if !self.reconnect_attempt_is_current(reconnect_generation) {
1388 return Ok(());
1389 }
1390
1391 match open_connection(&self.connection_file, self.opts.handshake_timeout).await {
1392 Ok(opened) => {
1393 if !self.reconnect_attempt_is_current(reconnect_generation) {
1396 return Ok(());
1397 }
1398 let (generation, epoch) = self.install_reconnected(opened)?;
1399 self.schedule_restored(generation, epoch);
1400 return Ok(());
1401 }
1402 Err(err) => {
1403 let transient = is_reconnect_transient(&err);
1404 last_error = Some(err);
1405 if !transient || attempt >= self.opts.reconnect_backoff.max_attempts {
1406 break;
1407 }
1408 let delay = self.opts.reconnect_backoff.delay_after_attempt(attempt);
1409 tokio::select! {
1410 () = self.close_token.cancelled() => return Err(ConsumerError::Closed),
1411 () = sleep(delay) => {}
1412 }
1413 }
1414 }
1415 }
1416 Err(last_error.unwrap_or(ConsumerError::Closed))
1417 }
1418
1419 fn reconnect_attempt_is_current(&self, generation: u64) -> bool {
1420 let inner = self.lock_inner();
1421 !inner.closed
1422 && inner.writer.is_none()
1423 && inner.generation == generation
1424 && matches!(
1425 &inner.reconnect,
1426 ReconnectState::Inline { generation: active }
1427 | ReconnectState::Background {
1428 generation: active,
1429 ..
1430 } if *active == generation
1431 )
1432 }
1433
1434 fn finish_inline_reconnect(&self, generation: u64) {
1435 let finished = {
1436 let mut inner = self.lock_inner();
1437 if matches!(
1438 &inner.reconnect,
1439 ReconnectState::Inline { generation: active } if *active == generation
1440 ) {
1441 inner.reconnect = ReconnectState::Idle;
1442 true
1443 } else {
1444 false
1445 }
1446 };
1447 if finished {
1448 self.notify.notify_waiters();
1449 }
1450 }
1451
1452 fn finish_background_reconnect(&self, generation: u64) {
1453 let completed_task = {
1454 let mut inner = self.lock_inner();
1455 if !matches!(
1456 &inner.reconnect,
1457 ReconnectState::Background { generation: active, .. } if *active == generation
1458 ) {
1459 None
1460 } else {
1461 match std::mem::replace(&mut inner.reconnect, ReconnectState::Idle) {
1462 ReconnectState::Background { task, .. } => Some(task),
1463 ReconnectState::Idle | ReconnectState::Inline { .. } => unreachable!(),
1464 }
1465 }
1466 };
1467 if completed_task.is_some() {
1468 drop(completed_task);
1469 self.notify.notify_waiters();
1470 }
1471 }
1472
1473 fn schedule_restored(self: &Arc<Self>, generation: u64, epoch: u64) {
1474 let token = {
1475 let mut inner = self.lock_inner();
1476 inner.restored_token = inner.restored_token.saturating_add(1);
1477 inner.restored_token
1478 };
1479 let shared = Arc::clone(self);
1480 tokio::spawn(async move {
1481 tokio::select! {
1482 () = shared.close_token.cancelled() => {}
1483 () = sleep(shared.opts.restored_debounce) => {
1484 let should_emit = {
1485 let inner = shared.lock_inner();
1486 !inner.closed
1487 && inner.generation == generation
1488 && inner.epoch == epoch
1489 && inner.restored_token == token
1490 && inner.writer.is_some()
1491 };
1492 if should_emit {
1493 shared.emit_connection_state(ConnectionState::Restored { epoch });
1494 }
1495 }
1496 }
1497 });
1498 }
1499
1500 fn install_one_shot_route(&self, route: RouteState) -> Result<(), CallError> {
1501 let handle = route.handle;
1502 let mut inner = self.lock_inner();
1503 if inner.closed || inner.generation != handle.connection_token() || inner.writer.is_none() {
1504 return Err(CallError::StaleRouteHandle(handle));
1505 }
1506 if inner.route_epochs.contains_key(&handle.channel) {
1507 return Err(CallError::not_sent(
1508 "daemon returned a route channel already in use",
1509 ));
1510 }
1511 inner.one_shot_routes.insert(handle.channel, route);
1512 inner.route_epochs.insert(handle.channel, handle);
1513 Ok(())
1514 }
1515
1516 async fn ensure_route(
1517 self: &Arc<Self>,
1518 key: &RouteKey,
1519 route_open: &RouteOpenParams<'_>,
1520 opts: &CallOptions,
1521 call_deadline: Instant,
1522 ) -> Result<RouteState, CallError> {
1523 loop {
1524 let action = {
1525 let mut inner = self.lock_inner();
1526 if inner.closed {
1527 return Err(CallError::not_sent("consumer closed"));
1528 }
1529 if let Some(route) = inner.routes.get(key) {
1530 if route.handle.connection_token() == inner.generation && inner.writer.is_some()
1531 {
1532 return Ok(route.clone());
1533 }
1534 }
1535 if let Some(opening) = inner.openings.get_mut(key) {
1536 let (tx, rx) = oneshot::channel();
1537 opening.waiters.push(tx);
1538 RouteOpenAction::Wait(rx)
1539 } else {
1540 inner.openings.insert(
1541 key.clone(),
1542 Opening {
1543 waiters: Vec::new(),
1544 closed: false,
1545 },
1546 );
1547 RouteOpenAction::Lead
1548 }
1549 };
1550
1551 match action {
1552 RouteOpenAction::Wait(rx) => match timeout_at(call_deadline, rx).await {
1553 Ok(Ok(Ok(route))) => return Ok(route),
1554 Ok(Ok(Err(err))) => return Err(err.into_call_error()),
1555 Ok(Err(_)) => continue,
1556 Err(_) => {
1557 return Err(CallError::not_sent(
1558 "call deadline elapsed waiting for route.open",
1559 ));
1560 }
1561 },
1562 RouteOpenAction::Lead => {
1563 let mut guard = OpeningGuard::new(Arc::clone(self), key.clone());
1564 let result = self
1565 .open_route_with_retry(key, route_open, opts, call_deadline)
1566 .await
1567 .map_err(SharedCallFailure::from);
1568 guard.finish(result.clone());
1569 return result.map_err(SharedCallFailure::into_call_error);
1570 }
1571 }
1572 }
1573 }
1574
1575 async fn open_route_with_retry(
1576 self: &Arc<Self>,
1577 key: &RouteKey,
1578 route_open: &RouteOpenParams<'_>,
1579 opts: &CallOptions,
1580 call_deadline: Instant,
1581 ) -> Result<RouteState, CallError> {
1582 let route_deadline = (Instant::now() + opts.route_retry_deadline).min(call_deadline);
1583 let mut attempt = 0usize;
1584 loop {
1585 attempt = attempt.saturating_add(1);
1586 let body = serde_json::to_vec(&ClientControlRequest::RouteOpen {
1587 target: route_open.target.clone(),
1588 identity: route_open.identity.clone(),
1589 consumer_identity: route_open.consumer_identity.clone(),
1590 consumer_capabilities: route_open.consumer_capabilities.clone(),
1591 admission_facts: None,
1592 })
1593 .map_err(|err| CallError::not_sent(format!("failed to encode route.open: {err}")))?;
1594 match self.control_call(body, route_deadline, true).await {
1595 Ok(TerminalFrame::Response {
1596 generation, body, ..
1597 }) => {
1598 let response =
1599 serde_json::from_slice::<ClientControlResponse>(&body).map_err(|err| {
1600 CallError::not_sent(format!(
1601 "failed to decode route.open response: {err}"
1602 ))
1603 })?;
1604 let ClientControlResponse::RouteOpen {
1605 route_channel,
1606 route_epoch,
1607 } = response
1608 else {
1609 return Err(CallError::not_sent(
1610 "route.open returned an unexpected control response",
1611 ));
1612 };
1613 let route = RouteState {
1614 handle: RouteHandle::new(route_channel, route_epoch, generation),
1615 sem: Arc::new(Semaphore::new(DEFAULT_ROUTE_WINDOW)),
1616 };
1617 let install = {
1618 let mut inner = self.lock_inner();
1619 if inner.closed {
1620 return Err(CallError::not_sent("consumer closed"));
1621 }
1622 let closed_during_open = inner.openings.get(key).is_some_and(|o| o.closed);
1627 if closed_during_open
1628 || inner.generation != generation
1629 || inner.writer.is_none()
1630 {
1631 RouteInstall::Discard {
1632 closed: closed_during_open,
1633 }
1634 } else {
1635 let cached = inner.cache_route(key.clone(), route.clone());
1636 RouteInstall::Cached(cached)
1637 }
1638 };
1639 match install {
1640 RouteInstall::Cached(cached) => return Ok(cached),
1641 RouteInstall::Discard { closed } => {
1642 if closed {
1643 self.send_route_goodbye(route.handle, true);
1646 self.uninstall_route_handle(route.handle);
1647 return Err(CallError::not_sent(
1648 "route was closed before route.open completed",
1649 ));
1650 }
1651 }
1653 }
1654 self.sleep_until_retry(route_deadline, opts.route_retry.base)
1655 .await?;
1656 }
1657 Ok(TerminalFrame::Error { body, .. }) => {
1658 if is_retryable_route_open_code(&body.code)
1659 && attempt < opts.route_retry.max_attempts
1660 && Instant::now() < route_deadline
1661 {
1662 let delay = opts.route_retry.delay_after_attempt(attempt);
1663 self.sleep_until_retry(route_deadline, delay).await?;
1664 continue;
1665 }
1666 return Err(CallError::not_sent(format!(
1667 "route.open failed for target {}: {} ({})",
1668 key.target_label(),
1669 body.code,
1670 body.message
1671 )));
1672 }
1673 Ok(TerminalFrame::StreamEnd) => {
1674 return Err(CallError::not_sent("route.open returned StreamEnd"));
1675 }
1676 Err(err)
1677 if err.is_not_sent()
1678 && attempt < opts.route_retry.max_attempts
1679 && Instant::now() < route_deadline =>
1680 {
1681 let delay = opts.route_retry.delay_after_attempt(attempt);
1682 self.sleep_until_retry(route_deadline, delay).await?;
1683 }
1684 Err(err) => return Err(err),
1685 }
1686 }
1687
1688 #[allow(unreachable_code)]
1689 Err(CallError::not_sent(format!(
1690 "route.open retry deadline elapsed for target {}",
1691 key.target_label()
1692 )))
1693 }
1694
1695 async fn sleep_until_retry(&self, deadline: Instant, delay: Duration) -> Result<(), CallError> {
1696 if Instant::now() >= deadline {
1697 return Err(CallError::not_sent("retry deadline elapsed"));
1698 }
1699 let bounded = delay.min(deadline.saturating_duration_since(Instant::now()));
1700 tokio::select! {
1701 () = self.close_token.cancelled() => Err(CallError::not_sent("consumer closed")),
1702 () = sleep(bounded) => Ok(()),
1703 }
1704 }
1705
1706 async fn control_call(
1707 self: &Arc<Self>,
1708 body: Vec<u8>,
1709 deadline: Instant,
1710 retain_late_route_open: bool,
1711 ) -> Result<TerminalFrame, CallError> {
1712 self.ensure_connected_for_call(deadline).await?;
1713 self.send_request(RequestSend {
1714 expected_handle: None,
1715 channel: 0,
1716 epoch: 0,
1717 body,
1718 priority: Priority::Interactive,
1719 admission_class: AdmissionClass::Normal,
1720 deadline,
1721 retain_late_route_open,
1722 })
1723 .await
1724 }
1725
1726 async fn send_request(
1727 self: &Arc<Self>,
1728 request: RequestSend,
1729 ) -> Result<TerminalFrame, CallError> {
1730 let RequestSend {
1731 expected_handle,
1732 channel,
1733 epoch,
1734 body,
1735 priority,
1736 admission_class,
1737 deadline,
1738 retain_late_route_open,
1739 } = request;
1740 if Instant::now() >= deadline {
1741 return Err(CallError::not_sent(
1742 "call deadline elapsed before request was sent",
1743 ));
1744 }
1745 let (generation, corr, writer) = {
1746 let mut inner = self.lock_inner();
1747 if inner.closed {
1748 return Err(CallError::not_sent("consumer closed"));
1749 }
1750 let generation = inner.generation;
1751 if let Some(expected) = expected_handle {
1752 let route_pair_matches =
1753 channel == 0 || (expected.channel == channel && expected.epoch == epoch);
1754 if expected.connection_token() != generation
1755 || !route_pair_matches
1756 || inner.route_epochs.get(&expected.channel) != Some(&expected)
1757 {
1758 return Err(CallError::StaleRouteHandle(expected));
1759 }
1760 }
1761 let Some(writer) = inner.writer.clone() else {
1762 return Err(CallError::not_sent("subc connection is down before send"));
1763 };
1764 let Some(corr) = inner.next_corr else {
1765 drop(inner);
1766 self.handle_generation_drop(
1767 generation,
1768 "channel-0 correlation allocator exhausted".to_string(),
1769 );
1770 return Err(CallError::not_sent(
1771 "correlation allocator exhausted; connection closed",
1772 ));
1773 };
1774 inner.next_corr = corr.checked_add(1);
1775 (generation, corr, writer)
1776 };
1777
1778 let frame = Frame::build(
1779 FrameType::Request,
1780 Flags::new(false, priority, false).with_admission_class(admission_class),
1781 channel,
1782 epoch,
1783 corr,
1784 body,
1785 )
1786 .map_err(|err| CallError::not_sent(format!("failed to build request frame: {err}")))?;
1787 let key = PendingKey {
1788 generation,
1789 channel,
1790 epoch,
1791 corr,
1792 };
1793 let (tx, rx) = oneshot::channel();
1794 {
1795 let mut inner = self.lock_inner();
1796 if inner.closed || inner.generation != generation || inner.writer.is_none() {
1797 return Err(CallError::not_sent(
1798 "connection changed before request registration",
1799 ));
1800 }
1801 if let Some(expected) = expected_handle {
1802 if inner.route_epochs.get(&expected.channel) != Some(&expected) {
1803 return Err(CallError::StaleRouteHandle(expected));
1804 }
1805 }
1806 let expected_control_handle = (channel == 0 && !retain_late_route_open)
1807 .then_some(expected_handle)
1808 .flatten();
1809 inner.pending.insert(
1810 key,
1811 PendingEntry::unary(tx, retain_late_route_open, expected_control_handle),
1812 );
1813 }
1814 let mut registration =
1815 PendingRegistration::new(Arc::clone(self), key, retain_late_route_open);
1816
1817 match timeout_at(
1818 deadline,
1819 writer.send(WriteCommand {
1820 frame,
1821 pending: Some(key),
1822 }),
1823 )
1824 .await
1825 {
1826 Ok(Ok(())) => {}
1827 Ok(Err(_)) => {
1828 let accepted = registration.remove_pending().unwrap_or(false);
1829 return Err(classify_failure(
1830 accepted,
1831 "writer task closed before accepting request",
1832 ));
1833 }
1834 Err(_) => {
1835 let _ = registration.remove_pending();
1836 return Err(CallError::not_sent(
1837 "call deadline elapsed waiting for writer capacity",
1838 ));
1839 }
1840 }
1841
1842 tokio::select! {
1843 result = timeout_at(deadline, rx) => match result {
1844 Ok(Ok(result)) => {
1845 registration.disarm();
1846 result.into_call_result()
1847 }
1848 Ok(Err(_)) => {
1849 registration.disarm();
1850 Err(CallError::not_sent("pending response channel closed"))
1851 }
1852 Err(_) => {
1853 let accepted = if retain_late_route_open {
1854 registration.disarm();
1855 self.pending_accepted(key).unwrap_or(false)
1856 } else {
1857 registration.remove_pending().unwrap_or(false)
1858 };
1859 Err(classify_failure(
1860 accepted,
1861 format!("request on channel {channel} timed out at its deadline"),
1862 ))
1863 }
1864 },
1865 () = self.close_token.cancelled() => {
1866 let accepted = registration.remove_pending().unwrap_or(false);
1867 Err(classify_failure(accepted, "consumer closed while request was pending"))
1868 }
1869 }
1870 }
1871
1872 async fn send_subscription(
1873 self: &Arc<Self>,
1874 subscription: SubscriptionSend,
1875 ) -> Result<Subscription, CallError> {
1876 let SubscriptionSend {
1877 expected_handle,
1878 channel,
1879 epoch,
1880 body,
1881 priority,
1882 admission_class,
1883 event_buffer,
1884 deadline,
1885 permit,
1886 } = subscription;
1887 if Instant::now() >= deadline {
1888 return Err(CallError::not_sent(
1889 "subscription deadline elapsed before request was sent",
1890 ));
1891 }
1892 let (generation, corr, writer) = {
1893 let mut inner = self.lock_inner();
1894 if inner.closed {
1895 return Err(CallError::not_sent("consumer closed"));
1896 }
1897 let generation = inner.generation;
1898 if let Some(expected) = expected_handle {
1899 if expected.connection_token() != generation
1900 || expected.channel != channel
1901 || expected.epoch != epoch
1902 || inner.route_epochs.get(&channel) != Some(&expected)
1903 {
1904 return Err(CallError::StaleRouteHandle(expected));
1905 }
1906 }
1907 let Some(writer) = inner.writer.clone() else {
1908 return Err(CallError::not_sent("subc connection is down before send"));
1909 };
1910 let Some(corr) = inner.next_corr else {
1911 drop(inner);
1912 self.handle_generation_drop(
1913 generation,
1914 "correlation allocator exhausted".to_string(),
1915 );
1916 return Err(CallError::not_sent(
1917 "correlation allocator exhausted; connection closed",
1918 ));
1919 };
1920 inner.next_corr = corr.checked_add(1);
1921 (generation, corr, writer)
1922 };
1923
1924 let frame = Frame::build(
1925 FrameType::Request,
1926 Flags::new(false, priority, false).with_admission_class(admission_class),
1927 channel,
1928 epoch,
1929 corr,
1930 body,
1931 )
1932 .map_err(|err| CallError::not_sent(format!("failed to build request frame: {err}")))?;
1933 let key = PendingKey {
1934 generation,
1935 channel,
1936 epoch,
1937 corr,
1938 };
1939 let (events_tx, events_rx) = mpsc::channel(event_buffer.max(1));
1940 let (closed_tx, closed_rx) = oneshot::channel();
1941 {
1942 let mut inner = self.lock_inner();
1943 if inner.closed || inner.generation != generation || inner.writer.is_none() {
1944 return Err(CallError::not_sent(
1945 "connection changed before subscription registration",
1946 ));
1947 }
1948 if let Some(expected) = expected_handle {
1949 if inner.route_epochs.get(&expected.channel) != Some(&expected) {
1950 return Err(CallError::StaleRouteHandle(expected));
1951 }
1952 }
1953 inner.pending.insert(
1954 key,
1955 PendingEntry::subscription(events_tx, closed_tx, permit, priority),
1956 );
1957 }
1958 let mut registration = PendingRegistration::new(Arc::clone(self), key, false);
1959
1960 match timeout_at(
1961 deadline,
1962 writer.send(WriteCommand {
1963 frame,
1964 pending: Some(key),
1965 }),
1966 )
1967 .await
1968 {
1969 Ok(Ok(())) => {}
1970 Ok(Err(_)) => {
1971 let accepted = registration.remove_pending().unwrap_or(false);
1972 return Err(classify_failure(
1973 accepted,
1974 "writer task closed before accepting subscription request",
1975 ));
1976 }
1977 Err(_) => {
1978 let _ = registration.remove_pending();
1979 return Err(CallError::not_sent(
1980 "subscription deadline elapsed waiting for writer capacity",
1981 ));
1982 }
1983 }
1984
1985 registration.disarm();
1986 Ok(Subscription {
1987 events: events_rx,
1988 closed: SubscriptionClosed { rx: closed_rx },
1989 cancel: SubscriptionCancel::new(Arc::clone(self), key, priority),
1990 })
1991 }
1992
1993 fn unsubscribe_subscription(
1994 &self,
1995 key: PendingKey,
1996 priority: Priority,
1997 ) -> Result<(), CallError> {
1998 let handle = RouteHandle::new(key.channel, key.epoch, key.generation);
1999 self.validate_current_handle(handle)?;
2000 let entry = self.lock_inner().pending.remove(&key);
2001 if let Some(entry) = entry {
2002 entry.settle_subscription_result(Ok(()));
2003 self.send_cancel(handle, key.corr, priority);
2004 }
2005 Ok(())
2006 }
2007
2008 fn route_stream_data(&self, key: PendingKey, body: Vec<u8>) {
2009 let overflow = {
2010 let mut inner = self.lock_inner();
2011 let Some(entry) = inner.pending.get(&key) else {
2012 return;
2013 };
2014 match entry.try_send_stream_data(body) {
2015 Ok(()) | Err(StreamDataDelivery::NotSubscription) => return,
2016 Err(StreamDataDelivery::Full) => {
2017 let priority = entry
2018 .subscription_priority()
2019 .unwrap_or(Priority::Interactive);
2020 let entry = inner.pending.remove(&key);
2021 entry.map(|entry| {
2022 (
2023 entry,
2024 priority,
2025 "subscription event channel filled; reader dropped the stream instead of blocking",
2026 )
2027 })
2028 }
2029 Err(StreamDataDelivery::Closed) => {
2030 let priority = entry
2031 .subscription_priority()
2032 .unwrap_or(Priority::Interactive);
2033 let entry = inner.pending.remove(&key);
2034 entry.map(|entry| {
2035 (
2036 entry,
2037 priority,
2038 "subscription event receiver closed before the stream ended",
2039 )
2040 })
2041 }
2042 }
2043 };
2044
2045 if let Some((entry, priority, reason)) = overflow {
2046 entry.settle_call_error(CallError::subscription_backpressure(reason));
2047 self.send_cancel(
2048 RouteHandle::new(key.channel, key.epoch, key.generation),
2049 key.corr,
2050 priority,
2051 );
2052 }
2053 }
2054
2055 fn register_push_events(
2056 &self,
2057 handle: RouteHandle,
2058 ) -> Result<mpsc::Receiver<PushEvent>, CallError> {
2059 let (events_tx, events_rx) = mpsc::channel(DEFAULT_PUSH_EVENT_BUFFER);
2060 let mut inner = self.lock_inner();
2061 if inner.closed
2062 || inner.generation != handle.connection_token()
2063 || inner.writer.is_none()
2064 || inner.route_epochs.get(&handle.channel) != Some(&handle)
2065 {
2066 return Err(CallError::StaleRouteHandle(handle));
2067 }
2068 inner.push_event_receivers.insert(handle, events_tx);
2069 Ok(events_rx)
2070 }
2071
2072 fn route_push(&self, handle: RouteHandle, body: Vec<u8>) {
2073 let should_count_drop = {
2074 let mut inner = self.lock_inner();
2075 if inner.closed
2076 || inner.generation != handle.connection_token()
2077 || inner.route_epochs.get(&handle.channel) != Some(&handle)
2078 {
2079 return;
2080 }
2081 match inner.push_event_receivers.get(&handle) {
2082 None => true,
2083 Some(events) => match events.try_send(PushEvent { handle, body }) {
2084 Ok(()) => false,
2085 Err(mpsc::error::TrySendError::Closed(_)) => {
2086 inner.push_event_receivers.remove(&handle);
2087 true
2088 }
2089 Err(mpsc::error::TrySendError::Full(_)) => {
2090 inner.push_event_receivers.remove(&handle);
2091 false
2092 }
2093 },
2094 }
2095 };
2096 if should_count_drop {
2097 self.pushes_dropped_no_receiver
2098 .fetch_add(1, Ordering::Relaxed);
2099 }
2100 }
2101
2102 fn send_cancel(&self, handle: RouteHandle, corr: u64, priority: Priority) {
2103 let writer = {
2104 let inner = self.lock_inner();
2105 if inner.closed
2106 || inner.generation != handle.connection_token()
2107 || inner.route_epochs.get(&handle.channel) != Some(&handle)
2108 {
2109 return;
2110 }
2111 inner.writer.clone()
2112 };
2113 let Some(writer) = writer else {
2114 return;
2115 };
2116 let Ok(frame) = Frame::build(
2117 FrameType::Cancel,
2118 Flags::new(false, priority, false),
2119 handle.channel,
2120 handle.epoch,
2121 corr,
2122 Vec::new(),
2123 ) else {
2124 return;
2125 };
2126 let _ = writer.try_send(WriteCommand {
2127 frame,
2128 pending: None,
2129 });
2130 }
2131
2132 fn mark_pending_accepted(&self, key: PendingKey) -> bool {
2133 let mut inner = self.lock_inner();
2134 if inner.closed || inner.generation != key.generation {
2135 return false;
2136 }
2137 let Some(entry) = inner.pending.get_mut(&key) else {
2138 return false;
2139 };
2140 entry.accepted = true;
2141 true
2142 }
2143
2144 fn settle_pending(self: &Arc<Self>, key: PendingKey, terminal: PendingTerminal) {
2145 let entry = self.lock_inner().pending.remove(&key);
2146 let Some(entry) = entry else {
2147 return;
2148 };
2149 if entry.retain_late_route_open && entry.completion_is_closed() {
2150 if let PendingTerminal::Response { generation, body } = &terminal {
2151 if let Ok(ClientControlResponse::RouteOpen {
2152 route_channel,
2153 route_epoch,
2154 }) = serde_json::from_slice::<ClientControlResponse>(body)
2155 {
2156 let handle = RouteHandle::new(route_channel, route_epoch, *generation);
2157 self.send_route_goodbye(handle, true);
2158 self.uninstall_route_handle(handle);
2159 }
2160 }
2161 return;
2162 }
2163 entry.settle_terminal(terminal);
2164 }
2165
2166 fn pending_accepted(&self, key: PendingKey) -> Option<bool> {
2167 self.lock_inner()
2168 .pending
2169 .get(&key)
2170 .map(|entry| entry.accepted)
2171 }
2172
2173 fn handle_generation_drop(self: &Arc<Self>, generation: u64, reason: String) {
2174 let (should_emit, pending, openings, callbacks) = {
2175 let mut inner = self.lock_inner();
2176 if inner.closed || inner.generation != generation || inner.writer.is_none() {
2177 return;
2178 }
2179 inner.writer = None;
2180 inner.restored_token = inner.restored_token.saturating_add(1);
2181 inner.close_routes();
2182 inner.route_epochs.clear();
2183 let pending = drain_pending_generation(&mut inner.pending, generation);
2184 let openings = drain_openings(&mut inner.openings);
2185 let callbacks = inner.callbacks.clone();
2186 (true, pending, openings, callbacks)
2187 };
2188
2189 if should_emit {
2190 settle_pending_entries(pending, reason.clone());
2191 fail_openings(openings, SharedCallFailure::not_sent(reason.clone()));
2192 emit_callbacks(callbacks, ConnectionState::Dropped);
2193 self.notify.notify_waiters();
2194 let _ = self.spawn_reconnect(generation);
2195 }
2196 }
2197
2198 fn close_sync(&self, reason: &str) {
2199 let (pending, openings, routes, reader, writer, reconnect) = {
2200 let mut inner = self.lock_inner();
2201 if inner.closed {
2202 return;
2203 }
2204 inner.closed = true;
2205 inner.writer = None;
2206 inner.route_epochs.clear();
2207 inner.push_event_receivers.clear();
2208 self.close_token.cancel();
2209 let reconnect = match std::mem::replace(&mut inner.reconnect, ReconnectState::Idle) {
2210 ReconnectState::Background { task, .. } => Some(task),
2211 ReconnectState::Idle | ReconnectState::Inline { .. } => None,
2212 };
2213 (
2214 inner
2215 .pending
2216 .drain()
2217 .map(|(_, entry)| entry)
2218 .collect::<Vec<_>>(),
2219 inner
2220 .openings
2221 .drain()
2222 .map(|(_, opening)| opening.waiters)
2223 .collect::<Vec<_>>(),
2224 inner.drain_routes(),
2225 inner.reader_task.take(),
2226 inner.writer_task.take(),
2227 reconnect,
2228 )
2229 };
2230 for route in routes {
2231 route.sem.close();
2232 }
2233 if let Some(handle) = reader {
2234 handle.abort();
2235 }
2236 if let Some(handle) = writer {
2237 handle.abort();
2238 }
2239 if let Some(handle) = reconnect {
2240 handle.abort();
2241 }
2242 settle_pending_entries(pending, reason.to_string());
2243 fail_openings(openings, SharedCallFailure::not_sent(reason.to_string()));
2244 self.notify.notify_waiters();
2245 }
2246
2247 fn validate_current_handle(&self, handle: RouteHandle) -> Result<(), CallError> {
2248 let inner = self.lock_inner();
2249 if inner.closed
2250 || inner.generation != handle.connection_token()
2251 || inner.writer.is_none()
2252 || inner.route_epochs.get(&handle.channel) != Some(&handle)
2253 {
2254 Err(CallError::StaleRouteHandle(handle))
2255 } else {
2256 Ok(())
2257 }
2258 }
2259
2260 fn route_state(&self, handle: RouteHandle) -> Result<RouteState, CallError> {
2261 let inner = self.lock_inner();
2262 if inner.closed
2263 || inner.generation != handle.connection_token()
2264 || inner.writer.is_none()
2265 || inner.route_epochs.get(&handle.channel) != Some(&handle)
2266 {
2267 return Err(CallError::StaleRouteHandle(handle));
2268 }
2269
2270 let route = inner
2271 .route_by_channel
2272 .get(&handle.channel)
2273 .and_then(|key| inner.routes.get(key))
2274 .or_else(|| inner.one_shot_routes.get(&handle.channel));
2275 debug_assert!(route.is_none_or(|route| route.handle == handle));
2276 route
2277 .filter(|route| route.handle == handle)
2278 .cloned()
2279 .ok_or(CallError::StaleRouteHandle(handle))
2280 }
2281
2282 fn route_is_current(&self, key: &RouteKey, route: &RouteState) -> bool {
2283 let inner = self.lock_inner();
2284 if inner.closed
2285 || inner.generation != route.handle.connection_token()
2286 || inner.writer.is_none()
2287 {
2288 return false;
2289 }
2290 inner.routes.get(key).is_some_and(|cached| {
2291 cached.handle == route.handle && Arc::ptr_eq(&cached.sem, &route.sem)
2292 })
2293 }
2294
2295 fn invalidate_route(&self, key: &RouteKey, expected_handle: Option<RouteHandle>) {
2296 let removed = {
2297 let mut inner = self.lock_inner();
2298 match inner.routes.get(key) {
2299 Some(route) if expected_handle.is_none_or(|expected| expected == route.handle) => {
2300 let removed = inner.remove_route(key);
2301 if let Some(route) = &removed {
2302 if inner.route_epochs.get(&route.handle.channel) == Some(&route.handle) {
2303 inner.route_epochs.remove(&route.handle.channel);
2304 }
2305 inner.push_event_receivers.remove(&route.handle);
2306 }
2307 removed
2308 }
2309 _ => None,
2310 }
2311 };
2312 if let Some(route) = removed {
2313 route.sem.close();
2314 }
2315 }
2316
2317 fn finish_opening(&self, key: &RouteKey, result: Result<RouteState, SharedCallFailure>) {
2318 let opening = self.lock_inner().openings.remove(key);
2319 for waiter in opening.map(|o| o.waiters).unwrap_or_default() {
2320 let _ = waiter.send(result.clone());
2321 }
2322 }
2323
2324 async fn close_handle(
2325 self: &Arc<Self>,
2326 handle: RouteHandle,
2327 opts: &CloseRouteOptions,
2328 ) -> Result<(), CallError> {
2329 self.validate_current_handle(handle)?;
2330 let routes = {
2331 let mut inner = self.lock_inner();
2332 inner
2333 .remove_route_by_handle(handle)
2334 .into_iter()
2335 .collect::<Vec<_>>()
2336 };
2337 if opts.drain {
2338 self.drain_channel(handle, opts.drain_timeout).await;
2339 }
2340 for route in routes {
2341 route.sem.close();
2342 }
2343 self.fail_channel_pending(handle, "route closed by close_handle");
2344 self.send_route_goodbye(handle, false);
2345 self.uninstall_route_handle(handle);
2346 Ok(())
2347 }
2348
2349 async fn close_route(self: &Arc<Self>, key: &RouteKey, opts: &CloseRouteOptions) {
2351 let route = {
2355 let mut inner = self.lock_inner();
2356 if let Some(opening) = inner.openings.get_mut(key) {
2357 opening.closed = true;
2358 }
2359 inner.remove_route(key)
2360 };
2361
2362 let Some(route) = route else {
2365 return;
2366 };
2367
2368 if opts.drain {
2369 self.drain_channel(route.handle, opts.drain_timeout).await;
2372 }
2373
2374 route.sem.close();
2378 self.fail_channel_pending(route.handle, "route closed by close_route");
2379
2380 self.send_route_goodbye(route.handle, false);
2383 self.uninstall_route_handle(route.handle);
2384 }
2385
2386 fn uninstall_route_handle(&self, handle: RouteHandle) {
2387 let mut inner = self.lock_inner();
2388 if inner.route_epochs.get(&handle.channel) == Some(&handle) {
2389 inner.route_epochs.remove(&handle.channel);
2390 inner.push_event_receivers.remove(&handle);
2391 }
2392 }
2393
2394 fn fail_channel_pending(&self, handle: RouteHandle, reason: &str) {
2398 let entries = {
2399 let mut inner = self.lock_inner();
2400 drain_pending_handle(&mut inner.pending, handle, true)
2401 };
2402 settle_pending_entries(entries, reason.to_string());
2403 }
2404
2405 async fn drain_channel(&self, handle: RouteHandle, timeout: Duration) {
2409 let deadline = Instant::now() + timeout;
2410 loop {
2411 let has_inflight = {
2412 let inner = self.lock_inner();
2413 inner.pending.iter().any(|(key, entry)| {
2414 key.generation == handle.connection_token()
2415 && key.channel == handle.channel
2416 && key.epoch == handle.epoch
2417 && !entry.is_subscription()
2418 })
2419 };
2420 if !has_inflight || Instant::now() >= deadline {
2421 return;
2422 }
2423 sleep(Duration::from_millis(5)).await;
2424 }
2425 }
2426
2427 fn send_route_goodbye(self: &Arc<Self>, handle: RouteHandle, close_on_failure: bool) -> bool {
2431 let writer = {
2432 let inner = self.lock_inner();
2433 if inner.closed
2434 || inner.generation != handle.connection_token()
2435 || inner.route_epochs.get(&handle.channel) != Some(&handle)
2436 {
2437 return false;
2438 }
2439 inner.writer.clone()
2440 };
2441 let Some(writer) = writer else {
2442 return false;
2443 };
2444 let Ok(frame) = Frame::build(
2445 FrameType::Goodbye,
2446 Flags::new(false, Priority::Interactive, false),
2447 handle.channel,
2448 handle.epoch,
2449 0,
2450 Vec::new(),
2451 ) else {
2452 return false;
2453 };
2454 if writer
2455 .try_send(WriteCommand {
2456 frame,
2457 pending: None,
2458 })
2459 .is_ok()
2460 {
2461 return true;
2462 }
2463 if close_on_failure {
2464 self.handle_generation_drop(
2465 handle.connection_token(),
2466 "failed to queue late route.open cleanup GOODBYE".to_string(),
2467 );
2468 }
2469 false
2470 }
2471
2472 fn emit_connection_state(&self, state: ConnectionState) {
2473 let callbacks = self.lock_inner().callbacks.clone();
2474 emit_callbacks(callbacks, state);
2475 }
2476}
2477
2478#[derive(Clone, Copy)]
2479enum InstallKind {
2480 Initial,
2481 Reconnect,
2482}
2483
2484enum EnsureAction {
2485 Wait,
2486 Lead {
2487 generation: u64,
2488 stale_task: Option<JoinHandle<()>>,
2489 },
2490}
2491
2492enum ReconnectState {
2496 Idle,
2497 Inline {
2498 generation: u64,
2499 },
2500 Background {
2501 generation: u64,
2502 task: JoinHandle<()>,
2503 },
2504}
2505
2506enum RouteInstall {
2509 Cached(RouteState),
2511 Discard { closed: bool },
2514}
2515
2516enum RouteOpenAction {
2517 Wait(oneshot::Receiver<Result<RouteState, SharedCallFailure>>),
2518 Lead,
2519}
2520
2521struct RouteOpenParams<'a> {
2522 target: &'a RouteTarget,
2523 identity: &'a BindIdentity,
2524 consumer_identity: &'a Option<ConsumerIdentity>,
2525 consumer_capabilities: &'a Option<Vec<String>>,
2526}
2527
2528struct RequestSend {
2529 expected_handle: Option<RouteHandle>,
2530 channel: u16,
2531 epoch: u32,
2532 body: Vec<u8>,
2533 priority: Priority,
2534 admission_class: AdmissionClass,
2535 deadline: Instant,
2536 retain_late_route_open: bool,
2537}
2538
2539struct SubscriptionSend {
2540 expected_handle: Option<RouteHandle>,
2541 channel: u16,
2542 epoch: u32,
2543 body: Vec<u8>,
2544 priority: Priority,
2545 admission_class: AdmissionClass,
2546 event_buffer: usize,
2547 deadline: Instant,
2548 permit: OwnedSemaphorePermit,
2549}
2550
2551struct OpeningGuard {
2552 shared: Arc<Shared>,
2553 key: RouteKey,
2554 finished: bool,
2555}
2556
2557impl OpeningGuard {
2558 fn new(shared: Arc<Shared>, key: RouteKey) -> Self {
2559 Self {
2560 shared,
2561 key,
2562 finished: false,
2563 }
2564 }
2565
2566 fn finish(&mut self, result: Result<RouteState, SharedCallFailure>) {
2567 self.shared.finish_opening(&self.key, result);
2568 self.finished = true;
2569 }
2570}
2571
2572impl Drop for OpeningGuard {
2573 fn drop(&mut self) {
2574 if !self.finished {
2575 self.shared.finish_opening(
2576 &self.key,
2577 Err(SharedCallFailure::not_sent(
2578 "route.open future was cancelled",
2579 )),
2580 );
2581 }
2582 }
2583}
2584
2585struct InlineReconnectGuard {
2586 shared: Arc<Shared>,
2587 generation: u64,
2588 finished: bool,
2589}
2590
2591impl InlineReconnectGuard {
2592 fn new(shared: Arc<Shared>, generation: u64) -> Self {
2593 Self {
2594 shared,
2595 generation,
2596 finished: false,
2597 }
2598 }
2599
2600 fn finish(&mut self) {
2601 self.shared.finish_inline_reconnect(self.generation);
2602 self.finished = true;
2603 }
2604}
2605
2606impl Drop for InlineReconnectGuard {
2607 fn drop(&mut self) {
2608 if !self.finished {
2609 self.shared.finish_inline_reconnect(self.generation);
2610 }
2611 }
2612}
2613
2614#[derive(Clone)]
2615struct RouteState {
2616 handle: RouteHandle,
2617 sem: Arc<Semaphore>,
2618}
2619
2620#[derive(Debug, Clone, Hash, PartialEq, Eq)]
2621struct RouteKey {
2622 target: RouteTargetKey,
2623 project_root: PathBuf,
2624 harness: String,
2625 session: String,
2626 consumer_identity: Option<ConsumerIdentityKey>,
2627 consumer_capabilities: Option<ConsumerCapabilitiesKey>,
2628}
2629
2630impl RouteKey {
2631 fn new(
2632 target: &RouteTarget,
2633 identity: &BindIdentity,
2634 consumer_identity: Option<&ConsumerIdentity>,
2635 consumer_capabilities: Option<&[String]>,
2636 ) -> Self {
2637 Self {
2638 target: RouteTargetKey::from(target),
2639 project_root: identity.project_root.clone(),
2640 harness: identity.harness.clone(),
2641 session: identity.session.clone(),
2642 consumer_identity: consumer_identity.map(ConsumerIdentityKey::from),
2643 consumer_capabilities: consumer_capabilities.map(ConsumerCapabilitiesKey::from_slice),
2644 }
2645 }
2646
2647 fn target_label(&self) -> String {
2648 match &self.target {
2649 RouteTargetKey::ToolProvider { module_id } => format!("tool_provider:{module_id}"),
2650 RouteTargetKey::ManagementSurface { module_id } => {
2651 format!("management_surface:{module_id}")
2652 }
2653 RouteTargetKey::InternalService {
2654 module_id,
2655 service_id,
2656 } => format!("internal_service:{module_id}:{service_id}"),
2657 }
2658 }
2659}
2660
2661#[derive(Debug, Clone, Hash, PartialEq, Eq)]
2662struct ConsumerIdentityKey {
2663 module_id: String,
2664 launch_nonce: String,
2665}
2666
2667impl From<&ConsumerIdentity> for ConsumerIdentityKey {
2668 fn from(value: &ConsumerIdentity) -> Self {
2669 Self {
2670 module_id: value.module_id.clone(),
2671 launch_nonce: value.launch_nonce.clone(),
2672 }
2673 }
2674}
2675
2676#[derive(Debug, Clone, Hash, PartialEq, Eq)]
2677struct ConsumerCapabilitiesKey {
2678 values: Vec<String>,
2679}
2680
2681impl ConsumerCapabilitiesKey {
2682 fn from_slice(values: &[String]) -> Self {
2683 let mut values = values.to_vec();
2684 values.sort();
2685 values.dedup();
2686 Self { values }
2687 }
2688}
2689
2690#[derive(Debug, Clone, Hash, PartialEq, Eq)]
2691enum RouteTargetKey {
2692 ToolProvider {
2693 module_id: String,
2694 },
2695 ManagementSurface {
2696 module_id: String,
2697 },
2698 InternalService {
2699 module_id: String,
2700 service_id: String,
2701 },
2702}
2703
2704impl From<&RouteTarget> for RouteTargetKey {
2705 fn from(value: &RouteTarget) -> Self {
2706 match value {
2707 RouteTarget::ToolProvider { module_id } => Self::ToolProvider {
2708 module_id: module_id.clone(),
2709 },
2710 RouteTarget::ManagementSurface { module_id } => Self::ManagementSurface {
2711 module_id: module_id.clone(),
2712 },
2713 RouteTarget::InternalService {
2714 module_id,
2715 service_id,
2716 } => Self::InternalService {
2717 module_id: module_id.clone(),
2718 service_id: service_id.clone(),
2719 },
2720 }
2721 }
2722}
2723
2724#[derive(Debug, Clone)]
2725struct SharedCallFailure {
2726 kind: FailureKind,
2727 message: String,
2728}
2729
2730impl SharedCallFailure {
2731 fn not_sent(message: impl Into<String>) -> Self {
2732 Self {
2733 kind: FailureKind::NotSent,
2734 message: message.into(),
2735 }
2736 }
2737
2738 fn into_call_error(self) -> CallError {
2739 match self.kind {
2740 FailureKind::NotSent => CallError::not_sent(self.message),
2741 FailureKind::OutcomeUnknown => CallError::outcome_unknown(self.message),
2742 }
2743 }
2744}
2745
2746impl From<CallError> for SharedCallFailure {
2747 fn from(value: CallError) -> Self {
2748 match value {
2749 CallError::NotSent(err) => Self {
2750 kind: FailureKind::NotSent,
2751 message: err.to_string(),
2752 },
2753 CallError::OutcomeUnknown(err) => Self {
2754 kind: FailureKind::OutcomeUnknown,
2755 message: err.to_string(),
2756 },
2757 CallError::Module(body) => Self {
2758 kind: FailureKind::OutcomeUnknown,
2759 message: format!(
2760 "unexpected module error during route.open: {} ({})",
2761 body.code, body.message
2762 ),
2763 },
2764 CallError::SubscriptionBackpressure(err) => Self {
2765 kind: FailureKind::OutcomeUnknown,
2766 message: err.to_string(),
2767 },
2768 CallError::StaleRouteHandle(handle) => Self {
2769 kind: FailureKind::NotSent,
2770 message: format!("stale route handle: {handle:?}"),
2771 },
2772 }
2773 }
2774}
2775
2776#[derive(Debug, Clone, Copy)]
2777enum FailureKind {
2778 NotSent,
2779 OutcomeUnknown,
2780}
2781
2782#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
2783struct PendingKey {
2784 generation: u64,
2785 channel: u16,
2786 epoch: u32,
2787 corr: u64,
2788}
2789
2790struct PendingEntry {
2791 accepted: bool,
2792 retain_late_route_open: bool,
2793 expected_control_handle: Option<RouteHandle>,
2794 completion: PendingCompletion,
2795}
2796
2797enum PendingCompletion {
2798 Unary(oneshot::Sender<PendingResult>),
2799 Subscription {
2800 events: mpsc::Sender<Vec<u8>>,
2801 closed: oneshot::Sender<Result<(), CallError>>,
2802 _permit: OwnedSemaphorePermit,
2803 priority: Priority,
2804 },
2805}
2806
2807enum StreamDataDelivery {
2808 NotSubscription,
2809 Full,
2810 Closed,
2811}
2812
2813impl PendingEntry {
2814 fn unary(
2815 tx: oneshot::Sender<PendingResult>,
2816 retain_late_route_open: bool,
2817 expected_control_handle: Option<RouteHandle>,
2818 ) -> Self {
2819 Self {
2820 accepted: false,
2821 retain_late_route_open,
2822 expected_control_handle,
2823 completion: PendingCompletion::Unary(tx),
2824 }
2825 }
2826
2827 fn subscription(
2828 events: mpsc::Sender<Vec<u8>>,
2829 closed: oneshot::Sender<Result<(), CallError>>,
2830 permit: OwnedSemaphorePermit,
2831 priority: Priority,
2832 ) -> Self {
2833 Self {
2834 accepted: false,
2835 retain_late_route_open: false,
2836 expected_control_handle: None,
2837 completion: PendingCompletion::Subscription {
2838 events,
2839 closed,
2840 _permit: permit,
2841 priority,
2842 },
2843 }
2844 }
2845
2846 fn completion_is_closed(&self) -> bool {
2847 match &self.completion {
2848 PendingCompletion::Unary(tx) => tx.is_closed(),
2849 PendingCompletion::Subscription { closed, .. } => closed.is_closed(),
2850 }
2851 }
2852
2853 fn is_subscription(&self) -> bool {
2854 matches!(&self.completion, PendingCompletion::Subscription { .. })
2855 }
2856
2857 fn subscription_priority(&self) -> Option<Priority> {
2858 match &self.completion {
2859 PendingCompletion::Subscription { priority, .. } => Some(*priority),
2860 PendingCompletion::Unary(_) => None,
2861 }
2862 }
2863
2864 fn try_send_stream_data(&self, body: Vec<u8>) -> Result<(), StreamDataDelivery> {
2865 let PendingCompletion::Subscription { events, .. } = &self.completion else {
2866 return Err(StreamDataDelivery::NotSubscription);
2867 };
2868 events.try_send(body).map_err(|err| match err {
2869 mpsc::error::TrySendError::Full(_) => StreamDataDelivery::Full,
2870 mpsc::error::TrySendError::Closed(_) => StreamDataDelivery::Closed,
2871 })
2872 }
2873
2874 fn settle_terminal(self, terminal: PendingTerminal) {
2875 match self.completion {
2876 PendingCompletion::Unary(tx) => {
2877 let _ = tx.send(PendingResult::Terminal(terminal));
2878 }
2879 PendingCompletion::Subscription { closed, .. } => {
2880 let result = match terminal {
2881 PendingTerminal::Response { .. } | PendingTerminal::StreamEnd => Ok(()),
2882 PendingTerminal::Error { body } => Err(CallError::Module(body)),
2883 };
2884 let _ = closed.send(result);
2885 }
2886 }
2887 }
2888
2889 fn settle_failure(self, reason: String) {
2890 let accepted = self.accepted;
2891 self.settle_call_error(classify_failure(accepted, reason));
2892 }
2893
2894 fn settle_call_error(self, err: CallError) {
2895 match self.completion {
2896 PendingCompletion::Unary(tx) => {
2897 let _ = tx.send(PendingResult::Failure {
2898 accepted: self.accepted,
2899 reason: err.to_string(),
2900 });
2901 }
2902 PendingCompletion::Subscription { closed, .. } => {
2903 let _ = closed.send(Err(err));
2904 }
2905 }
2906 }
2907
2908 fn settle_subscription_result(self, result: Result<(), CallError>) {
2909 match self.completion {
2910 PendingCompletion::Subscription { closed, .. } => {
2911 let _ = closed.send(result);
2912 }
2913 PendingCompletion::Unary(tx) => {
2914 let _ = tx.send(PendingResult::Failure {
2915 accepted: self.accepted,
2916 reason: "subscription cancel matched a unary request".to_string(),
2917 });
2918 }
2919 }
2920 }
2921}
2922
2923struct PendingRegistration {
2924 shared: Arc<Shared>,
2925 key: PendingKey,
2926 active: bool,
2927 retain_on_drop: bool,
2928}
2929
2930impl PendingRegistration {
2931 fn new(shared: Arc<Shared>, key: PendingKey, retain_on_drop: bool) -> Self {
2932 Self {
2933 shared,
2934 key,
2935 active: true,
2936 retain_on_drop,
2937 }
2938 }
2939
2940 fn remove_pending(&mut self) -> Option<bool> {
2941 if !self.active {
2942 return None;
2943 }
2944 self.active = false;
2945 self.shared
2946 .lock_inner()
2947 .pending
2948 .remove(&self.key)
2949 .map(|entry| entry.accepted)
2950 }
2951
2952 fn disarm(&mut self) {
2953 self.active = false;
2954 }
2955}
2956
2957impl Drop for PendingRegistration {
2958 fn drop(&mut self) {
2959 if self.retain_on_drop {
2960 self.disarm();
2961 } else {
2962 let _ = self.remove_pending();
2963 }
2964 }
2965}
2966
2967enum PendingResult {
2968 Terminal(PendingTerminal),
2969 Failure { accepted: bool, reason: String },
2970}
2971
2972impl PendingResult {
2973 fn into_call_result(self) -> Result<TerminalFrame, CallError> {
2974 match self {
2975 Self::Terminal(terminal) => Ok(terminal.into_terminal_frame()),
2976 Self::Failure { accepted, reason } => Err(classify_failure(accepted, reason)),
2977 }
2978 }
2979}
2980
2981enum PendingTerminal {
2982 Response { generation: u64, body: Vec<u8> },
2983 Error { body: ErrorBody },
2984 StreamEnd,
2985}
2986
2987impl PendingTerminal {
2988 fn into_terminal_frame(self) -> TerminalFrame {
2989 match self {
2990 Self::Response { generation, body } => TerminalFrame::Response { generation, body },
2991 Self::Error { body } => TerminalFrame::Error { body },
2992 Self::StreamEnd => TerminalFrame::StreamEnd,
2993 }
2994 }
2995}
2996
2997#[derive(Debug)]
2998enum TerminalFrame {
2999 Response { generation: u64, body: Vec<u8> },
3000 Error { body: ErrorBody },
3001 StreamEnd,
3002}
3003
3004struct WriteCommand {
3005 frame: Frame,
3006 pending: Option<PendingKey>,
3007}
3008
3009struct OpenedConnection {
3010 stream: TcpStream,
3011}
3012
3013async fn open_connection(
3014 path: &Path,
3015 deadline: Duration,
3016) -> Result<OpenedConnection, ConsumerError> {
3017 let conn =
3018 connection_file::read_for_client(path).map_err(|source| ConsumerError::ConnectionFile {
3019 path: path.to_path_buf(),
3020 source,
3021 })?;
3022 let endpoint = conn
3023 .endpoints
3024 .first()
3025 .ok_or_else(|| ConsumerError::NoEndpoint {
3026 path: path.to_path_buf(),
3027 })?;
3028 let endpoint_label = format!("{}:{}", endpoint.host, endpoint.port);
3029 let mut stream = TcpStream::connect(&endpoint_label)
3030 .await
3031 .map_err(|source| ConsumerError::Connect {
3032 path: path.to_path_buf(),
3033 endpoint: endpoint_label.clone(),
3034 source,
3035 })?;
3036 let _ = stream.set_nodelay(true);
3044 authenticate_client(&mut stream, &conn, deadline)
3045 .await
3046 .map_err(|source| ConsumerError::Auth {
3047 path: path.to_path_buf(),
3048 endpoint: endpoint_label,
3049 source,
3050 })?;
3051 Ok(OpenedConnection { stream })
3052}
3053
3054async fn reader_loop(shared: Arc<Shared>, mut reader: OwnedReadHalf, generation: u64) {
3055 loop {
3056 match read_frame(&mut reader).await {
3057 Ok(Some(frame)) => {
3058 if !dispatch_frame(&shared, generation, frame).await {
3059 return;
3060 }
3061 }
3062 Ok(None) => {
3063 shared.handle_generation_drop(generation, "subc connection closed".to_string());
3064 return;
3065 }
3066 Err(err) => {
3067 shared.handle_generation_drop(generation, err.to_string());
3068 return;
3069 }
3070 }
3071 }
3072}
3073
3074async fn dispatch_frame(shared: &Arc<Shared>, generation: u64, frame: Frame) -> bool {
3075 if !shared.generation_is_current(generation) {
3076 return false;
3077 }
3078 if frame.header.channel != 0
3079 && !shared.validate_ingress_handle(generation, frame.header.channel, frame.header.epoch)
3080 {
3081 return true;
3082 }
3083
3084 let key = PendingKey {
3085 generation,
3086 channel: frame.header.channel,
3087 epoch: frame.header.epoch,
3088 corr: frame.header.corr,
3089 };
3090
3091 if frame.header.channel == 0 && frame.header.ty == FrameType::Response {
3092 if let Some(expected) = shared.pending_expected_control_handle(key) {
3093 let echoes_expected = matches!(
3094 serde_json::from_slice::<ClientControlResponse>(&frame.body),
3095 Ok(ClientControlResponse::RoutePoll {
3096 route_channel,
3097 route_epoch,
3098 ..
3099 }) if route_channel == expected.channel && route_epoch == expected.epoch
3100 );
3101 if !echoes_expected {
3102 shared.count_dropped_route_frame();
3103 return true;
3104 }
3105 }
3106 }
3107
3108 if frame.header.channel == 0
3111 && frame.header.ty == FrameType::Response
3112 && shared.pending_expects_route_open(key)
3113 {
3114 if let Ok(ClientControlResponse::RouteOpen {
3115 route_channel,
3116 route_epoch,
3117 }) = serde_json::from_slice::<ClientControlResponse>(&frame.body)
3118 {
3119 shared.install_ingress_handle(RouteHandle::new(route_channel, route_epoch, generation));
3120 }
3121 }
3122
3123 match frame.header.ty {
3124 FrameType::Response => shared.settle_pending(
3125 key,
3126 PendingTerminal::Response {
3127 generation,
3128 body: frame.body,
3129 },
3130 ),
3131 FrameType::Error => {
3132 let body =
3133 serde_json::from_slice::<ErrorBody>(&frame.body).unwrap_or_else(|err| ErrorBody {
3134 code: "invalid_error_body".to_string(),
3135 message: err.to_string(),
3136 });
3137 shared.settle_pending(key, PendingTerminal::Error { body });
3138 }
3139 FrameType::StreamEnd => shared.settle_pending(key, PendingTerminal::StreamEnd),
3140 FrameType::StreamData => shared.route_stream_data(key, frame.body),
3141 FrameType::Push => shared.route_push(
3142 RouteHandle::new(frame.header.channel, frame.header.epoch, generation),
3143 frame.body,
3144 ),
3145 FrameType::Goodbye if frame.header.channel == 0 => {
3146 shared.handle_generation_drop(generation, "subc sent GOODBYE".to_string());
3147 return false;
3148 }
3149 FrameType::Goodbye => {
3150 let handle = RouteHandle::new(frame.header.channel, frame.header.epoch, generation);
3151 shared.invalidate_routes_for_handle(handle);
3152 let pending = {
3153 let mut inner = shared.lock_inner();
3154 drain_pending_handle(&mut inner.pending, handle, true)
3155 };
3156 settle_pending_entries(pending, "route closed by subc".to_string());
3157 }
3158 FrameType::Ping if frame.header.channel == 0 => {
3159 if let Ok(pong) = Frame::build_with_version(
3160 frame.header.ver,
3161 FrameType::Pong,
3162 frame.header.flags,
3163 0,
3164 0,
3165 frame.header.corr,
3166 Vec::new(),
3167 ) {
3168 let writer = shared.lock_inner().writer.clone();
3169 if let Some(writer) = writer {
3170 let _ = writer
3171 .send(WriteCommand {
3172 frame: pong,
3173 pending: None,
3174 })
3175 .await;
3176 }
3177 }
3178 }
3179 _ => {}
3180 }
3181 true
3182}
3183
3184impl Shared {
3185 fn generation_is_current(&self, generation: u64) -> bool {
3186 let inner = self.lock_inner();
3187 !inner.closed && inner.generation == generation && inner.writer.is_some()
3188 }
3189
3190 fn pending_expected_control_handle(&self, key: PendingKey) -> Option<RouteHandle> {
3191 self.lock_inner()
3192 .pending
3193 .get(&key)
3194 .and_then(|entry| entry.expected_control_handle)
3195 }
3196
3197 fn count_dropped_route_frame(&self) {
3198 let mut inner = self.lock_inner();
3199 inner.dropped_route_frames = inner.dropped_route_frames.saturating_add(1);
3200 }
3201
3202 fn pending_expects_route_open(&self, key: PendingKey) -> bool {
3203 self.lock_inner()
3204 .pending
3205 .get(&key)
3206 .is_some_and(|entry| entry.retain_late_route_open)
3207 }
3208
3209 fn validate_ingress_handle(&self, generation: u64, channel: u16, epoch: u32) -> bool {
3210 let mut inner = self.lock_inner();
3211 let expected = RouteHandle::new(channel, epoch, generation);
3212 if inner.route_epochs.get(&channel) == Some(&expected) {
3213 true
3214 } else {
3215 inner.dropped_route_frames = inner.dropped_route_frames.saturating_add(1);
3216 false
3217 }
3218 }
3219
3220 fn install_ingress_handle(&self, handle: RouteHandle) {
3221 let mut inner = self.lock_inner();
3222 if !inner.closed && inner.generation == handle.connection_token() && inner.writer.is_some()
3223 {
3224 inner.route_epochs.insert(handle.channel, handle);
3225 }
3226 }
3227
3228 fn invalidate_routes_for_handle(&self, handle: RouteHandle) {
3229 let removed = {
3230 let mut inner = self.lock_inner();
3231 if inner.route_epochs.get(&handle.channel) != Some(&handle) {
3232 return;
3233 }
3234 inner.route_epochs.remove(&handle.channel);
3235 inner.push_event_receivers.remove(&handle);
3236 inner
3237 .remove_route_by_handle(handle)
3238 .into_iter()
3239 .collect::<Vec<_>>()
3240 };
3241 for route in removed {
3242 route.sem.close();
3243 }
3244 }
3245}
3246
3247async fn writer_loop<W>(
3248 shared: Arc<Shared>,
3249 writer: W,
3250 mut rx: mpsc::Receiver<WriteCommand>,
3251 generation: u64,
3252) where
3253 W: AsyncWrite + Unpin,
3254{
3255 let mut writer = BufWriter::new(writer);
3256 while let Some(command) = rx.recv().await {
3257 if let Some(key) = command.pending {
3258 if !shared.mark_pending_accepted(key) {
3259 continue;
3260 }
3261 }
3262 if let Err(err) = write_frame(&mut writer, &command.frame).await {
3263 shared.handle_generation_drop(generation, err.to_string());
3264 return;
3265 }
3266 while let Ok(command) = rx.try_recv() {
3267 if let Some(key) = command.pending {
3268 if !shared.mark_pending_accepted(key) {
3269 continue;
3270 }
3271 }
3272 if let Err(err) = write_frame(&mut writer, &command.frame).await {
3273 shared.handle_generation_drop(generation, err.to_string());
3274 return;
3275 }
3276 }
3277 if let Err(err) = writer.flush().await.map_err(FrameIoError::Io) {
3278 shared.handle_generation_drop(generation, err.to_string());
3279 return;
3280 }
3281 }
3282}
3283
3284fn route_open_consumer_identity(opts: &CallOptions) -> Option<ConsumerIdentity> {
3285 opts.consumer_identity
3286 .clone()
3287 .or_else(consumer_identity_from_env)
3288}
3289
3290fn close_route_consumer_identity(opts: &CloseRouteOptions) -> Option<ConsumerIdentity> {
3291 opts.consumer_identity
3292 .clone()
3293 .or_else(consumer_identity_from_env)
3294}
3295
3296fn route_open_consumer_capabilities(opts: &CallOptions) -> Option<Vec<String>> {
3297 opts.consumer_capabilities.clone()
3298}
3299
3300fn close_route_consumer_capabilities(opts: &CloseRouteOptions) -> Option<Vec<String>> {
3301 opts.consumer_capabilities.clone()
3302}
3303
3304fn consumer_identity_from_env() -> Option<ConsumerIdentity> {
3305 let module_id = std::env::var(SUBC_MODULE_ID_ENV)
3306 .ok()
3307 .filter(|value| !value.is_empty())?;
3308 let launch_nonce = std::env::var(SUBC_LAUNCH_NONCE_ENV)
3309 .ok()
3310 .filter(|value| !value.is_empty())?;
3311 Some(ConsumerIdentity {
3312 module_id,
3313 launch_nonce,
3314 })
3315}
3316
3317fn classify_failure(accepted: bool, reason: impl Into<String>) -> CallError {
3318 if accepted {
3319 CallError::outcome_unknown(reason)
3320 } else {
3321 CallError::not_sent(reason)
3322 }
3323}
3324
3325fn settle_pending_entries(entries: Vec<PendingEntry>, reason: String) {
3326 for entry in entries {
3327 entry.settle_failure(reason.clone());
3328 }
3329}
3330
3331fn drain_pending_generation(
3332 pending: &mut HashMap<PendingKey, PendingEntry>,
3333 generation: u64,
3334) -> Vec<PendingEntry> {
3335 let keys = pending
3336 .keys()
3337 .copied()
3338 .filter(|key| key.generation == generation)
3339 .collect::<Vec<_>>();
3340 keys.into_iter()
3341 .filter_map(|key| pending.remove(&key))
3342 .collect()
3343}
3344
3345fn drain_pending_handle(
3346 pending: &mut HashMap<PendingKey, PendingEntry>,
3347 handle: RouteHandle,
3348 include_subscriptions: bool,
3349) -> Vec<PendingEntry> {
3350 let keys = pending
3351 .iter()
3352 .filter_map(|(key, entry)| {
3353 (key.generation == handle.connection_token()
3354 && key.channel == handle.channel
3355 && key.epoch == handle.epoch
3356 && (include_subscriptions || !entry.is_subscription()))
3357 .then_some(*key)
3358 })
3359 .collect::<Vec<_>>();
3360 keys.into_iter()
3361 .filter_map(|key| pending.remove(&key))
3362 .collect()
3363}
3364
3365fn drain_openings(openings: &mut HashMap<RouteKey, Opening>) -> Vec<Vec<OpeningWaiter>> {
3366 openings
3367 .drain()
3368 .map(|(_, opening)| opening.waiters)
3369 .collect()
3370}
3371
3372fn fail_openings(openings: Vec<Vec<OpeningWaiter>>, failure: SharedCallFailure) {
3373 for waiters in openings {
3374 for waiter in waiters {
3375 let _ = waiter.send(Err(failure.clone()));
3376 }
3377 }
3378}
3379
3380fn emit_callbacks(callbacks: Vec<Callback>, state: ConnectionState) {
3381 for callback in callbacks {
3382 if let Ok(callback) = callback.lock() {
3383 callback(state.clone());
3384 }
3385 }
3386}
3387
3388fn is_retryable_route_open_code(code: &str) -> bool {
3389 matches!(
3390 code,
3391 "unknown_module" | "module_reloading" | "target_unavailable" | "module_timeout"
3392 )
3393}
3394
3395fn is_retryable_catalog_transport_error(err: &CallError) -> bool {
3396 matches!(err, CallError::NotSent(_) | CallError::OutcomeUnknown(_))
3397}
3398
3399fn is_reconnect_transient(err: &ConsumerError) -> bool {
3400 match err {
3401 ConsumerError::Connect { source, .. } => matches!(
3402 source.kind(),
3403 io::ErrorKind::ConnectionRefused
3404 | io::ErrorKind::ConnectionReset
3405 | io::ErrorKind::ConnectionAborted
3406 | io::ErrorKind::TimedOut
3407 | io::ErrorKind::NotConnected
3408 | io::ErrorKind::AddrNotAvailable
3409 ),
3410 ConsumerError::ConnectionFile { source, .. } => match source {
3411 ConnectionFileError::Io { source, .. } => source.kind() == io::ErrorKind::NotFound,
3412 _ => false,
3413 },
3414 ConsumerError::Auth { .. } => true,
3423 ConsumerError::NoEndpoint { .. } | ConsumerError::Closed => false,
3424 }
3425}
3426
3427impl From<FrameBuildError> for CallError {
3428 fn from(err: FrameBuildError) -> Self {
3429 Self::not_sent(err.to_string())
3430 }
3431}
3432
3433#[cfg(test)]
3434mod tests {
3435 use super::*;
3436
3437 #[derive(Clone)]
3438 struct InstrumentedWriter {
3439 state: Arc<InstrumentedWriterState>,
3440 fail_flush: bool,
3441 }
3442
3443 #[derive(Default)]
3444 struct InstrumentedWriterState {
3445 bytes: Mutex<Vec<u8>>,
3446 flushes: std::sync::atomic::AtomicUsize,
3447 }
3448
3449 impl InstrumentedWriter {
3450 fn new(fail_flush: bool) -> Self {
3451 Self {
3452 state: Arc::new(InstrumentedWriterState::default()),
3453 fail_flush,
3454 }
3455 }
3456
3457 fn bytes(&self) -> Vec<u8> {
3458 self.state
3459 .bytes
3460 .lock()
3461 .unwrap_or_else(|poisoned| poisoned.into_inner())
3462 .clone()
3463 }
3464
3465 fn flush_count(&self) -> usize {
3466 self.state.flushes.load(Ordering::SeqCst)
3467 }
3468 }
3469
3470 impl AsyncWrite for InstrumentedWriter {
3471 fn poll_write(
3472 self: Pin<&mut Self>,
3473 _cx: &mut Context<'_>,
3474 buf: &[u8],
3475 ) -> Poll<io::Result<usize>> {
3476 self.state
3477 .bytes
3478 .lock()
3479 .unwrap_or_else(|poisoned| poisoned.into_inner())
3480 .extend_from_slice(buf);
3481 Poll::Ready(Ok(buf.len()))
3482 }
3483
3484 fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
3485 self.state.flushes.fetch_add(1, Ordering::SeqCst);
3486 if self.fail_flush {
3487 Poll::Ready(Err(io::Error::new(
3488 io::ErrorKind::BrokenPipe,
3489 "instrumented flush failure",
3490 )))
3491 } else {
3492 Poll::Ready(Ok(()))
3493 }
3494 }
3495
3496 fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
3497 Poll::Ready(Ok(()))
3498 }
3499 }
3500
3501 fn writer_test_shared() -> Arc<Shared> {
3502 Arc::new(Shared::new(
3503 PathBuf::from("/tmp/does-not-exist"),
3504 ConsumerOptions {
3505 reconnect_backoff: RetryBackoff {
3506 max_attempts: 1,
3507 ..RetryBackoff::default()
3508 },
3509 ..ConsumerOptions::default()
3510 },
3511 ))
3512 }
3513
3514 #[tokio::test]
3515 async fn writer_batches_ready_frames_into_one_flush() {
3516 const FRAME_COUNT: usize = 8;
3517
3518 let shared = writer_test_shared();
3519 let (live_writer, _live_rx) = mpsc::channel(1);
3520 let (tx, rx) = mpsc::channel(FRAME_COUNT + 1);
3521 let instrumented = InstrumentedWriter::new(false);
3522 let observer = instrumented.clone();
3523 let mut expected = Vec::with_capacity(FRAME_COUNT);
3524 let mut keys = Vec::with_capacity(FRAME_COUNT);
3525
3526 {
3527 let mut inner = shared.lock_inner();
3528 inner.writer = Some(live_writer);
3529 for index in 0..FRAME_COUNT {
3530 let corr = index as u64 + 1;
3531 let frame = response_frame(7, 1, corr, vec![index as u8; index + 1]);
3532 let key = PendingKey {
3533 generation: 1,
3534 channel: 7,
3535 epoch: 1,
3536 corr,
3537 };
3538 let (pending_tx, _pending_rx) = oneshot::channel();
3539 inner
3540 .pending
3541 .insert(key, PendingEntry::unary(pending_tx, false, None));
3542 expected.push(frame.clone());
3543 keys.push(key);
3544 tx.try_send(WriteCommand {
3545 frame,
3546 pending: Some(key),
3547 })
3548 .expect("the burst should fit in the writer queue");
3549 if index == 0 {
3550 tx.try_send(WriteCommand {
3551 frame: response_frame(7, 1, 999, b"skip".to_vec()),
3552 pending: Some(PendingKey {
3553 generation: 1,
3554 channel: 7,
3555 epoch: 1,
3556 corr: 999,
3557 }),
3558 })
3559 .expect("the skipped command should fit in the writer queue");
3560 }
3561 }
3562 }
3563 drop(tx);
3564
3565 writer_loop(Arc::clone(&shared), instrumented, rx, 1).await;
3566
3567 {
3568 let inner = shared.lock_inner();
3569 for key in keys {
3570 assert!(
3571 inner.pending.get(&key).is_some_and(|entry| entry.accepted),
3572 "every written command must be marked accepted"
3573 );
3574 }
3575 }
3576
3577 let mut wire = std::io::Cursor::new(observer.bytes());
3578 for expected_frame in expected {
3579 let actual = read_frame(&mut wire)
3580 .await
3581 .expect("the emitted frame should decode")
3582 .expect("the emitted frame should be present");
3583 assert_eq!(actual, expected_frame);
3584 }
3585 assert!(
3586 read_frame(&mut wire)
3587 .await
3588 .expect("the end of the emitted burst should be clean")
3589 .is_none(),
3590 "the writer must not emit extra frames"
3591 );
3592
3593 let flush_count = observer.flush_count();
3594 assert_eq!(
3595 flush_count, 1,
3596 "a ready burst must be coalesced into one flush"
3597 );
3598 shared.close_sync("test complete");
3599 }
3600
3601 #[tokio::test]
3602 async fn writer_flush_failure_drops_generation_and_preserves_acceptance_classification() {
3603 let shared = writer_test_shared();
3604 let (live_writer, _live_rx) = mpsc::channel(1);
3605 let accepted_key = PendingKey {
3606 generation: 1,
3607 channel: 3,
3608 epoch: 1,
3609 corr: 1,
3610 };
3611 let not_sent_key = PendingKey {
3612 corr: 2,
3613 ..accepted_key
3614 };
3615 let (accepted_tx, accepted_rx) = oneshot::channel();
3616 let (not_sent_tx, not_sent_rx) = oneshot::channel();
3617 {
3618 let mut inner = shared.lock_inner();
3619 inner.writer = Some(live_writer);
3620 inner
3621 .pending
3622 .insert(accepted_key, PendingEntry::unary(accepted_tx, false, None));
3623 inner
3624 .pending
3625 .insert(not_sent_key, PendingEntry::unary(not_sent_tx, false, None));
3626 }
3627
3628 let (tx, rx) = mpsc::channel(1);
3629 tx.send(WriteCommand {
3630 frame: response_frame(3, 1, accepted_key.corr, b"accepted".to_vec()),
3631 pending: Some(accepted_key),
3632 })
3633 .await
3634 .unwrap();
3635 drop(tx);
3636
3637 writer_loop(Arc::clone(&shared), InstrumentedWriter::new(true), rx, 1).await;
3638
3639 assert!(
3640 shared.lock_inner().writer.is_none(),
3641 "a flush failure must drop the active generation"
3642 );
3643 let accepted_error = accepted_rx
3644 .await
3645 .expect("the accepted request should be settled")
3646 .into_call_result()
3647 .unwrap_err();
3648 assert!(matches!(accepted_error, CallError::OutcomeUnknown(_)));
3649 let not_sent_error = not_sent_rx
3650 .await
3651 .expect("the unwritten request should be settled")
3652 .into_call_result()
3653 .unwrap_err();
3654 assert!(matches!(not_sent_error, CallError::NotSent(_)));
3655 shared.close_sync("test complete");
3656 }
3657
3658 #[test]
3659 fn reconnect_classifier_treats_auth_failure_as_transient() {
3660 let auth = ConsumerError::Auth {
3666 path: PathBuf::from("/tmp/subc-connection.json"),
3667 endpoint: "127.0.0.1:8757".to_string(),
3668 source: subc_transport::AuthError::InvalidServerProof,
3669 };
3670 assert!(is_reconnect_transient(&auth), "rotation race must retry");
3671
3672 let absent = ConsumerError::ConnectionFile {
3674 path: PathBuf::from("/tmp/subc-connection.json"),
3675 source: ConnectionFileError::Io {
3676 op: "read",
3677 path: PathBuf::from("/tmp/subc-connection.json"),
3678 source: io::Error::new(io::ErrorKind::NotFound, "gone"),
3679 },
3680 };
3681 assert!(is_reconnect_transient(&absent));
3682 }
3683
3684 #[tokio::test]
3685 async fn newer_drop_supersedes_reconnect_and_ignores_stale_completion() {
3686 let shared = Arc::new(Shared::new(
3687 PathBuf::from("/tmp/does-not-exist"),
3688 ConsumerOptions::default(),
3689 ));
3690 let stale_task = tokio::spawn(std::future::pending::<()>());
3691 {
3692 let mut inner = shared.lock_inner();
3693 inner.generation = 2;
3694 inner.reconnect = ReconnectState::Background {
3695 generation: 1,
3696 task: stale_task,
3697 };
3698 }
3699
3700 assert!(shared.spawn_reconnect(2));
3701 assert!(matches!(
3702 &shared.lock_inner().reconnect,
3703 ReconnectState::Background { generation, .. } if *generation == 2
3704 ));
3705
3706 shared.finish_background_reconnect(1);
3707 assert!(matches!(
3708 &shared.lock_inner().reconnect,
3709 ReconnectState::Background { generation, .. } if *generation == 2
3710 ));
3711 shared.close_sync("test complete");
3712 }
3713
3714 #[test]
3715 fn retryable_route_open_codes_are_code_specific() {
3716 for code in [
3717 "unknown_module",
3718 "module_reloading",
3719 "target_unavailable",
3720 "module_timeout",
3721 ] {
3722 assert!(is_retryable_route_open_code(code), "{code} should retry");
3723 }
3724 assert!(!is_retryable_route_open_code("invalid_project_root"));
3725 assert!(!is_retryable_route_open_code("route_rejected"));
3726 }
3727
3728 #[tokio::test]
3729 async fn close_route_flips_inflight_opening_so_a_racing_open_discards() {
3730 let shared = Arc::new(Shared::new(
3735 PathBuf::from("/tmp/does-not-exist"),
3736 ConsumerOptions::default(),
3737 ));
3738 let key = RouteKey::new(
3739 &RouteTarget::ToolProvider {
3740 module_id: "m".into(),
3741 },
3742 &BindIdentity {
3743 project_root: PathBuf::from("/tmp/p"),
3744 harness: "h".into(),
3745 session: "s".into(),
3746 },
3747 None,
3748 None,
3749 );
3750 shared.lock_inner().openings.insert(
3753 key.clone(),
3754 Opening {
3755 waiters: Vec::new(),
3756 closed: false,
3757 },
3758 );
3759
3760 shared
3763 .close_route(&key, &CloseRouteOptions::default())
3764 .await;
3765 assert!(
3766 shared
3767 .lock_inner()
3768 .openings
3769 .get(&key)
3770 .is_some_and(|o| o.closed),
3771 "close_route must flip the in-flight opening's closed flag (close-beats-reopen)"
3772 );
3773
3774 let absent = RouteKey::new(
3776 &RouteTarget::ToolProvider {
3777 module_id: "absent".into(),
3778 },
3779 &BindIdentity {
3780 project_root: PathBuf::from("/tmp/p"),
3781 harness: "h".into(),
3782 session: "s".into(),
3783 },
3784 None,
3785 None,
3786 );
3787 shared
3788 .close_route(&absent, &CloseRouteOptions::default())
3789 .await;
3790 }
3791
3792 #[test]
3793 fn route_key_is_structured() {
3794 let target = RouteTarget::InternalService {
3795 module_id: "a\0b".into(),
3796 service_id: "svc".into(),
3797 };
3798 let identity = BindIdentity {
3799 project_root: PathBuf::from("/tmp/project"),
3800 harness: "h".into(),
3801 session: "s".into(),
3802 };
3803 let key = RouteKey::new(&target, &identity, None, None);
3804 assert_eq!(key.project_root, PathBuf::from("/tmp/project"));
3805 assert!(matches!(key.target, RouteTargetKey::InternalService { .. }));
3806 }
3807
3808 #[tokio::test]
3809 async fn route_channel_index_tracks_lookup_close_and_generation_drop() {
3810 let shared = writer_test_shared();
3811 let (writer, _rx) = mpsc::channel(32);
3812 let mut expected = Vec::new();
3813 {
3814 let mut inner = shared.lock_inner();
3815 inner.writer = Some(writer);
3816 for channel in 1..=8 {
3817 let key = RouteKey::new(
3818 &RouteTarget::ToolProvider {
3819 module_id: format!("module-{channel}"),
3820 },
3821 &BindIdentity {
3822 project_root: PathBuf::from("/tmp/project"),
3823 harness: "test".into(),
3824 session: format!("session-{channel}"),
3825 },
3826 None,
3827 None,
3828 );
3829 let route = RouteState {
3830 handle: RouteHandle::new(channel, channel.into(), 1),
3831 sem: Arc::new(Semaphore::new(DEFAULT_ROUTE_WINDOW)),
3832 };
3833 inner.cache_route(key.clone(), route.clone());
3834 expected.push((key, route));
3835 }
3836 }
3837
3838 for (key, expected_route) in &expected {
3839 let resolved = shared
3840 .route_state(expected_route.handle)
3841 .expect("an indexed route handle should resolve");
3842 assert_eq!(resolved.handle, expected_route.handle);
3843 assert!(Arc::ptr_eq(&resolved.sem, &expected_route.sem));
3844
3845 let inner = shared.lock_inner();
3846 assert_eq!(
3847 inner.route_by_channel.get(&expected_route.handle.channel),
3848 Some(key)
3849 );
3850 assert_eq!(
3851 inner.route_epochs.get(&expected_route.handle.channel),
3852 Some(&expected_route.handle)
3853 );
3854 assert!(inner
3855 .routes
3856 .get(key)
3857 .is_some_and(|route| route.handle == expected_route.handle));
3858 }
3859
3860 let (closed_key, closed_route) = &expected[3];
3861 shared
3862 .close_route(closed_key, &CloseRouteOptions::default())
3863 .await;
3864 {
3865 let inner = shared.lock_inner();
3866 assert!(!inner.routes.contains_key(closed_key));
3867 assert!(!inner
3868 .route_by_channel
3869 .contains_key(&closed_route.handle.channel));
3870 assert!(!inner
3871 .route_epochs
3872 .contains_key(&closed_route.handle.channel));
3873 }
3874 assert!(matches!(
3875 shared.route_state(closed_route.handle),
3876 Err(CallError::StaleRouteHandle(handle)) if handle == closed_route.handle
3877 ));
3878
3879 shared.handle_generation_drop(1, "test generation dropped".into());
3880 {
3881 let inner = shared.lock_inner();
3882 assert!(inner.routes.is_empty());
3883 assert!(inner.route_by_channel.is_empty());
3884 assert!(inner.route_epochs.is_empty());
3885 }
3886 shared.close_sync("test complete");
3887 }
3888
3889 #[tokio::test]
3890 async fn stale_push_is_not_delivered_after_connection_generation_changes() {
3891 let shared = writer_test_shared();
3892 let old_handle = RouteHandle::new(7, 1, 1);
3893 let (writer, _writer_rx) = mpsc::channel(1);
3894 {
3895 let mut inner = shared.lock_inner();
3896 inner.writer = Some(writer);
3897 inner.route_epochs.insert(old_handle.channel, old_handle);
3898 }
3899 let mut pushes = shared
3900 .register_push_events(old_handle)
3901 .expect("the old live route should accept a receiver");
3902
3903 {
3904 let mut inner = shared.lock_inner();
3905 inner.generation = 2;
3906 inner.close_routes();
3907 inner.route_epochs.clear();
3908 }
3909 shared.route_push(old_handle, b"stale".to_vec());
3910
3911 assert!(
3912 pushes.recv().await.is_none(),
3913 "connection teardown must end the old receiver before a stale Push can arrive"
3914 );
3915 shared.close_sync("test complete");
3916 }
3917
3918 #[test]
3919 fn route_key_canonicalizes_consumer_capabilities() {
3920 let target = RouteTarget::ToolProvider {
3921 module_id: "aft".into(),
3922 };
3923 let identity = BindIdentity {
3924 project_root: PathBuf::from("/tmp/project"),
3925 harness: "h".into(),
3926 session: "s".into(),
3927 };
3928 let left = RouteKey::new(
3929 &target,
3930 &identity,
3931 None,
3932 Some(&["sampling".to_string(), "elicitation".to_string()]),
3933 );
3934 let right = RouteKey::new(
3935 &target,
3936 &identity,
3937 None,
3938 Some(&[
3939 "elicitation".to_string(),
3940 "sampling".to_string(),
3941 "sampling".to_string(),
3942 ]),
3943 );
3944 assert_eq!(left, right);
3945 }
3946
3947 #[test]
3948 fn drain_pending_channel_can_skip_subscriptions() {
3949 let mut pending = HashMap::new();
3950 let generation = 7;
3951 let channel = 11;
3952 let handle = RouteHandle::new(channel, 3, generation);
3953 let unary_key = PendingKey {
3954 generation,
3955 channel,
3956 epoch: handle.epoch,
3957 corr: 1,
3958 };
3959 let subscription_key = PendingKey {
3960 generation,
3961 channel,
3962 epoch: handle.epoch,
3963 corr: 2,
3964 };
3965 let (unary_tx, _unary_rx) = oneshot::channel();
3966 pending.insert(unary_key, PendingEntry::unary(unary_tx, false, None));
3967
3968 let (events_tx, _events_rx) = mpsc::channel(1);
3969 let (closed_tx, _closed_rx) = oneshot::channel();
3970 let permit = Arc::new(Semaphore::new(1))
3971 .try_acquire_owned()
3972 .expect("test semaphore permit should be available");
3973 pending.insert(
3974 subscription_key,
3975 PendingEntry::subscription(events_tx, closed_tx, permit, Priority::Interactive),
3976 );
3977
3978 let drained = drain_pending_handle(&mut pending, handle, false);
3979 assert_eq!(drained.len(), 1);
3980 assert!(pending.contains_key(&subscription_key));
3981
3982 let drained = drain_pending_handle(&mut pending, handle, true);
3983 assert_eq!(drained.len(), 1);
3984 assert!(pending.is_empty());
3985 }
3986
3987 fn response_frame(channel: u16, epoch: u32, corr: u64, body: Vec<u8>) -> Frame {
3988 Frame::build(
3989 FrameType::Response,
3990 Flags::new(false, Priority::Interactive, false),
3991 channel,
3992 epoch,
3993 corr,
3994 body,
3995 )
3996 .unwrap()
3997 }
3998
3999 #[tokio::test]
4000 async fn stale_epoch_ingress_drops_without_settling_matching_corr() {
4001 let shared = Arc::new(Shared::new(
4002 PathBuf::from("/tmp/does-not-exist"),
4003 ConsumerOptions::default(),
4004 ));
4005 let (writer, _rx) = mpsc::channel(4);
4006 let current = RouteHandle::new(9, 2, 1);
4007 let stale_key = PendingKey {
4008 generation: 1,
4009 channel: 9,
4010 epoch: 1,
4011 corr: 77,
4012 };
4013 let key = PendingKey {
4014 generation: 1,
4015 channel: 9,
4016 epoch: 2,
4017 corr: 77,
4018 };
4019 let (stale_tx, mut stale_response) = oneshot::channel();
4020 let (tx, mut response) = oneshot::channel();
4021 {
4022 let mut inner = shared.lock_inner();
4023 inner.writer = Some(writer);
4024 inner.route_epochs.insert(9, current);
4025 inner
4026 .pending
4027 .insert(stale_key, PendingEntry::unary(stale_tx, false, None));
4028 inner
4029 .pending
4030 .insert(key, PendingEntry::unary(tx, false, None));
4031 }
4032
4033 assert!(dispatch_frame(&shared, 1, response_frame(9, 1, 77, b"stale".to_vec())).await);
4034 assert!(matches!(
4035 response.try_recv(),
4036 Err(oneshot::error::TryRecvError::Empty)
4037 ));
4038 assert!(matches!(
4039 stale_response.try_recv(),
4040 Err(oneshot::error::TryRecvError::Empty)
4041 ));
4042 assert!(shared.lock_inner().pending.contains_key(&stale_key));
4043 assert!(shared.lock_inner().pending.contains_key(&key));
4044 assert_eq!(shared.lock_inner().dropped_route_frames, 1);
4045
4046 assert!(dispatch_frame(&shared, 1, response_frame(9, 2, 77, b"current".to_vec())).await);
4047 let PendingResult::Terminal(PendingTerminal::Response { body, .. }) =
4048 response.await.unwrap()
4049 else {
4050 panic!("current epoch must settle its own pending request");
4051 };
4052 assert_eq!(body, b"current");
4053 assert!(shared.lock_inner().pending.contains_key(&stale_key));
4054 }
4055
4056 #[tokio::test]
4057 async fn route_poll_response_must_echo_expected_handle_before_settling() {
4058 let shared = Arc::new(Shared::new(
4059 PathBuf::from("/tmp/does-not-exist"),
4060 ConsumerOptions::default(),
4061 ));
4062 let (writer, _rx) = mpsc::channel(4);
4063 let handle = RouteHandle::new(3, 9, 1);
4064 let key = PendingKey {
4065 generation: 1,
4066 channel: 0,
4067 epoch: 0,
4068 corr: 88,
4069 };
4070 let (tx, mut response) = oneshot::channel();
4071 {
4072 let mut inner = shared.lock_inner();
4073 inner.writer = Some(writer);
4074 inner.route_epochs.insert(handle.channel, handle);
4075 inner
4076 .pending
4077 .insert(key, PendingEntry::unary(tx, false, Some(handle)));
4078 }
4079 let wrong = serde_json::to_vec(&ClientControlResponse::RoutePoll {
4080 route_channel: handle.channel,
4081 route_epoch: handle.epoch + 1,
4082 status: Some("wrong".to_string()),
4083 live: Some(true),
4084 })
4085 .unwrap();
4086 assert!(dispatch_frame(&shared, 1, response_frame(0, 0, key.corr, wrong)).await);
4087 assert!(matches!(
4088 response.try_recv(),
4089 Err(oneshot::error::TryRecvError::Empty)
4090 ));
4091 assert!(shared.lock_inner().pending.contains_key(&key));
4092
4093 let correct = serde_json::to_vec(&ClientControlResponse::RoutePoll {
4094 route_channel: handle.channel,
4095 route_epoch: handle.epoch,
4096 status: Some("ready".to_string()),
4097 live: Some(true),
4098 })
4099 .unwrap();
4100 assert!(dispatch_frame(&shared, 1, response_frame(0, 0, key.corr, correct)).await);
4101 assert!(matches!(
4102 response.await.unwrap(),
4103 PendingResult::Terminal(PendingTerminal::Response { .. })
4104 ));
4105 }
4106
4107 #[tokio::test]
4108 async fn stale_connection_handle_emits_no_request_cancel_or_goodbye() {
4109 let shared = Arc::new(Shared::new(
4110 PathBuf::from("/tmp/does-not-exist"),
4111 ConsumerOptions::default(),
4112 ));
4113 let (writer, mut rx) = mpsc::channel(4);
4114 let stale = RouteHandle::new(4, 1, 1);
4115 let current = RouteHandle::new(4, 1, 2);
4116 {
4117 let mut inner = shared.lock_inner();
4118 inner.generation = 2;
4119 inner.writer = Some(writer);
4120 inner.route_epochs.insert(4, current);
4121 }
4122
4123 let err = shared
4124 .send_request(RequestSend {
4125 expected_handle: Some(stale),
4126 channel: stale.channel,
4127 epoch: stale.epoch,
4128 body: b"request".to_vec(),
4129 priority: Priority::Interactive,
4130 admission_class: AdmissionClass::Normal,
4131 deadline: Instant::now() + Duration::from_millis(10),
4132 retain_late_route_open: false,
4133 })
4134 .await
4135 .unwrap_err();
4136 assert!(matches!(err, CallError::StaleRouteHandle(handle) if handle == stale));
4137 shared.send_cancel(stale, 8, Priority::Interactive);
4138 assert!(!shared.send_route_goodbye(stale, false));
4139 assert!(
4140 rx.try_recv().is_err(),
4141 "stale operations must not queue frames"
4142 );
4143 }
4144
4145 #[tokio::test]
4146 async fn late_route_open_queues_goodbye_and_full_queue_closes_connection() {
4147 let shared = Arc::new(Shared::new(
4148 PathBuf::from("/tmp/does-not-exist"),
4149 ConsumerOptions::default(),
4150 ));
4151 let (writer, mut rx) = mpsc::channel(2);
4152 let key = PendingKey {
4153 generation: 1,
4154 channel: 0,
4155 epoch: 0,
4156 corr: 41,
4157 };
4158 let (tx, response) = oneshot::channel();
4159 drop(response);
4160 {
4161 let mut inner = shared.lock_inner();
4162 inner.writer = Some(writer);
4163 inner
4164 .pending
4165 .insert(key, PendingEntry::unary(tx, true, None));
4166 }
4167 let body = serde_json::to_vec(&ClientControlResponse::RouteOpen {
4168 route_channel: 12,
4169 route_epoch: 7,
4170 })
4171 .unwrap();
4172 assert!(dispatch_frame(&shared, 1, response_frame(0, 0, 41, body)).await);
4173 let cleanup = rx.recv().await.unwrap().frame;
4174 assert_eq!(cleanup.header.ty, FrameType::Goodbye);
4175 assert_eq!((cleanup.header.channel, cleanup.header.epoch), (12, 7));
4176
4177 let shared = Arc::new(Shared::new(
4178 PathBuf::from("/tmp/does-not-exist"),
4179 ConsumerOptions {
4180 reconnect_backoff: RetryBackoff {
4181 max_attempts: 1,
4182 ..RetryBackoff::default()
4183 },
4184 ..ConsumerOptions::default()
4185 },
4186 ));
4187 let (writer, _rx) = mpsc::channel(1);
4188 let filler = response_frame(0, 0, 1, Vec::new());
4189 writer
4190 .try_send(WriteCommand {
4191 frame: filler,
4192 pending: None,
4193 })
4194 .unwrap();
4195 let key = PendingKey {
4196 generation: 1,
4197 channel: 0,
4198 epoch: 0,
4199 corr: 42,
4200 };
4201 let (tx, response) = oneshot::channel();
4202 drop(response);
4203 {
4204 let mut inner = shared.lock_inner();
4205 inner.writer = Some(writer);
4206 inner
4207 .pending
4208 .insert(key, PendingEntry::unary(tx, true, None));
4209 }
4210 let body = serde_json::to_vec(&ClientControlResponse::RouteOpen {
4211 route_channel: 13,
4212 route_epoch: 8,
4213 })
4214 .unwrap();
4215 assert!(dispatch_frame(&shared, 1, response_frame(0, 0, 42, body)).await);
4216 assert!(shared.lock_inner().writer.is_none());
4217 }
4218
4219 #[tokio::test]
4220 async fn correlation_allocator_emits_max_once_then_closes_without_reuse() {
4221 let shared = Arc::new(Shared::new(
4222 PathBuf::from("/tmp/does-not-exist"),
4223 ConsumerOptions {
4224 reconnect_backoff: RetryBackoff {
4225 max_attempts: 1,
4226 ..RetryBackoff::default()
4227 },
4228 ..ConsumerOptions::default()
4229 },
4230 ));
4231 let (writer, mut rx) = mpsc::channel(4);
4232 {
4233 let mut inner = shared.lock_inner();
4234 inner.writer = Some(writer);
4235 inner.next_corr = Some(u64::MAX);
4236 }
4237 let request_shared = Arc::clone(&shared);
4238 let request = tokio::spawn(async move {
4239 request_shared
4240 .send_request(RequestSend {
4241 expected_handle: None,
4242 channel: 0,
4243 epoch: 0,
4244 body: Vec::new(),
4245 priority: Priority::Interactive,
4246 admission_class: AdmissionClass::Normal,
4247 deadline: Instant::now() + Duration::from_secs(1),
4248 retain_late_route_open: false,
4249 })
4250 .await
4251 });
4252 let command = rx.recv().await.unwrap();
4253 assert_eq!(command.frame.header.corr, u64::MAX);
4254 assert!(dispatch_frame(&shared, 1, response_frame(0, 0, u64::MAX, Vec::new()),).await);
4255 assert!(request.await.unwrap().is_ok());
4256
4257 let exhausted = shared
4258 .send_request(RequestSend {
4259 expected_handle: None,
4260 channel: 0,
4261 epoch: 0,
4262 body: Vec::new(),
4263 priority: Priority::Interactive,
4264 admission_class: AdmissionClass::Normal,
4265 deadline: Instant::now() + Duration::from_millis(10),
4266 retain_late_route_open: false,
4267 })
4268 .await
4269 .unwrap_err();
4270 assert!(matches!(exhausted, CallError::NotSent(_)));
4271 assert!(rx.try_recv().is_err());
4272 assert!(shared.lock_inner().writer.is_none());
4273 }
4274
4275 #[tokio::test]
4276 async fn managed_call_deadline_bounds_flow_control_wait() {
4277 let shared = Arc::new(Shared::new(
4278 PathBuf::from("/tmp/does-not-exist"),
4279 ConsumerOptions::default(),
4280 ));
4281 let (writer, mut rx) = mpsc::channel(4);
4282 let target = RouteTarget::ToolProvider {
4283 module_id: "flow-controlled".to_string(),
4284 };
4285 let identity = BindIdentity {
4286 project_root: PathBuf::from("/tmp/project"),
4287 harness: "test".to_string(),
4288 session: "deadline".to_string(),
4289 };
4290 let consumer_identity = Some(ConsumerIdentity {
4291 module_id: "caller".to_string(),
4292 launch_nonce: "nonce".to_string(),
4293 });
4294 let first_opts = CallOptions {
4295 timeout: Duration::from_secs(1),
4296 consumer_identity: consumer_identity.clone(),
4297 ..CallOptions::default()
4298 };
4299 let second_opts = CallOptions {
4300 timeout: Duration::from_millis(25),
4301 consumer_identity,
4302 ..CallOptions::default()
4303 };
4304 let key = RouteKey::new(
4305 &target,
4306 &identity,
4307 first_opts.consumer_identity.as_ref(),
4308 None,
4309 );
4310 let handle = RouteHandle::new(5, 3, 1);
4311 {
4312 let mut inner = shared.lock_inner();
4313 inner.writer = Some(writer);
4314 inner.cache_route(
4315 key,
4316 RouteState {
4317 handle,
4318 sem: Arc::new(Semaphore::new(1)),
4319 },
4320 );
4321 }
4322
4323 let first = tokio::spawn({
4324 let consumer = SubcConsumer {
4325 shared: Arc::clone(&shared),
4326 };
4327 let target = target.clone();
4328 let identity = identity.clone();
4329 async move {
4330 consumer
4331 .call(target, identity, b"first".to_vec(), first_opts)
4332 .await
4333 }
4334 });
4335 let first_frame = rx
4336 .recv()
4337 .await
4338 .expect("the first request should enter the fake daemon queue");
4339 assert_eq!(first_frame.frame.header.ty, FrameType::Request);
4340 assert_eq!(first_frame.frame.body, b"first");
4341 assert!(shared.mark_pending_accepted(
4342 first_frame
4343 .pending
4344 .expect("request commands retain their pending key"),
4345 ));
4346
4347 let consumer = SubcConsumer {
4348 shared: Arc::clone(&shared),
4349 };
4350 let result = tokio::time::timeout(
4351 Duration::from_millis(250),
4352 consumer.call(target, identity, b"second".to_vec(), second_opts),
4353 )
4354 .await
4355 .expect("a flow-controlled call must finish at its own deadline")
4356 .unwrap_err();
4357 assert!(matches!(result, CallError::NotSent(_)));
4358 assert!(
4359 rx.try_recv().is_err(),
4360 "the timed-out second request must not reach the fake daemon"
4361 );
4362
4363 first.abort();
4364 let _ = first.await;
4365 }
4366
4367 #[tokio::test]
4368 async fn admitted_route_open_emits_one_frame_without_retrying_daemon_errors() {
4369 let shared = Arc::new(Shared::new(
4370 PathBuf::from("/tmp/does-not-exist"),
4371 ConsumerOptions {
4372 call_timeout: Duration::from_secs(1),
4373 ..ConsumerOptions::default()
4374 },
4375 ));
4376 let (writer, mut rx) = mpsc::channel(4);
4377 {
4378 let mut inner = shared.lock_inner();
4379 inner.writer = Some(writer);
4380 }
4381
4382 let consumer = SubcConsumer {
4383 shared: Arc::clone(&shared),
4384 };
4385 let target = RouteTarget::ToolProvider {
4386 module_id: "admitted-target".to_string(),
4387 };
4388 let identity = BindIdentity {
4389 project_root: PathBuf::from("/tmp/project"),
4390 harness: "test".to_string(),
4391 session: "admitted".to_string(),
4392 };
4393 let task = tokio::spawn(async move {
4394 consumer
4395 .open_route_with_admission_facts(
4396 target,
4397 identity,
4398 serde_json::json!({"schema": 1, "verified_class": "member"}),
4399 )
4400 .await
4401 });
4402
4403 let command = rx.recv().await.expect("one route.open must be queued");
4404 let request: ClientControlRequest = serde_json::from_slice(&command.frame.body).unwrap();
4405 let ClientControlRequest::RouteOpen {
4406 admission_facts, ..
4407 } = request
4408 else {
4409 panic!("expected route.open")
4410 };
4411 assert_eq!(
4412 admission_facts,
4413 Some(serde_json::json!({"schema": 1, "verified_class": "member"}))
4414 );
4415
4416 let error_body = serde_json::to_vec(&ErrorBody {
4417 code: "admission_facts_not_permitted".to_string(),
4418 message: "not permitted".to_string(),
4419 })
4420 .unwrap();
4421 assert!(
4422 dispatch_frame(
4423 &shared,
4424 1,
4425 Frame::build(
4426 FrameType::Error,
4427 Flags::new(false, Priority::Interactive, false),
4428 0,
4429 0,
4430 command.frame.header.corr,
4431 error_body,
4432 )
4433 .unwrap(),
4434 )
4435 .await
4436 );
4437 let result = task.await.unwrap();
4438 assert!(matches!(result, Err(CallError::NotSent(_))));
4439 assert!(rx.try_recv().is_err(), "one-shot route.open must not retry");
4440 }
4441
4442 #[tokio::test]
4443 async fn route_open_waiter_deadline_is_not_sent_without_writing() {
4444 let shared = Arc::new(Shared::new(
4445 PathBuf::from("/tmp/does-not-exist"),
4446 ConsumerOptions::default(),
4447 ));
4448 let (writer, mut rx) = mpsc::channel(4);
4449 let target = RouteTarget::ToolProvider {
4450 module_id: "single-flight".to_string(),
4451 };
4452 let identity = BindIdentity {
4453 project_root: PathBuf::from("/tmp/project"),
4454 harness: "test".to_string(),
4455 session: "route-open".to_string(),
4456 };
4457 let opts = CallOptions {
4458 timeout: Duration::from_millis(25),
4459 consumer_identity: Some(ConsumerIdentity {
4460 module_id: "caller".to_string(),
4461 launch_nonce: "nonce".to_string(),
4462 }),
4463 ..CallOptions::default()
4464 };
4465 let key = RouteKey::new(&target, &identity, opts.consumer_identity.as_ref(), None);
4466 {
4467 let mut inner = shared.lock_inner();
4468 inner.writer = Some(writer);
4469 inner.openings.insert(
4470 key,
4471 Opening {
4472 waiters: Vec::new(),
4473 closed: false,
4474 },
4475 );
4476 }
4477
4478 let consumer = SubcConsumer { shared };
4479 let result = tokio::time::timeout(
4480 Duration::from_millis(250),
4481 consumer.open_route(target, identity, opts),
4482 )
4483 .await
4484 .expect("a route.open waiter must finish at its own deadline")
4485 .unwrap_err();
4486 assert!(matches!(result, CallError::NotSent(_)));
4487 assert!(
4488 rx.try_recv().is_err(),
4489 "a timed-out route.open waiter must not write a control frame"
4490 );
4491 }
4492
4493 #[tokio::test]
4494 async fn route_poll_deadline_bounds_writer_capacity() {
4495 let shared = Arc::new(Shared::new(
4496 PathBuf::from("/tmp/does-not-exist"),
4497 ConsumerOptions::default(),
4498 ));
4499 let (writer, mut rx) = mpsc::channel(1);
4500 let handle = RouteHandle::new(8, 4, 1);
4501 writer
4502 .try_send(WriteCommand {
4503 frame: response_frame(0, 0, 99, Vec::new()),
4504 pending: None,
4505 })
4506 .expect("the fake daemon queue should accept its filler frame");
4507 {
4508 let mut inner = shared.lock_inner();
4509 inner.writer = Some(writer);
4510 inner.route_epochs.insert(handle.channel, handle);
4511 }
4512
4513 let consumer = SubcConsumer { shared };
4514 let result = tokio::time::timeout(
4515 Duration::from_millis(250),
4516 consumer.poll_route(&handle, PollKind::Liveness, Duration::from_millis(25)),
4517 )
4518 .await
4519 .expect("a control request must finish at its own deadline")
4520 .unwrap_err();
4521 assert!(matches!(result, CallError::NotSent(_)));
4522 assert_eq!(
4523 rx.recv()
4524 .await
4525 .expect("the filler must still be the only queued frame")
4526 .frame
4527 .header
4528 .corr,
4529 99
4530 );
4531 assert!(
4532 rx.try_recv().is_err(),
4533 "the timed-out control request must not reach the fake daemon"
4534 );
4535 }
4536
4537 #[tokio::test]
4538 async fn subscription_deadline_bounds_writer_capacity() {
4539 let shared = Arc::new(Shared::new(
4540 PathBuf::from("/tmp/does-not-exist"),
4541 ConsumerOptions::default(),
4542 ));
4543 let (writer, mut rx) = mpsc::channel(1);
4544 let handle = RouteHandle::new(9, 2, 1);
4545 let route_sem = Arc::new(Semaphore::new(1));
4546 writer
4547 .try_send(WriteCommand {
4548 frame: response_frame(0, 0, 100, Vec::new()),
4549 pending: None,
4550 })
4551 .expect("the fake daemon queue should accept its filler frame");
4552 {
4553 let mut inner = shared.lock_inner();
4554 inner.writer = Some(writer);
4555 inner.cache_route(
4556 RouteKey::new(
4557 &RouteTarget::ToolProvider {
4558 module_id: "subscriptions".to_string(),
4559 },
4560 &BindIdentity {
4561 project_root: PathBuf::from("/tmp/project"),
4562 harness: "test".to_string(),
4563 session: "subscription".to_string(),
4564 },
4565 None,
4566 None,
4567 ),
4568 RouteState {
4569 handle,
4570 sem: Arc::clone(&route_sem),
4571 },
4572 );
4573 }
4574
4575 let consumer = SubcConsumer { shared };
4576 let result = tokio::time::timeout(
4577 Duration::from_millis(250),
4578 consumer.subscribe_route(
4579 &handle,
4580 b"subscribe".to_vec(),
4581 SubscribeOptions {
4582 route_open_timeout: Duration::from_millis(25),
4583 ..SubscribeOptions::default()
4584 },
4585 ),
4586 )
4587 .await
4588 .expect("a subscription must finish at its route-open deadline");
4589 let result = match result {
4590 Ok(_) => panic!("a subscription blocked before writing must time out"),
4591 Err(err) => err,
4592 };
4593 assert!(matches!(result, CallError::NotSent(_)));
4594 assert_eq!(
4595 rx.recv()
4596 .await
4597 .expect("the filler must still be the only queued frame")
4598 .frame
4599 .header
4600 .corr,
4601 100
4602 );
4603 assert!(
4604 rx.try_recv().is_err(),
4605 "the timed-out subscription must not reach the fake daemon"
4606 );
4607 assert!(
4608 route_sem.try_acquire().is_ok(),
4609 "a pre-write subscription timeout must release its route credit"
4610 );
4611 }
4612
4613 #[test]
4614 fn catalog_list_deserializes_golden_reply_and_ignores_unknown_fields() {
4615 let mut reply: serde_json::Value = serde_json::from_str(include_str!(
4616 "../../subc-control/tests/golden/client_control_response_catalog_list.json"
4617 ))
4618 .expect("the catalog.list golden reply must be valid JSON");
4619 reply["future_top_level"] = serde_json::json!(true);
4620 reply["modules"][0]["future_module_field"] = serde_json::json!("ignored");
4621
4622 let catalog: CatalogList =
4623 serde_json::from_value(reply).expect("catalog.list should tolerate additive fields");
4624 assert_eq!(catalog.generation, 7);
4625 assert_eq!(catalog.modules.len(), 1);
4626 assert!(catalog.subc_ops.iter().any(|op| op == "catalog.list"));
4627
4628 let tools = catalog.modules[0]
4629 .roles
4630 .iter()
4631 .find_map(|role| match role {
4632 subc_protocol::manifest::ProviderRole::ToolProvider { tools, .. } => Some(tools),
4633 _ => None,
4634 })
4635 .expect("the golden module must advertise a tool_provider role");
4636 let tool = tools
4637 .first()
4638 .expect("the golden tool_provider role must advertise a tool");
4639 assert!(!tool.name.is_empty());
4640 assert_eq!(
4641 tool.schema.get("type").and_then(serde_json::Value::as_str),
4642 Some("object")
4643 );
4644 assert!(matches!(
4645 tool.execution_mode,
4646 subc_protocol::manifest::ExecutionMode::Pure
4647 ));
4648 }
4649
4650 #[tokio::test]
4651 async fn catalog_list_sends_an_unfiltered_channel_zero_request() {
4652 let shared = Arc::new(Shared::new(
4653 PathBuf::from("/tmp/does-not-exist"),
4654 ConsumerOptions::default(),
4655 ));
4656 let (writer, mut rx) = mpsc::channel(1);
4657 shared.lock_inner().writer = Some(writer);
4658
4659 let consumer = SubcConsumer {
4660 shared: Arc::clone(&shared),
4661 };
4662 let request = tokio::spawn(async move { consumer.catalog_list().await });
4663 let command = rx
4664 .recv()
4665 .await
4666 .expect("catalog.list must queue a channel-0 request");
4667 assert_eq!(command.frame.header.channel, 0);
4668 let body: serde_json::Value = serde_json::from_slice(&command.frame.body).unwrap();
4669 assert_eq!(body["op"], "catalog.list");
4670 assert!(
4671 body.get("module_id").is_none(),
4672 "catalog.list must request the complete catalog without a module filter"
4673 );
4674
4675 let response = serde_json::to_vec(&ClientControlResponse::CatalogList {
4676 generation: 9,
4677 modules: Vec::new(),
4678 subc_ops: vec!["catalog.list".to_string()],
4679 })
4680 .unwrap();
4681 assert!(
4682 dispatch_frame(
4683 &shared,
4684 1,
4685 response_frame(0, 0, command.frame.header.corr, response),
4686 )
4687 .await
4688 );
4689 let catalog = request.await.unwrap().unwrap();
4690 assert_eq!(catalog.generation, 9);
4691 assert!(catalog.modules.is_empty());
4692 }
4693
4694 #[tokio::test]
4695 async fn catalog_list_deadline_is_not_sent_when_reconnection_stays_down() {
4696 let shared = Arc::new(Shared::new(
4697 PathBuf::from("/tmp/subc-client-rs-catalog-list-unavailable"),
4698 ConsumerOptions {
4699 call_timeout: Duration::from_millis(25),
4700 reconnect_backoff: RetryBackoff {
4701 base: Duration::from_millis(1),
4702 cap: Duration::from_millis(1),
4703 max_attempts: 100,
4704 },
4705 ..ConsumerOptions::default()
4706 },
4707 ));
4708 let consumer = SubcConsumer { shared };
4709 let result = tokio::time::timeout(Duration::from_millis(250), consumer.catalog_list())
4710 .await
4711 .expect("catalog.list must finish at its configured deadline")
4712 .unwrap_err();
4713 assert!(matches!(result, CallError::NotSent(_)));
4714 }
4715}