1use crate::diagnostics::{Diagnostics, RecoveryFailure, RecoveryPhase};
2use crate::endpoint::{RecoveryWake, RelayEndpoint};
3use crate::relay::CarrierId;
4pub mod error;
5mod stream;
6
7use std::fmt::Debug;
8use std::sync::{Arc, Mutex};
9use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
10
11use snafu::ResultExt;
12use tokio::task::JoinSet;
13use tokio::time::MissedTickBehavior;
14use tokio_util::sync::CancellationToken;
15use tracing::instrument;
16
17use self::error::{
18 ControlIoTimeoutSnafu, DecodeRegisterRespSnafu, DecodeStreamReqSnafu, EncodePingMsgSnafu,
19 EncodeRegisterReqSnafu, EncodeStreamAckMsgSnafu, ReadRegisterRespSnafu, ReadStreamReqSnafu,
20 RegisterRespNotMatchSnafu, SendRegisterReqSnafu, WritePingMsgSnafu, WriteStreamAckMsgSnafu,
21};
22use self::stream::{StreamConnect, handle_stream_on_endpoint};
23use crate::addr::resolve_tunnel_ends;
24use crate::recovery::{RecoveryTiming, jitter};
25use pb_mapper_core::checksum::{Credential, get_process_credential};
26use pb_mapper_core::config::{
27 ResolvedAddrs, control_conn_pool_size, control_heartbeat_interval, control_heartbeat_tolerance,
28 control_io_timeout, control_suspect_grace, registration_probe_timeout,
29 registration_reject_backoff,
30};
31use pb_mapper_core::timeout::RetryBackoff;
32use pb_mapper_core::{
33 snafu_error_get_or_continue, snafu_error_get_or_return, snafu_error_get_or_return_ok,
34 snafu_error_handle,
35};
36use pb_mapper_protocol::command::{
37 CONTROL_PROTOCOL_V2, LocalServer, MessageSerializer, PbConnRequest, PbConnResponse,
38 PbConnStatusReq, PbConnStatusResp, PbServerRequest,
39};
40use pb_mapper_protocol::forward::StreamForward;
41use pb_mapper_protocol::secure::ClientHeaderSession;
42use pb_mapper_protocol::{MessageReader, MessageWriter};
43use uni_stream::addr::ToSocketAddrs;
44use uni_stream::stream::StreamProvider;
45
46fn get_ping_message(protocol_version: u16, seq: u64) -> error::Result<Vec<u8>> {
47 if protocol_version >= CONTROL_PROTOCOL_V2 {
48 PbServerRequest::PingV2 { seq }
49 .encode()
50 .context(EncodePingMsgSnafu)
51 } else {
52 PbServerRequest::Ping.encode().context(EncodePingMsgSnafu)
53 }
54}
55
56#[derive(Debug)]
68enum Status {
69 ReadMsg,
70 SendPing,
71 ConnectRemote,
72 Resolve,
73 Timeout,
74 NetworkChanged,
75 Cancelled,
76 Rejected(String),
81 RejectedRetryable(String),
86}
87
88impl Status {
89 fn rejected(error: pb_mapper_protocol::command::PbErrorResponse) -> Self {
90 let reason = format!("{}: {}", error.code, error.message);
91 if error.retryable {
92 Self::RejectedRetryable(reason)
93 } else {
94 Self::Rejected(reason)
95 }
96 }
97}
98
99#[derive(Debug)]
106struct ControlBackoff {
107 transport: RetryBackoff,
110 reject: RetryBackoff,
113 timing: RecoveryTiming,
114}
115
116impl ControlBackoff {
117 fn new() -> Self {
118 let (reject_min, reject_max) = registration_reject_backoff();
119 Self {
120 transport: RetryBackoff::new(Duration::from_millis(100), Duration::from_secs(2)),
121 timing: RecoveryTiming::default(),
122 reject: RetryBackoff::new(reject_min, reject_max),
123 }
124 }
125
126 fn reset(&mut self) {
129 self.transport.reset();
130 self.reject.reset();
131 }
132}
133
134pub(crate) enum LocalControlWrite {
135 #[cfg(feature = "p2p")]
137 Direct(Box<pb_mapper_protocol::direct::DirectProviderMessage>),
138 Ping {
139 seq: u64,
140 },
141 StreamAck {
142 client_id: u32,
143 server_generation: u64,
144 },
145}
146
147#[derive(Debug, Clone, Copy)]
148struct ControlRegistration {
149 conn_id: u32,
150 generation: u64,
151 protocol_version: u16,
152 lease_ttl_ms: u64,
153}
154
155#[derive(Debug)]
156struct ControlLeaseState {
157 last_rx_at: Instant,
158 last_pong_at: Option<Instant>,
159}
160
161impl ControlLeaseState {
162 fn new() -> Self {
163 Self {
164 last_rx_at: Instant::now(),
165 last_pong_at: None,
166 }
167 }
168
169 fn record_rx(&mut self) {
170 self.last_rx_at = Instant::now();
171 }
172
173 fn record_pong(&mut self) {
174 let now = Instant::now();
175 self.last_rx_at = now;
176 self.last_pong_at = Some(now);
177 }
178
179 fn last_rx_age(&self) -> Duration {
180 self.last_rx_at.elapsed()
181 }
182}
183
184#[derive(Debug)]
185enum RegistrationProbeResult {
186 Present,
187 Missing,
188 Failed(String),
189}
190
191pub type StatusCallback = Box<dyn Fn(&str) + Send + Sync>;
193
194struct PoolStatus {
213 callback: StatusCallback,
214 workers: Mutex<PoolStatusState>,
215}
216
217struct PoolStatusState {
219 workers: Vec<WorkerStatus>,
220 published: Option<WorkerStatus>,
221}
222
223#[derive(Clone, Debug, Eq, PartialEq)]
225enum WorkerStatus {
226 Retrying,
228 Connected,
230 Failed(String),
232}
233
234impl PoolStatus {
235 fn new(callback: StatusCallback, pool_size: usize) -> Self {
236 Self {
237 callback,
238 workers: Mutex::new(PoolStatusState {
239 workers: vec![WorkerStatus::Retrying; pool_size],
240 published: None,
241 }),
242 }
243 }
244
245 fn report(&self, worker_index: usize, status: WorkerStatus) {
248 let next = {
249 let mut state = self
250 .workers
251 .lock()
252 .unwrap_or_else(|poisoned| poisoned.into_inner());
253 if let Some(slot) = state.workers.get_mut(worker_index) {
254 *slot = status;
255 }
256 let aggregate = Self::aggregate(&state.workers);
257 if state.published.as_ref() == Some(&aggregate) {
258 return;
259 }
260 state.published = Some(aggregate.clone());
261 aggregate
262 };
263 match next {
266 WorkerStatus::Connected => (self.callback)("connected"),
267 WorkerStatus::Retrying => (self.callback)("retrying"),
268 WorkerStatus::Failed(reason) => (self.callback)(&format!("failed: {reason}")),
269 }
270 }
271
272 fn aggregate(workers: &[WorkerStatus]) -> WorkerStatus {
277 let mut retrying = false;
278 let mut first_failure = None;
279 for status in workers {
280 match status {
281 WorkerStatus::Connected => return WorkerStatus::Connected,
282 WorkerStatus::Retrying => retrying = true,
283 WorkerStatus::Failed(reason) => {
284 first_failure.get_or_insert(reason);
285 }
286 }
287 }
288 match first_failure {
289 Some(reason) if !retrying => WorkerStatus::Failed(reason.clone()),
292 _ => WorkerStatus::Retrying,
293 }
294 }
295}
296
297#[derive(Clone, Copy, Debug)]
304pub struct ServerTunnelOptions {
305 pub need_codec: bool,
306 pub is_datagram: bool,
307 pub keep_alive: bool,
308 pub namespace: Option<u64>,
309 pub force_namespace: bool,
310}
311
312#[derive(Clone)]
313struct ServerCliRunConfig {
314 local_addr: ResolvedAddrs,
315 remote_addr: RelayEndpoint,
316 diagnostics: Diagnostics,
317 key: Arc<str>,
318 options: ServerTunnelOptions,
319 worker_index: usize,
320 credential: Credential,
321 idle_timeout: Option<Duration>,
322 #[cfg(feature = "p2p")]
323 direct: Option<Arc<crate::direct::Provider>>,
324}
325
326impl Debug for ServerCliRunConfig {
327 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
328 formatter
329 .debug_struct("ServerCliRunConfig")
330 .field("local_addr", &self.local_addr)
331 .field("remote_addr", &self.remote_addr)
332 .field("key", &self.key)
333 .field("options", &self.options)
334 .field("worker_index", &self.worker_index)
335 .field("credential_key_id", &self.credential.key_id())
336 .finish()
337 }
338}
339
340fn duration_to_millis(duration: Duration) -> u64 {
341 duration.as_millis().min(u128::from(u64::MAX)) as u64
342}
343
344fn new_client_instance_id(worker_index: usize) -> String {
345 let now_ms = SystemTime::now()
346 .duration_since(UNIX_EPOCH)
347 .map(duration_to_millis)
348 .unwrap_or_default();
349 format!("{}-{worker_index}-{now_ms}", std::process::id())
350}
351
352async fn probe_remote_registration(
353 remote_addr: ResolvedAddrs,
354 endpoint: RelayEndpoint,
355 key: Arc<str>,
356 registration: ControlRegistration,
357 namespace: Option<u64>,
358 credential: Credential,
359) -> RegistrationProbeResult {
360 let timeout = registration_probe_timeout();
361 let result = tokio::time::timeout(timeout, async {
362 let mut stream = endpoint
363 .connect_addresses(&remote_addr, credential, &mut None)
364 .await
365 .map_err(|e| format!("connect remote status stream failed: {e}"))?;
366 crate::client::status::get_status_with_credential(
367 &mut stream,
368 PbConnStatusReq::Service {
369 direct_query: None,
370 key: key.to_string(),
371 },
372 namespace,
373 &credential,
374 )
375 .await
376 .map_err(|e| {
377 format!(
378 "get service status failed: {}",
379 snafu::Report::from_error(e)
380 )
381 })
382 })
383 .await;
384
385 let status = match result {
386 Ok(Ok(status)) => status,
387 Ok(Err(reason)) => return RegistrationProbeResult::Failed(reason),
388 Err(_) => {
389 return RegistrationProbeResult::Failed(format!(
390 "status probe timed out after {timeout:?}"
391 ));
392 }
393 };
394
395 match status {
396 PbConnStatusResp::Service { connections, .. } => {
397 let present = connections.iter().any(|conn| {
398 conn.conn_id == registration.conn_id
399 && conn.generation == registration.generation
400 && conn.healthy
401 });
402 if present {
403 RegistrationProbeResult::Present
404 } else {
405 RegistrationProbeResult::Missing
406 }
407 }
408 PbConnStatusResp::Keys(keys) => {
409 if keys.iter().any(|candidate| candidate == key.as_ref()) {
410 RegistrationProbeResult::Present
411 } else {
412 RegistrationProbeResult::Missing
413 }
414 }
415 other => RegistrationProbeResult::Failed(format!(
416 "unexpected status response while probing registration: {other:?}"
417 )),
418 }
419}
420
421pub async fn run_server_side_cli<LocalStream, A>(
422 local_addr: A,
423 remote_addr: A,
424 key: Arc<str>,
425 options: ServerTunnelOptions,
426) where
427 LocalStream: StreamProvider + Send + 'static,
428 LocalStream::Item: StreamForward,
429 A: ToSocketAddrs,
430{
431 run_server_side_cli_with_callback::<LocalStream, A>(local_addr, remote_addr, key, options, None)
432 .await
433}
434
435pub async fn run_server_side_cli_with_pinned_credential<LocalStream, A>(
436 local_addr: A,
437 remote_addr: A,
438 key: Arc<str>,
439 options: ServerTunnelOptions,
440 status_callback: Option<StatusCallback>,
441 credential: Credential,
442) where
443 LocalStream: StreamProvider + Send + 'static,
444 LocalStream::Item: StreamForward,
445 A: ToSocketAddrs,
446{
447 let Some((local_addr, remote_addr)) = resolve_tunnel_ends(local_addr, remote_addr).await else {
448 return;
449 };
450 run_server_side_cli_pool::<LocalStream>(
451 local_addr,
452 remote_addr,
453 key,
454 options,
455 status_callback,
456 Some(credential),
457 CancellationToken::new(),
458 )
459 .await;
460}
461
462pub async fn run_server_side_cli_with_shutdown<LocalStream>(
468 local_addr: ResolvedAddrs,
469 remote_addr: ResolvedAddrs,
470 key: Arc<str>,
471 options: ServerTunnelOptions,
472 status_callback: Option<StatusCallback>,
473 credential: Credential,
474 shutdown: CancellationToken,
475) where
476 LocalStream: StreamProvider + Send + 'static,
477 LocalStream::Item: StreamForward,
478{
479 run_server_side_cli_pool::<LocalStream>(
480 local_addr,
481 remote_addr,
482 key,
483 options,
484 status_callback,
485 Some(credential),
486 shutdown,
487 )
488 .await;
489}
490
491pub async fn run_server_side_cli_with_callback<LocalStream, A>(
492 local_addr: A,
493 remote_addr: A,
494 key: Arc<str>,
495 options: ServerTunnelOptions,
496 status_callback: Option<StatusCallback>,
497) where
498 LocalStream: StreamProvider + Send + 'static,
499 LocalStream::Item: StreamForward,
500 A: ToSocketAddrs,
501{
502 let Some((local_addr, remote_addr)) = resolve_tunnel_ends(local_addr, remote_addr).await else {
503 return;
504 };
505 run_server_side_cli_pool::<LocalStream>(
506 local_addr,
507 remote_addr,
508 key,
509 options,
510 status_callback,
511 None,
512 CancellationToken::new(),
513 )
514 .await;
515}
516
517async fn resolve_registration_credential(
518 pinned: Option<Credential>,
519 shutdown: &CancellationToken,
520) -> Option<Credential> {
521 if let Some(credential) = pinned {
522 return Some(credential);
523 }
524 let mut retry_backoff = RetryBackoff::default();
525 loop {
526 if shutdown.is_cancelled() {
527 return None;
528 }
529 match get_process_credential() {
530 Ok(credential) => return Some(credential),
531 Err(error) => {
532 tracing::error!("load registration credential failed: {error}");
533 tokio::select! {
534 () = shutdown.cancelled() => return None,
535 () = tokio::time::sleep(retry_backoff.next_delay()) => {}
536 }
537 }
538 }
539 }
540}
541
542async fn run_server_side_cli_pool<LocalStream>(
543 local_addr: ResolvedAddrs,
544 remote_addr: ResolvedAddrs,
545 key: Arc<str>,
546 options: ServerTunnelOptions,
547 status_callback: Option<StatusCallback>,
548 pinned_credential: Option<Credential>,
549 shutdown: CancellationToken,
550) where
551 LocalStream: StreamProvider + Send + 'static,
552 LocalStream::Item: StreamForward,
553{
554 run_server_side_cli_recovering::<LocalStream>(
555 local_addr,
556 RelayEndpoint::fixed(remote_addr),
557 key,
558 options,
559 status_callback,
560 pinned_credential,
561 shutdown,
562 Diagnostics::default(),
563 None,
564 false,
565 )
566 .await;
567}
568
569#[allow(clippy::too_many_arguments)]
570pub(crate) async fn run_server_side_cli_recovering<LocalStream>(
571 local_addr: ResolvedAddrs,
572 remote_addr: RelayEndpoint,
573 key: Arc<str>,
574 options: ServerTunnelOptions,
575 status_callback: Option<StatusCallback>,
576 pinned_credential: Option<Credential>,
577 shutdown: CancellationToken,
578 diagnostics: Diagnostics,
579 idle_timeout: Option<Duration>,
580 allow_direct: bool,
581) where
582 LocalStream: StreamProvider + Send + 'static,
583 LocalStream::Item: StreamForward,
584{
585 let Some(credential) = resolve_registration_credential(pinned_credential, &shutdown).await
586 else {
587 return;
588 };
589 remote_addr.start();
590 let pool_size = control_conn_pool_size().max(1);
591 tracing::info!(
592 event = "local_server_control_pool_starting",
593 key = %key,
594 pool_size,
595 "starting local server control connection pool"
596 );
597 #[cfg(feature = "p2p")]
598 let direct = allow_direct.then(|| {
599 crate::direct::Provider::spawn::<LocalStream>(
600 local_addr.clone(),
601 key.clone(),
602 options.is_datagram,
603 shutdown.clone(),
604 idle_timeout,
605 diagnostics.clone(),
606 )
607 });
608 #[cfg(not(feature = "p2p"))]
609 let _ = allow_direct;
610 let mut workers = JoinSet::new();
611 let pool_status =
615 status_callback.map(|callback| Arc::new(PoolStatus::new(callback, pool_size)));
616 for worker_index in 0..pool_size {
617 let worker_key = key.clone();
618 let worker_status = pool_status.clone();
619 let worker_shutdown = shutdown.clone();
620 let worker_local = local_addr.clone();
621 let worker_remote = remote_addr.clone();
622 let worker_diagnostics = diagnostics.clone();
623 #[cfg(feature = "p2p")]
624 let worker_direct = direct.clone();
625 workers.spawn(async move {
626 run_server_side_cli_worker::<LocalStream>(
627 worker_local,
628 worker_remote,
629 worker_key,
630 options,
631 worker_status,
632 worker_index,
633 credential,
634 worker_shutdown,
635 worker_diagnostics,
636 idle_timeout,
637 #[cfg(feature = "p2p")]
638 worker_direct,
639 )
640 .await;
641 });
642 }
643 while let Some(result) = workers.join_next().await {
644 if let Err(e) = result {
645 tracing::warn!(
646 event = "local_server_control_worker_join_failed",
647 error = %e,
648 "local server control worker join failed"
649 );
650 }
651 }
652 #[cfg(feature = "p2p")]
653 if let Some(direct) = direct {
654 direct.stop().await;
655 }
656}
657
658#[allow(clippy::too_many_arguments)]
659async fn run_server_side_cli_worker<LocalStream>(
660 local_addr: ResolvedAddrs,
661 remote_addr: RelayEndpoint,
662 key: Arc<str>,
663 options: ServerTunnelOptions,
664 pool_status: Option<Arc<PoolStatus>>,
665 worker_index: usize,
666 credential: Credential,
667 shutdown: CancellationToken,
668 diagnostics: Diagnostics,
669 idle_timeout: Option<Duration>,
670 #[cfg(feature = "p2p")] direct: Option<Arc<crate::direct::Provider>>,
671) where
672 LocalStream: StreamProvider + Send + 'static,
673 LocalStream::Item: StreamForward,
674{
675 let report = |status: WorkerStatus| {
676 if let Some(ref pool_status) = pool_status {
677 pool_status.report(worker_index, status);
678 }
679 };
680 let mut backoff = ControlBackoff::new();
681 let run_config = ServerCliRunConfig {
682 local_addr: local_addr.clone(),
683 remote_addr: remote_addr.clone(),
684 diagnostics: diagnostics.clone(),
685 key: key.clone(),
686 options,
687 worker_index,
688 credential,
689 idle_timeout,
690 #[cfg(feature = "p2p")]
691 direct: direct.clone(),
692 };
693 let mut stream_tasks = JoinSet::new();
697 let setup_slots = remote_addr.data_slots();
698 let mut wake = remote_addr.wake();
699 diagnostics.network_seen(remote_addr.network_generation());
700 'outer: loop {
701 if shutdown.is_cancelled() {
702 break 'outer;
703 }
704 wake.acknowledge();
705 diagnostics.attempt();
706 let mut carrier = None;
707 let status = if let Err(status) = run_server_side_cli_inner::<LocalStream>(
708 &mut backoff,
709 run_config.clone(),
710 &report,
711 shutdown.clone(),
712 &mut stream_tasks,
713 &setup_slots,
714 &mut wake,
715 &mut carrier,
716 )
717 .await
718 {
719 status
720 } else {
721 if shutdown.is_cancelled() {
722 break 'outer;
723 }
724 tracing::warn!(
725 event = "local_server_control_worker_finished",
726 key = %key,
727 worker_index,
728 "local server control worker finished without an error; reconnecting"
729 );
730 Status::ReadMsg
731 };
732 if matches!(status, Status::Cancelled) {
733 break 'outer;
734 }
735 let failure = match &status {
737 Status::Resolve => RecoveryFailure::Dns,
738 Status::Timeout => RecoveryFailure::Timeout,
739 Status::Rejected(_) | Status::RejectedRetryable(_) => RecoveryFailure::Rejected,
740 Status::NetworkChanged => RecoveryFailure::NetworkChanged,
741 _ => RecoveryFailure::Transport,
742 };
743 if matches!(
744 failure,
745 RecoveryFailure::Transport | RecoveryFailure::Timeout | RecoveryFailure::Dns
746 ) {
747 remote_addr.transport_failed(carrier);
748 }
749 let (retry_interval, retry_count) = match status {
750 Status::Cancelled => break 'outer,
751 #[cfg(feature = "p2p")]
752 Status::Rejected(_)
753 if direct
754 .as_ref()
755 .is_some_and(|provider| provider.has_active()) =>
756 {
757 (Duration::from_secs(30), 0)
759 }
760 Status::Rejected(reason) => {
761 diagnostics.failed(RecoveryFailure::Rejected, Duration::ZERO);
762 tracing::error!(
763 event = "local_server_registration_rejected_permanently",
764 key = %key,
765 worker_index,
766 reason = %reason,
767 "pb server permanently rejected this registration; not reconnecting"
768 );
769 report(WorkerStatus::Failed(reason));
770 break 'outer;
771 }
772 Status::RejectedRetryable(ref reason) => {
773 let interval = backoff.reject.next_delay();
774 tracing::debug!(
775 event = "local_server_registration_rejected_retryable",
776 key = %key,
777 worker_index,
778 reason = %reason,
779 retry_delay = ?interval,
780 retry_count = backoff.reject.failures(),
781 "pb server rejected this registration for a condition that may clear"
782 );
783 (interval, backoff.reject.failures())
784 }
785 Status::NetworkChanged => {
786 diagnostics.network_changed(remote_addr.network_generation());
787 backoff.transport.reset();
788 (Duration::ZERO, 0)
789 }
790 Status::ReadMsg
791 | Status::SendPing
792 | Status::ConnectRemote
793 | Status::Resolve
794 | Status::Timeout => (
795 jitter(backoff.transport.next_delay()),
796 backoff.transport.failures(),
797 ),
798 };
799 if diagnostics.failed(failure, retry_interval) {
800 tracing::info!(
801 event = "local_server_control_reconnect_scheduled",
802 key = %key,
803 worker_index,
804 local_addr = ?local_addr,
805 remote_addr = ?remote_addr,
806 status = ?status,
807 retry_delay = ?retry_interval,
808 retry_count,
809 "local server control connection will reconnect"
810 );
811 }
812 report(WorkerStatus::Retrying);
813
814 tokio::select! {
815 () = shutdown.cancelled() => break 'outer,
816 () = tokio::time::sleep(retry_interval) => {},
817 () = wake.changed(), if !matches!(failure, RecoveryFailure::Rejected) => {
818 diagnostics.network_changed(remote_addr.network_generation());
819 backoff.transport.reset();
820 }
821 }
822 if shutdown.is_cancelled() {
823 break 'outer;
824 }
825 }
826
827 stream_tasks.shutdown().await;
830}
831
832#[allow(clippy::too_many_arguments)]
835#[instrument(skip(backoff, report, shutdown, stream_tasks, setup_slots, wake, carrier))]
836async fn run_server_side_cli_inner<LocalStream: StreamProvider>(
837 backoff: &mut ControlBackoff,
838 config: ServerCliRunConfig,
839 report: &impl Fn(WorkerStatus),
840 shutdown: CancellationToken,
841 stream_tasks: &mut JoinSet<()>,
842 setup_slots: &Arc<tokio::sync::Semaphore>,
843 wake: &mut RecoveryWake,
844 carrier: &mut Option<CarrierId>,
845) -> std::result::Result<(), Status>
846where
847 LocalStream::Item: StreamForward,
848{
849 let ServerCliRunConfig {
850 local_addr,
851 remote_addr,
852 diagnostics,
853 key,
854 options:
855 ServerTunnelOptions {
856 need_codec,
857 is_datagram,
858 keep_alive,
859 namespace,
860 force_namespace,
861 },
862 worker_index,
863 credential,
864 idle_timeout,
865 #[cfg(feature = "p2p")]
866 direct,
867 } = config;
868 let control_permit = tokio::select! {
869 () = shutdown.cancelled() => return Err(Status::Cancelled),
870 () = wake.network_changed() => return Err(Status::NetworkChanged),
871 permit = remote_addr.control_permit() => permit,
872 };
873 let started = tokio::time::Instant::now();
874 let timeout = backoff.timing.timeout();
875 let deadline = started + timeout;
876 diagnostics.phase(RecoveryPhase::Resolving);
877 let addresses = tokio::select! {
878 () = shutdown.cancelled() => return Err(Status::Cancelled),
879 () = wake.network_changed() => return Err(Status::NetworkChanged),
880 result = tokio::time::timeout_at(deadline, remote_addr.addresses()) => match result {
881 Ok(Ok(addresses)) => addresses,
882 _ => return Err(Status::Resolve),
883 },
884 };
885 diagnostics.phase(RecoveryPhase::Connecting);
886 let mut manager_stream = tokio::select! {
887 () = shutdown.cancelled() => return Err(Status::Cancelled),
888 () = wake.network_changed() => return Err(Status::NetworkChanged),
889 result = tokio::time::timeout_at(deadline, remote_addr.connect_addresses(&addresses, credential, carrier)) => {
890 match result {
891 Ok(Ok(stream)) => stream,
892 Ok(Err(error)) => {
893 tracing::debug!(event = "local_server_dial_failed", %error, %key, worker_index);
894 if let crate::relay::ConnectError::Rejected { response } = error {
895 remote_addr.protocol_succeeded();
896 diagnostics.responded(started.elapsed());
897 backoff.timing.record(started.elapsed());
898 return Err(Status::rejected(response));
899 }
900 return Err(Status::ConnectRemote);
901 }
902 Err(_) => {
903 backoff.timing.timed_out();
904 tracing::debug!(event = "local_server_setup_timeout", %key, worker_index, ?timeout, phase = "dial");
905 return Err(Status::Timeout);
906 }
907 }
908 }
909 };
910 tracing::debug!(
911 event = "local_server_connected_remote",
912 key = %key,
913 worker_index,
914 local_addr = %local_addr,
915 remote_addr = %remote_addr,
916 need_codec,
917 is_datagram,
918 "local server connected to pb server"
919 );
920
921 snafu_error_handle!(manager_stream.configure(keep_alive));
922
923 let registered_addr = manager_stream
924 .relay_addr()
925 .map(ResolvedAddrs::from)
926 .unwrap_or(addresses);
927 diagnostics.phase(RecoveryPhase::Handshake);
928 let session = match ClientHeaderSession::new_v2(&credential) {
932 Ok(session) => session,
933 Err(error) => {
934 tracing::error!("create manager protocol-v2 session failed: {error}");
935 return Err(Status::ConnectRemote);
936 }
937 };
938 let heartbeat_interval = control_heartbeat_interval();
939 let heartbeat_tolerance = control_heartbeat_tolerance();
940 #[cfg(feature = "p2p")]
941 let direct_version = direct
942 .as_ref()
943 .map(|_| pb_mapper_protocol::direct::DIRECT_VERSION);
944 #[cfg(not(feature = "p2p"))]
945 let direct_version = None;
946 let request = match namespace {
947 Some(namespace) => PbConnRequest::RegisterScoped {
948 direct: direct_version,
949 key: key.to_string(),
950 namespace,
951 force_namespace,
952 need_codec,
953 is_datagram,
954 protocol_version: Some(CONTROL_PROTOCOL_V2),
955 client_instance_id: Some(new_client_instance_id(worker_index)),
956 heartbeat_interval_ms: Some(duration_to_millis(heartbeat_interval)),
957 heartbeat_tolerance_ms: Some(duration_to_millis(heartbeat_tolerance)),
958 },
959 None => PbConnRequest::Register {
960 direct: direct_version,
961 key: key.to_string(),
962 need_codec,
963 is_datagram,
964 protocol_version: Some(CONTROL_PROTOCOL_V2),
965 client_instance_id: Some(new_client_instance_id(worker_index)),
966 heartbeat_interval_ms: Some(duration_to_millis(heartbeat_interval)),
967 heartbeat_tolerance_ms: Some(duration_to_millis(heartbeat_tolerance)),
968 },
969 };
970 let msg = snafu_error_get_or_return_ok!(request.encode().context(EncodeRegisterReqSnafu));
971 tokio::select! {
972 () = shutdown.cancelled() => return Err(Status::Cancelled),
973 () = wake.network_changed() => return Err(Status::NetworkChanged),
974 result = tokio::time::timeout_at(deadline, session.write_initial(&mut manager_stream, &msg)) => {
975 match result {
976 Ok(result) => snafu_error_get_or_return_ok!(result.context(SendRegisterReqSnafu)),
977 Err(_) => {
978 backoff.timing.timed_out();
979 tracing::debug!(event = "local_server_setup_timeout", %key, worker_index, ?timeout, phase = "write");
980 return Err(Status::Timeout);
981 }
982 }
983 }
984 }
985 let (mut reader, mut writer) = manager_stream.into_split();
986 let mut msg_reader = match session.response_reader(&mut reader) {
987 Ok(reader) => reader,
988 Err(e) => {
989 tracing::error!("create manager header reader failed: {e}");
990 return Err(Status::ReadMsg);
991 }
992 };
993 let (key, registration) = {
995 let msg = tokio::select! {
996 () = shutdown.cancelled() => return Err(Status::Cancelled),
997 () = wake.network_changed() => return Err(Status::NetworkChanged),
998 result = tokio::time::timeout_at(deadline, msg_reader.read_msg()) => {
999 match result {
1000 Ok(result) => snafu_error_get_or_return_ok!(result.context(ReadRegisterRespSnafu)),
1001 Err(_) => {
1002 backoff.timing.timed_out();
1003 tracing::debug!(event = "local_server_setup_timeout", %key, worker_index, ?timeout, phase = "response");
1004 return Err(Status::Timeout);
1005 }
1006 }
1007 }
1008 };
1009 let resp = snafu_error_get_or_return_ok!(
1010 PbConnResponse::decode(msg).context(DecodeRegisterRespSnafu)
1011 );
1012 remote_addr.protocol_succeeded();
1013 diagnostics.responded(started.elapsed());
1014 backoff.timing.record(started.elapsed());
1015 let registration = match resp {
1016 PbConnResponse::RegisterV2 {
1017 conn_id,
1018 generation,
1019 lease_ttl_ms,
1020 ..
1021 } => ControlRegistration {
1022 conn_id,
1023 generation,
1024 protocol_version: CONTROL_PROTOCOL_V2,
1025 lease_ttl_ms,
1026 },
1027 PbConnResponse::Register(conn_id) => ControlRegistration {
1028 conn_id,
1029 generation: 0,
1030 protocol_version: 1,
1031 lease_ttl_ms: 0,
1032 },
1033 PbConnResponse::Error(error) => {
1039 tracing::debug!(
1040 event = "local_server_registration_rejected",
1041 key = %key,
1042 worker_index,
1043 reason = %error.code,
1044 retryable = error.retryable,
1045 message = %error.message,
1046 "pb server rejected service registration"
1047 );
1048 return Err(Status::rejected(error));
1049 }
1050 _ => snafu_error_get_or_return_ok!(RegisterRespNotMatchSnafu {}.fail()),
1051 };
1052 tracing::info!(
1053 event = "local_server_registered",
1054 key = %key,
1055 conn_id = %registration.conn_id,
1056 generation = registration.generation,
1057 protocol_version = registration.protocol_version,
1058 lease_ttl_ms = registration.lease_ttl_ms,
1059 worker_index,
1060 local_addr = %local_addr,
1061 remote_addr = %remote_addr,
1062 "local server registered with pb server"
1063 );
1064
1065 report(WorkerStatus::Connected);
1068 (key, registration)
1069 };
1070
1071 drop(control_permit);
1072 diagnostics.succeeded(started.elapsed());
1073 remote_addr.protocol_succeeded();
1074 backoff.timing.record(started.elapsed());
1075 tracing::debug!(event = "local_server_setup_latency", elapsed_ms = duration_to_millis(started.elapsed()), next_timeout_ms = duration_to_millis(backoff.timing.timeout()), %key, worker_index);
1076 backoff.reset();
1077 let (write_tx, mut write_rx) = tokio::sync::mpsc::channel::<LocalControlWrite>(64);
1078 #[cfg(feature = "p2p")]
1079 if let Some(direct) = &direct {
1080 direct.connected(write_tx.clone());
1081 }
1082 let lease_state = Arc::new(tokio::sync::Mutex::new(ControlLeaseState::new()));
1083 let writer_key = key.clone();
1084 let writer_registration = registration;
1085 let mut writer_handle = tokio_util::task::AbortOnDropHandle::new(tokio::spawn(async move {
1086 let mut msg_writer = match session.continuation_writer(&mut writer) {
1087 Ok(writer) => writer,
1088 Err(e) => {
1089 tracing::error!("create manager header writer failed: {e}");
1090 return Err(Status::SendPing);
1091 }
1092 };
1093 loop {
1094 let Some(cmd) = write_rx.recv().await else {
1095 return Ok(());
1096 };
1097 match cmd {
1098 #[cfg(feature = "p2p")]
1099 LocalControlWrite::Direct(message) => {
1100 let payload = PbServerRequest::Direct(*message)
1101 .encode()
1102 .map_err(|_| Status::SendPing)?;
1103 tokio::time::timeout(control_io_timeout(), msg_writer.write_msg(&payload))
1104 .await
1105 .map_err(|_| Status::SendPing)?
1106 .map_err(|_| Status::SendPing)?;
1107 }
1108 LocalControlWrite::Ping { seq } => {
1109 snafu_error_get_or_return!(
1110 handle_ping_interval(
1111 &mut msg_writer,
1112 writer_key.clone(),
1113 writer_registration,
1114 seq
1115 )
1116 .await,
1117 "[send ping]",
1118 Err(Status::SendPing)
1119 );
1120 tracing::debug!(
1121 event = "local_server_heartbeat_sent",
1122 key = %writer_key,
1123 conn_id = %writer_registration.conn_id,
1124 generation = writer_registration.generation,
1125 protocol_version = writer_registration.protocol_version,
1126 seq,
1127 interval = ?control_heartbeat_interval(),
1128 "local server heartbeat sent"
1129 );
1130 }
1131 LocalControlWrite::StreamAck {
1132 client_id,
1133 server_generation,
1134 } => {
1135 snafu_error_get_or_return!(
1136 write_stream_ack(&mut msg_writer, client_id, server_generation).await,
1137 "[send stream ack]",
1138 Err(Status::SendPing)
1139 );
1140 }
1141 }
1142 }
1143 }));
1144
1145 let heartbeat_interval = control_heartbeat_interval();
1146 let heartbeat_tolerance = control_heartbeat_tolerance();
1147 let suspect_grace = control_suspect_grace();
1148 let mut heartbeat = tokio::time::interval(heartbeat_interval);
1149 heartbeat.set_missed_tick_behavior(MissedTickBehavior::Delay);
1150 heartbeat.tick().await;
1151 let mut probes = JoinSet::new();
1152 let mut ping_seq = 0_u64;
1153 let mut control_deadline = tokio::time::Instant::now() + heartbeat_tolerance + suspect_grace;
1154
1155 let result = loop {
1156 tokio::select! {
1157 msg = msg_reader.read_msg() => {
1158 let msg = match msg.context(ReadStreamReqSnafu) {
1159 Ok(msg) => msg,
1160 Err(e) => {
1161 tracing::error!(
1162 event = "local_server_control_read_failed",
1163 key = %key,
1164 conn_id = %registration.conn_id,
1165 generation = registration.generation,
1166 error = %snafu::Report::from_error(e),
1167 "local server control read failed"
1168 );
1169 break Err(Status::ReadMsg);
1170 }
1171 };
1172 remote_addr.protocol_succeeded();
1173 diagnostics.heard();
1174 lease_state.lock().await.record_rx();
1175 control_deadline = tokio::time::Instant::now() + heartbeat_tolerance + suspect_grace;
1176 snafu_error_get_or_continue!(
1177 handle_request::<LocalStream>(
1178 msg,
1179 StreamConnect {
1180 local_addr: local_addr.clone(),
1181 remote_addr: registered_addr.clone(),
1182 keep_alive,
1183 namespace,
1184 credential,
1185 idle_timeout,
1186 },
1187 remote_addr.clone(),
1188 key.clone(),
1189 registration.conn_id,
1190 &write_tx,
1191 lease_state.clone(),
1192 stream_tasks,
1193 &shutdown,
1194 setup_slots,
1195 #[cfg(feature = "p2p")]
1196 direct.as_ref(),
1197 )
1198 .await
1199 );
1200 }
1201 Some(_) = stream_tasks.join_next() => {
1202 }
1205 result = &mut writer_handle => {
1206 break match result {
1207 Ok(result) => result,
1208 Err(e) => {
1209 tracing::error!(
1210 event = "local_server_control_writer_join_failed",
1211 key = %key,
1212 conn_id = %registration.conn_id,
1213 generation = registration.generation,
1214 error = %e,
1215 "local server control writer task failed"
1216 );
1217 Err(Status::SendPing)
1218 }
1219 };
1220 }
1221 _ = heartbeat.tick() => {
1222 ping_seq = ping_seq.wrapping_add(1);
1223 if write_tx.try_send(LocalControlWrite::Ping { seq: ping_seq }).is_err() {
1224 break Err(Status::SendPing);
1225 }
1226
1227 let last_rx_age = lease_state.lock().await.last_rx_age();
1228 if registration.protocol_version >= CONTROL_PROTOCOL_V2
1229 && last_rx_age >= heartbeat_tolerance
1230 && probes.is_empty()
1231 {
1232 tracing::warn!(
1233 event = "local_server_lease_suspect",
1234 key = %key,
1235 conn_id = %registration.conn_id,
1236 generation = registration.generation,
1237 worker_index,
1238 last_rx_age_ms = duration_to_millis(last_rx_age),
1239 heartbeat_tolerance_ms = duration_to_millis(heartbeat_tolerance),
1240 "local server control lease is suspect; probing remote registration"
1241 );
1242 let probe_key = key.clone();
1243 let probe_remote = registered_addr.clone();
1244 let probe_endpoint = remote_addr.clone();
1245 probes.spawn(async move {
1246 let _permit = probe_endpoint.control_permit().await;
1247 probe_remote_registration(
1248 probe_remote,
1249 probe_endpoint.clone(),
1250 probe_key,
1251 registration,
1252 namespace,
1253 credential,
1254 )
1255 .await
1256 });
1257 }
1258 }
1259 () = shutdown.cancelled() => {
1260 tracing::info!(
1261 event = "local_server_control_cancelled",
1262 key = %key,
1263 conn_id = %registration.conn_id,
1264 generation = registration.generation,
1265 worker_index,
1266 "local server control loop cancelled"
1267 );
1268 break Err(Status::Cancelled);
1269 }
1270 () = tokio::time::sleep_until(control_deadline) => {
1271 tracing::warn!(event = "local_server_control_unresponsive", %key, worker_index, "control socket received no reply past grace; reconnecting");
1272 break Err(Status::ReadMsg);
1273 }
1274 Some(probe_result) = probes.join_next() => {
1275 let probe_result = probe_result.unwrap_or_else(|error| RegistrationProbeResult::Failed(error.to_string()));
1276 let last_rx_age = lease_state.lock().await.last_rx_age();
1277 match probe_result {
1278 RegistrationProbeResult::Present => {
1279 tracing::debug!(
1280 event = "local_server_registration_probe_ok",
1281 key = %key,
1282 conn_id = %registration.conn_id,
1283 generation = registration.generation,
1284 worker_index,
1285 last_rx_age_ms = duration_to_millis(last_rx_age),
1286 "remote registration still contains this control connection"
1287 );
1288 }
1289 RegistrationProbeResult::Missing if last_rx_age >= heartbeat_tolerance => {
1290 tracing::warn!(
1291 event = "local_server_registration_missing",
1292 key = %key,
1293 conn_id = %registration.conn_id,
1294 generation = registration.generation,
1295 worker_index,
1296 last_rx_age_ms = duration_to_millis(last_rx_age),
1297 "remote registration no longer contains this control connection; reconnecting"
1298 );
1299 break Err(Status::ReadMsg);
1300 }
1301 RegistrationProbeResult::Missing => {
1302 tracing::debug!(
1303 event = "local_server_registration_missing_ignored_after_recent_activity",
1304 key = %key,
1305 conn_id = %registration.conn_id,
1306 generation = registration.generation,
1307 worker_index,
1308 last_rx_age_ms = duration_to_millis(last_rx_age),
1309 "remote registration probe was stale after recent control activity"
1310 );
1311 }
1312 RegistrationProbeResult::Failed(reason)
1313 if last_rx_age >= heartbeat_tolerance + suspect_grace =>
1314 {
1315 tracing::warn!(
1316 event = "local_server_status_probe_failed",
1317 key = %key,
1318 conn_id = %registration.conn_id,
1319 generation = registration.generation,
1320 worker_index,
1321 last_rx_age_ms = duration_to_millis(last_rx_age),
1322 reason = %reason,
1323 "registration probe failed past suspect grace; reconnecting"
1324 );
1325 break Err(Status::ReadMsg);
1326 }
1327 RegistrationProbeResult::Failed(reason) => {
1328 tracing::warn!(
1329 event = "local_server_status_probe_failed",
1330 key = %key,
1331 conn_id = %registration.conn_id,
1332 generation = registration.generation,
1333 worker_index,
1334 last_rx_age_ms = duration_to_millis(last_rx_age),
1335 reason = %reason,
1336 "registration probe failed; waiting inside suspect grace"
1337 );
1338 }
1339 }
1340 }
1341 }
1342 };
1343 if !writer_handle.is_finished() {
1344 writer_handle.abort();
1345 }
1346 result
1347}
1348
1349#[instrument(skip(writer))]
1350async fn handle_ping_interval<T: MessageWriter>(
1351 writer: &mut T,
1352 _key: Arc<str>,
1353 registration: ControlRegistration,
1354 seq: u64,
1355) -> error::Result<()> {
1356 let ping_msg = get_ping_message(registration.protocol_version, seq)?;
1357 let timeout = control_io_timeout();
1358 match tokio::time::timeout(timeout, writer.write_msg(&ping_msg)).await {
1359 Ok(result) => result.context(WritePingMsgSnafu),
1360 Err(_) => ControlIoTimeoutSnafu {
1361 action: "write ping message",
1362 timeout,
1363 }
1364 .fail(),
1365 }
1366}
1367
1368#[instrument(skip_all, fields(%key, conn_id))]
1369#[allow(clippy::too_many_arguments)]
1370async fn handle_request<LocalStream: StreamProvider>(
1371 msg: &[u8],
1372 target: StreamConnect,
1373 relay_endpoint: RelayEndpoint,
1374 key: Arc<str>,
1375 conn_id: u32,
1376 write_tx: &tokio::sync::mpsc::Sender<LocalControlWrite>,
1377 lease_state: Arc<tokio::sync::Mutex<ControlLeaseState>>,
1378 stream_tasks: &mut JoinSet<()>,
1379 shutdown: &CancellationToken,
1380 setup_slots: &Arc<tokio::sync::Semaphore>,
1381 #[cfg(feature = "p2p")] direct: Option<&Arc<crate::direct::Provider>>,
1382) -> error::Result<()>
1383where
1384 LocalStream::Item: StreamForward,
1385{
1386 let req = LocalServer::decode(msg).context(DecodeStreamReqSnafu)?;
1387
1388 match req {
1389 LocalServer::Direct(event) => {
1390 #[cfg(feature = "p2p")]
1391 if let Some(direct) = direct {
1392 let mut event = event;
1393 if let (
1397 pb_mapper_protocol::direct::DirectEvent::Offer { stun_addr, .. },
1398 Some(relay),
1399 ) = (&mut *event, target.remote_addr.as_slice().first())
1400 {
1401 *stun_addr = pb_mapper_protocol::direct::stun_target(*stun_addr, relay.ip());
1402 }
1403 direct.event(*event, write_tx.clone());
1404 }
1405 #[cfg(not(feature = "p2p"))]
1406 let _ = event;
1407 }
1408 LocalServer::Stream {
1409 client_id,
1410 server_generation,
1411 } => {
1412 tracing::debug!(
1413 event = "local_server_stream_request_received",
1414 key = %key,
1415 server_conn_id = %conn_id,
1416 client_conn_id = client_id,
1417 server_generation,
1418 "local server received stream request"
1419 );
1420 let Ok(permit) = setup_slots.clone().try_acquire_owned() else {
1421 tracing::warn!(event = "local_server_setup_saturated", %key, client_id, "stream setup capacity reached; relay can select another worker");
1422 return Ok(());
1423 };
1424 write_tx
1425 .try_send(LocalControlWrite::StreamAck {
1426 client_id,
1427 server_generation,
1428 })
1429 .map_err(|_| error::Error::ControlWriterClosed {
1430 action: "stream ack message",
1431 })?;
1432 let key = key.clone();
1433 let stream_shutdown = shutdown.clone();
1434 stream_tasks.spawn(async move {
1438 let forward = handle_stream_on_endpoint::<LocalStream>(
1439 key,
1440 client_id,
1441 server_generation,
1442 target,
1443 permit,
1444 relay_endpoint,
1445 );
1446 tokio::select! {
1447 () = stream_shutdown.cancelled() => {}
1448 result = forward => snafu_error_handle!(result),
1449 }
1450 });
1451 }
1452 LocalServer::Pong => {
1454 lease_state.lock().await.record_pong();
1455 tracing::debug!(
1456 event = "local_server_pong_received",
1457 key = %key,
1458 server_conn_id = %conn_id,
1459 "local server received pong"
1460 );
1461 }
1462 LocalServer::PongV2 { seq } => {
1463 lease_state.lock().await.record_pong();
1464 tracing::debug!(
1465 event = "local_server_pong_received",
1466 key = %key,
1467 server_conn_id = %conn_id,
1468 seq,
1469 "local server received pong v2"
1470 );
1471 }
1472 LocalServer::Retire {
1473 reason,
1474 conn_id: retired_conn_id,
1475 server_generation,
1476 } => {
1477 tracing::warn!(
1478 event = "local_server_control_retired",
1479 key = %key,
1480 server_conn_id = %conn_id,
1481 retired_conn_id,
1482 server_generation,
1483 reason = %reason,
1484 "remote server retired this local control connection"
1485 );
1486 }
1487 }
1488 Ok(())
1489}
1490
1491async fn write_stream_ack<T: MessageWriter>(
1492 writer: &mut T,
1493 client_id: u32,
1494 server_generation: u64,
1495) -> error::Result<()> {
1496 let ack = PbServerRequest::StreamAck {
1497 client_id,
1498 server_generation,
1499 }
1500 .encode()
1501 .context(EncodeStreamAckMsgSnafu)?;
1502 let timeout = control_io_timeout();
1503 match tokio::time::timeout(timeout, writer.write_msg(&ack)).await {
1504 Ok(result) => result.context(WriteStreamAckMsgSnafu),
1505 Err(_) => ControlIoTimeoutSnafu {
1506 action: "write stream ack message",
1507 timeout,
1508 }
1509 .fail(),
1510 }
1511}
1512
1513#[cfg(test)]
1514mod pool_status_tests {
1515 use std::sync::Mutex as StdMutex;
1516
1517 use super::*;
1518
1519 fn recording_pool(pool_size: usize) -> (PoolStatus, Arc<StdMutex<Vec<String>>>) {
1521 let seen = Arc::new(StdMutex::new(Vec::new()));
1522 let sink = seen.clone();
1523 let callback: StatusCallback = Box::new(move |status: &str| {
1524 sink.lock().unwrap().push(status.to_string());
1525 });
1526 (PoolStatus::new(callback, pool_size), seen)
1527 }
1528
1529 fn published(seen: &Arc<StdMutex<Vec<String>>>) -> Vec<String> {
1530 seen.lock().unwrap().clone()
1531 }
1532
1533 #[test]
1536 fn one_connected_worker_makes_the_pool_connected() {
1537 let (pool, seen) = recording_pool(2);
1538 pool.report(0, WorkerStatus::Connected);
1539 pool.report(1, WorkerStatus::Retrying);
1540 assert_eq!(published(&seen), vec!["connected".to_string()]);
1541 }
1542
1543 #[test]
1546 fn a_single_failure_does_not_fail_the_pool() {
1547 let (pool, seen) = recording_pool(2);
1548 pool.report(0, WorkerStatus::Failed("service_transport_mismatch".into()));
1549 assert_eq!(published(&seen), vec!["retrying".to_string()]);
1550
1551 pool.report(1, WorkerStatus::Connected);
1552 assert_eq!(
1553 published(&seen),
1554 vec!["retrying".to_string(), "connected".to_string()]
1555 );
1556 }
1557
1558 #[test]
1561 fn the_pool_fails_only_when_every_worker_has() {
1562 let (pool, seen) = recording_pool(2);
1563 pool.report(0, WorkerStatus::Failed("service_transport_mismatch".into()));
1564 pool.report(1, WorkerStatus::Failed("namespace_access_denied".into()));
1565 assert_eq!(
1566 published(&seen),
1567 vec![
1568 "retrying".to_string(),
1569 "failed: service_transport_mismatch".to_string(),
1570 ],
1571 "the first permanent rejection is the one that explains the pool"
1572 );
1573 }
1574
1575 #[test]
1578 fn unchanged_aggregates_are_not_republished() {
1579 let (pool, seen) = recording_pool(2);
1580 pool.report(0, WorkerStatus::Connected);
1581 pool.report(1, WorkerStatus::Connected);
1582 pool.report(0, WorkerStatus::Connected);
1583 assert_eq!(published(&seen), vec!["connected".to_string()]);
1584 }
1585
1586 #[test]
1589 fn losing_the_last_connection_publishes_retrying() {
1590 let (pool, seen) = recording_pool(1);
1591 pool.report(0, WorkerStatus::Connected);
1592 pool.report(0, WorkerStatus::Retrying);
1593 assert_eq!(
1594 published(&seen),
1595 vec!["connected".to_string(), "retrying".to_string()]
1596 );
1597 }
1598}
1599
1600#[cfg(test)]
1601mod shutdown_tests {
1602 use std::time::Duration;
1603
1604 use super::*;
1605 use uni_stream::stream::TcpStreamProvider;
1606
1607 #[tokio::test]
1608 async fn register_loop_stops_on_cancel() {
1609 let shutdown = CancellationToken::new();
1610 let token = shutdown.clone();
1611 let addr = ResolvedAddrs::from("127.0.0.1:1".parse::<std::net::SocketAddr>().unwrap());
1612 let task = tokio::spawn(async move {
1613 run_server_side_cli_with_shutdown::<TcpStreamProvider>(
1614 addr.clone(),
1615 addr,
1616 "k".into(),
1617 ServerTunnelOptions {
1618 need_codec: false,
1619 is_datagram: false,
1620 keep_alive: false,
1621 namespace: None,
1622 force_namespace: false,
1623 },
1624 None,
1625 Credential::Admin(*b"0123456789abcdefghijklmnopqrstuv"),
1626 token,
1627 )
1628 .await;
1629 });
1630 tokio::time::sleep(Duration::from_millis(80)).await;
1631 shutdown.cancel();
1632 tokio::time::timeout(Duration::from_secs(3), task)
1633 .await
1634 .expect("register loop did not stop after cancel")
1635 .expect("join");
1636 }
1637}