1pub mod error;
2pub mod status;
3pub(crate) mod stream;
4
5use crate::diagnostics::{Diagnostics, RecoveryFailure, RecoveryPhase};
6use crate::endpoint::RelayEndpoint;
7use crate::relay::CarrierId;
8use std::fmt::Debug;
9use std::sync::Arc;
10use std::time::Duration;
11
12use snafu::ResultExt;
13use tokio::sync::{Mutex, Semaphore};
14use tokio::task::JoinSet;
15use tokio::time::Instant;
16use tokio_util::sync::CancellationToken;
17use uni_stream::udp::set_custom_timeout;
18
19use self::error::{AcceptLocalStreamSnafu, BindLocalListenerSnafu};
20use self::status::{get_status_scoped, get_status_with_credential};
21use self::stream::{StreamSetup, handle_local_stream};
22use crate::addr::{resolve_all, resolve_tunnel_ends};
23use crate::recovery::{RecoveryTiming, jitter};
24use pb_mapper_core::checksum::{Credential, get_process_credential};
25use pb_mapper_core::config::ResolvedAddrs;
26use pb_mapper_core::config::{
27 StatusOp, client_health_check_interval, client_health_check_timeout,
28 client_health_failure_threshold,
29};
30use pb_mapper_core::timeout::RetryBackoff;
31use pb_mapper_protocol::command::{PbConnStatusReq, PbConnStatusResp};
32use pb_mapper_protocol::forward::StreamForward;
33use uni_stream::addr::ToSocketAddrs;
34use uni_stream::stream::{ListenerProvider, StreamAccept};
35
36pub type ClientStatusCallback = Box<dyn Fn(&str) + Send + Sync>;
38
39async fn resolve_ends_or_fail<A: ToSocketAddrs>(
45 local_addr: A,
46 remote_addr: A,
47 status_callback: Option<&ClientStatusCallback>,
48) -> Option<(ResolvedAddrs, ResolvedAddrs)> {
49 let resolved = resolve_tunnel_ends(local_addr, remote_addr).await;
50 if let (None, Some(callback)) = (&resolved, status_callback) {
51 callback("failed");
52 }
53 resolved
54}
55
56pub async fn run_client_side_cli<LocalListener: ListenerProvider, A: ToSocketAddrs>(
57 local_addr: A,
58 remote_addr: A,
59 key: Arc<str>,
60 keep_alive: bool,
61) where
62 <LocalListener::Listener as StreamAccept>::Item: StreamForward,
63{
64 run_client_side_cli_with_callback::<LocalListener, A>(
65 local_addr,
66 remote_addr,
67 key,
68 keep_alive,
69 None,
70 )
71 .await
72}
73
74pub async fn run_client_side_cli_with_callback<LocalListener: ListenerProvider, A: ToSocketAddrs>(
75 local_addr: A,
76 remote_addr: A,
77 key: Arc<str>,
78 keep_alive: bool,
79 status_callback: Option<ClientStatusCallback>,
80) where
81 <LocalListener::Listener as StreamAccept>::Item: StreamForward,
82{
83 run_client_side_cli_with_callback_scoped::<LocalListener, A>(
84 local_addr,
85 remote_addr,
86 key,
87 keep_alive,
88 None,
89 status_callback,
90 None,
91 )
92 .await
93}
94
95pub async fn run_client_side_cli_with_pinned_credential<
96 LocalListener: ListenerProvider,
97 A: ToSocketAddrs,
98>(
99 local_addr: A,
100 remote_addr: A,
101 key: Arc<str>,
102 keep_alive: bool,
103 status_callback: Option<ClientStatusCallback>,
104 credential: pb_mapper_core::checksum::Credential,
105) where
106 <LocalListener::Listener as StreamAccept>::Item: StreamForward,
107{
108 run_client_side_cli_with_callback_scoped::<LocalListener, A>(
109 local_addr,
110 remote_addr,
111 key,
112 keep_alive,
113 None,
114 status_callback,
115 Some(credential),
116 )
117 .await
118}
119
120pub async fn run_client_side_cli_with_callback_scoped<
121 LocalListener: ListenerProvider,
122 A: ToSocketAddrs,
123>(
124 local_addr: A,
125 remote_addr: A,
126 key: Arc<str>,
127 keep_alive: bool,
128 namespace: Option<u64>,
129 status_callback: Option<ClientStatusCallback>,
130 pinned_credential: Option<pb_mapper_core::checksum::Credential>,
131) where
132 <LocalListener::Listener as StreamAccept>::Item: StreamForward,
133{
134 let Some((local_addr, remote_addr)) =
135 resolve_ends_or_fail(local_addr, remote_addr, status_callback.as_ref()).await
136 else {
137 return;
138 };
139 run_client_side_cli_loop::<LocalListener>(
140 local_addr,
141 remote_addr,
142 key,
143 keep_alive,
144 namespace,
145 status_callback,
146 pinned_credential,
147 CancellationToken::new(),
148 )
149 .await
150}
151
152#[allow(clippy::too_many_arguments)]
158pub async fn run_client_side_cli_with_shutdown<LocalListener: ListenerProvider>(
159 local_addr: ResolvedAddrs,
160 remote_addr: ResolvedAddrs,
161 key: Arc<str>,
162 keep_alive: bool,
163 namespace: Option<u64>,
164 status_callback: Option<ClientStatusCallback>,
165 pinned_credential: Option<pb_mapper_core::checksum::Credential>,
166 shutdown: CancellationToken,
167) where
168 <LocalListener::Listener as StreamAccept>::Item: StreamForward,
169{
170 run_client_side_cli_loop::<LocalListener>(
171 local_addr,
172 remote_addr,
173 key,
174 keep_alive,
175 namespace,
176 status_callback,
177 pinned_credential,
178 shutdown,
179 )
180 .await
181}
182
183#[allow(clippy::too_many_arguments)]
184async fn run_client_side_cli_loop<LocalListener: ListenerProvider>(
185 local_addr: ResolvedAddrs,
186 remote_addr: ResolvedAddrs,
187 key: Arc<str>,
188 keep_alive: bool,
189 namespace: Option<u64>,
190 status_callback: Option<ClientStatusCallback>,
191 pinned_credential: Option<pb_mapper_core::checksum::Credential>,
192 shutdown: CancellationToken,
193) where
194 <LocalListener::Listener as StreamAccept>::Item: StreamForward,
195{
196 run_client_side_cli_recovering::<LocalListener>(
197 local_addr,
198 RelayEndpoint::fixed(remote_addr),
199 key,
200 keep_alive,
201 namespace,
202 status_callback,
203 pinned_credential,
204 shutdown,
205 Diagnostics::default(),
206 )
207 .await;
208}
209
210#[allow(clippy::too_many_arguments)]
211pub(crate) async fn run_client_side_cli_recovering<LocalListener: ListenerProvider>(
212 local_addr: ResolvedAddrs,
213 remote_addr: RelayEndpoint,
214 key: Arc<str>,
215 keep_alive: bool,
216 namespace: Option<u64>,
217 status_callback: Option<ClientStatusCallback>,
218 pinned_credential: Option<pb_mapper_core::checksum::Credential>,
219 shutdown: CancellationToken,
220 diagnostics: Diagnostics,
221) where
222 <LocalListener::Listener as StreamAccept>::Item: StreamForward,
223{
224 run_client_side_cli_recovering_with_path::<LocalListener>(
225 local_addr,
226 remote_addr,
227 key,
228 keep_alive,
229 namespace,
230 status_callback,
231 pinned_credential,
232 shutdown,
233 diagnostics,
234 crate::sdk::PathPolicy::RelayOnly,
235 false,
236 )
237 .await;
238}
239
240#[allow(clippy::too_many_arguments)]
241pub(crate) async fn run_client_side_cli_recovering_with_path<LocalListener: ListenerProvider>(
242 local_addr: ResolvedAddrs,
243 remote_addr: RelayEndpoint,
244 key: Arc<str>,
245 keep_alive: bool,
246 namespace: Option<u64>,
247 status_callback: Option<ClientStatusCallback>,
248 pinned_credential: Option<Credential>,
249 shutdown: CancellationToken,
250 diagnostics: Diagnostics,
251 path: crate::sdk::PathPolicy,
252 is_datagram: bool,
253) where
254 <LocalListener::Listener as StreamAccept>::Item: StreamForward,
255{
256 remote_addr.start();
257 let mut wake = remote_addr.wake();
258 diagnostics.network_seen(remote_addr.network_generation());
259 set_custom_timeout(Duration::from_secs(120));
260
261 let credential = match pinned_credential {
262 Some(credential) => credential,
263 None => match get_process_credential() {
264 Ok(credential) => credential,
265 Err(e) => {
266 tracing::error!("load client credential failed: {e}");
267 if let Some(ref callback) = status_callback {
268 callback("failed");
269 }
270 return;
271 }
272 },
273 };
274
275 #[cfg(feature = "p2p")]
276 let direct = (path == crate::sdk::PathPolicy::Auto).then(|| {
277 crate::direct::Consumer::new(
278 remote_addr.clone(),
279 credential,
280 namespace,
281 is_datagram,
282 keep_alive,
283 diagnostics.clone(),
284 )
285 });
286 #[cfg(not(feature = "p2p"))]
287 let _ = (path, is_datagram);
288 let mut retry_backoff = RetryBackoff::new(Duration::from_millis(100), Duration::from_secs(2));
289 let mut stream_tasks = JoinSet::new();
290 let setup_slots = Arc::new(Semaphore::new(64));
291 let timing = Arc::new(Mutex::new(RecoveryTiming::default()));
292
293 'outer: loop {
294 let listener = tokio::select! {
297 () = shutdown.cancelled() => break,
298 result = LocalListener::bind(local_addr.as_slice()) => match result.context(BindLocalListenerSnafu) {
299 Ok(listener) => listener,
300 Err(error) => {
301 tracing::warn!(event = "client_local_bind_failed", %key, %local_addr, %error);
302 if let Some(callback) = &status_callback { callback("retrying"); }
303 tokio::select! {
304 () = shutdown.cancelled() => break,
305 () = tokio::time::sleep(jitter(retry_backoff.next_delay())) => {}
306 }
307 continue;
308 }
309 }
310 };
311 tracing::info!(event = "client_local_listener_bound", %key, %local_addr, "local listener bound; checking remote service");
312 let mut probes = JoinSet::new();
313 let mut next_probe = Instant::now();
314 let mut last_success = None;
315 let mut connected = false;
316 let mut consecutive_health_failures = 0usize;
317 let (stream_event_tx, mut stream_event_rx) = tokio::sync::mpsc::channel(1);
318
319 loop {
320 tokio::select! {
321 () = shutdown.cancelled() => break 'outer,
322 () = wake.changed() => {
323 diagnostics.network_changed(remote_addr.network_generation());
324 #[cfg(feature = "p2p")]
325 if let Some(direct) = &direct { direct.invalidate(); }
326 probes.abort_all();
327 next_probe = Instant::now();
328 retry_backoff.reset();
329 },
330 accepted = async {
331 let permit = setup_slots.clone().acquire_owned().await;
334 (permit, listener.accept().await)
335 } => {
336 let (Ok(permit), accepted) = accepted else { break 'outer; };
337 let (stream, _) = match accepted.context(AcceptLocalStreamSnafu) {
338 Ok(accepted) => accepted,
339 Err(error) => {
340 tracing::warn!(event = "client_local_accept_failed", %key, %local_addr, %error);
341 break;
342 }
343 };
344 let stream_key = key.clone();
345 let stream_remote = remote_addr.clone();
346 let stream_shutdown = shutdown.clone();
347 let event_tx = stream_event_tx.clone();
348 let setup = StreamSetup { permit, timing: timing.clone(), events: event_tx.clone(), diagnostics: diagnostics.clone(), #[cfg(feature = "p2p")] direct: direct.clone() };
349 stream_tasks.spawn(async move {
350 let result = tokio::select! {
351 () = stream_shutdown.cancelled() => return,
352 result = handle_local_stream(stream, stream_key, stream_remote, keep_alive, namespace, credential, setup) => result,
353 };
354 if let Err(error) = result {
355 tracing::warn!(event = "client_local_stream_failed_before_forward", reason = %snafu::Report::from_error(error));
356 let _ = event_tx.try_send(false);
357 }
358 });
359 }
360 () = tokio::time::sleep_until(next_probe), if probes.is_empty() => {
361 diagnostics.attempt();
362 diagnostics.phase(RecoveryPhase::Handshake);
363 let probe_remote = remote_addr.clone();
364 let probe_key = key.clone();
365 let budget = timing.lock().await.timeout().min(client_health_check_timeout());
366 probes.spawn(async move {
367 let _permit = probe_remote.control_permit().await;
368 let started = Instant::now();
369 let mut carrier = None;
370 let result = probe_remote_key(&probe_remote, &probe_key, namespace, credential, budget, &mut carrier).await;
371 (started, started.elapsed(), result, carrier)
372 });
373 }
374 Some(result) = probes.join_next() => {
375 let (started, elapsed, result, carrier) = match result {
376 Ok(result) => result,
377 Err(error) if error.is_cancelled() => continue,
378 Err(error) => (Instant::now(), Duration::ZERO, Err(ProbeFailure::transient(error.to_string())), None),
379 };
380 match result {
381 Ok(()) => {
382 timing.lock().await.record(elapsed);
383 remote_addr.protocol_succeeded();
384 diagnostics.succeeded(elapsed);
385 last_success = Some(Instant::now());
386 consecutive_health_failures = 0;
387 retry_backoff.reset();
388 next_probe = Instant::now() + client_health_check_interval();
389 if !connected {
390 connected = true;
391 if let Some(callback) = &status_callback { callback("connected"); }
392 }
393 }
394 Err(failure) if failure.permanent && diagnostics.has_active_direct() => {
395 diagnostics.failed(failure.kind, Duration::ZERO);
396 if let Some(callback) = &status_callback { callback("retrying"); }
397 next_probe = Instant::now() + Duration::from_secs(30);
398 }
399 Err(failure) if failure.permanent => {
400 if failure.protocol_replied {
401 remote_addr.protocol_succeeded();
402 diagnostics.responded(elapsed);
403 }
404 diagnostics.failed(failure.kind, Duration::ZERO);
405 if let Some(callback) = &status_callback { callback(&format!("failed: {failure}")); }
406 break 'outer;
407 }
408 Err(_) if last_success.is_some_and(|success| success > started) => {
409 next_probe = Instant::now() + client_health_check_interval();
411 }
412 Err(failure) => {
413 failure.update_timing(&mut *timing.lock().await, elapsed);
414 if failure.protocol_replied {
415 remote_addr.protocol_succeeded();
416 diagnostics.responded(elapsed);
417 } else { remote_addr.transport_failed(carrier); }
418 consecutive_health_failures = consecutive_health_failures.saturating_add(1);
419 if !connected || consecutive_health_failures >= client_health_failure_threshold() {
420 connected = false;
421 if let Some(callback) = &status_callback { callback("retrying"); }
422 }
423 let delay = jitter(retry_backoff.next_delay());
424 next_probe = Instant::now() + delay;
425 if diagnostics.failed(failure.kind, delay) {
426 tracing::warn!(event = "client_remote_probe_failed", %key, reason = %failure, consecutive_health_failures, retry_delay = ?delay, "remote probe failed; listener remains active");
427 }
428 }
429 }
430 }
431 Some(success) = stream_event_rx.recv() => {
432 if success {
433 last_success = Some(Instant::now());
434 consecutive_health_failures = 0;
435 retry_backoff.reset();
436 if !connected {
437 connected = true;
438 if let Some(callback) = &status_callback { callback("connected"); }
439 }
440 } else if probes.is_empty() {
441 next_probe = next_probe.min(Instant::now() + Duration::from_millis(100));
444 }
445 }
446 Some(_) = stream_tasks.join_next() => {}
447 }
448 }
449 drop(probes);
450 if let Some(callback) = &status_callback {
451 callback("retrying");
452 }
453 tokio::select! {
454 () = shutdown.cancelled() => break,
455 () = tokio::time::sleep(jitter(retry_backoff.next_delay())) => {}
456 }
457 }
458
459 stream_tasks.shutdown().await;
462 #[cfg(feature = "p2p")]
463 if let Some(direct) = direct {
464 direct.stop().await;
465 }
466}
467
468#[derive(Debug)]
475struct ProbeFailure {
476 reason: String,
477 permanent: bool,
478 kind: RecoveryFailure,
479 protocol_replied: bool,
480}
481
482impl ProbeFailure {
483 fn update_timing(&self, timing: &mut RecoveryTiming, elapsed: Duration) {
484 if self.kind == RecoveryFailure::Timeout {
485 timing.timed_out();
486 } else if self.protocol_replied {
487 timing.record(elapsed);
488 }
489 }
490
491 fn transient(reason: impl Into<String>) -> Self {
494 Self {
495 reason: reason.into(),
496 permanent: false,
497 kind: RecoveryFailure::Transport,
498 protocol_replied: false,
499 }
500 }
501
502 fn from_status_error(context: &str, error: crate::client::error::Error) -> Self {
505 let permanent = error.remote_retryable() == Some(false);
506 let protocol_replied = error.remote_retryable().is_some();
507 Self {
508 reason: format!("{context}: {}", snafu::Report::from_error(error)),
509 permanent,
510 kind: if protocol_replied {
511 RecoveryFailure::Rejected
512 } else {
513 RecoveryFailure::Transport
514 },
515 protocol_replied,
516 }
517 }
518}
519
520impl std::fmt::Display for ProbeFailure {
521 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
522 formatter.write_str(&self.reason)
523 }
524}
525
526async fn probe_remote_key(
527 remote_addr: &RelayEndpoint,
528 key: &str,
529 namespace: Option<u64>,
530 credential: Credential,
531 timeout: Duration,
532 carrier: &mut Option<CarrierId>,
533) -> std::result::Result<(), ProbeFailure> {
534 match tokio::time::timeout(timeout, async {
535 let addresses = remote_addr
538 .addresses()
539 .await
540 .map_err(|error| ProbeFailure {
541 reason: error.to_string(),
542 permanent: false,
543 kind: RecoveryFailure::Dns,
544 protocol_replied: false,
545 })?;
546 probe_remote_key_once(remote_addr, &addresses, key, namespace, credential, carrier).await
547 })
548 .await
549 {
550 Ok(result) => result,
551 Err(_) => Err(ProbeFailure {
552 reason: format!("remote key probe timed out after {timeout:?}"),
553 permanent: false,
554 kind: RecoveryFailure::Timeout,
555 protocol_replied: false,
556 }),
557 }
558}
559
560async fn probe_remote_key_once(
561 remote_addr: &RelayEndpoint,
562 addresses: &ResolvedAddrs,
563 key: &str,
564 namespace: Option<u64>,
565 credential: Credential,
566 carrier: &mut Option<CarrierId>,
567) -> std::result::Result<(), ProbeFailure> {
568 match fetch_remote_status(
569 remote_addr,
570 addresses,
571 PbConnStatusReq::Service {
572 direct_query: None,
573 key: key.to_string(),
574 },
575 namespace,
576 credential,
577 carrier,
578 )
579 .await
580 {
581 Ok(PbConnStatusResp::Service { connections, .. }) => {
582 if connections.iter().any(|conn| conn.healthy) {
583 return Ok(());
584 }
585 return Err(ProbeFailure {
586 reason: format!("client key `{key}` has no healthy remote server connections"),
587 permanent: false,
588 kind: RecoveryFailure::ServiceUnavailable,
589 protocol_replied: true,
590 });
591 }
592 Ok(status_resp) => {
593 return Err(ProbeFailure::transient(format!(
594 "expected service status response, got {status_resp:?}"
595 )));
596 }
597 Err(failure) if failure.permanent => return Err(failure),
600 Err(service_failure) => {
601 tracing::debug!(
602 event = "client_remote_service_probe_failed",
603 key = %key,
604 remote_addr = %remote_addr,
605 reason = %service_failure,
606 "service status probe failed; falling back to key status"
607 );
608 }
609 }
610
611 let status_resp = fetch_remote_status(
612 remote_addr,
613 addresses,
614 PbConnStatusReq::Keys,
615 namespace,
616 credential,
617 carrier,
618 )
619 .await?;
620 let PbConnStatusResp::Keys(keys) = status_resp else {
621 return Err(ProbeFailure::transient(format!(
622 "expected keys status response, got {status_resp:?}"
623 )));
624 };
625 if keys.iter().any(|candidate| candidate == key) {
626 Ok(())
627 } else {
628 Err(ProbeFailure {
629 reason: format!("client key `{key}` is not registered on remote server"),
630 permanent: false,
631 kind: RecoveryFailure::ServiceUnavailable,
632 protocol_replied: true,
633 })
634 }
635}
636
637async fn fetch_remote_status(
638 remote_addr: &RelayEndpoint,
639 addresses: &ResolvedAddrs,
640 req: PbConnStatusReq,
641 namespace: Option<u64>,
642 credential: Credential,
643 carrier: &mut Option<CarrierId>,
644) -> std::result::Result<PbConnStatusResp, ProbeFailure> {
645 let mut stream = remote_addr
646 .connect_addresses(addresses, credential, carrier)
647 .await
648 .map_err(|error| match error {
649 crate::relay::ConnectError::Rejected { response } => ProbeFailure {
650 reason: format!("{}: {}", response.code, response.message),
651 permanent: !response.retryable,
652 kind: RecoveryFailure::Rejected,
653 protocol_replied: true,
654 },
655 error => ProbeFailure::transient(format!("connect remote stream failed: {error}")),
656 })?;
657 get_status_with_credential(&mut stream, req, namespace, &credential)
658 .await
659 .map_err(|error| ProbeFailure::from_status_error("get status failed", error))
660}
661
662pub async fn handle_status_cli_scoped<A: ToSocketAddrs>(
663 op: StatusOp,
664 addr: A,
665 namespace: Option<u64>,
666) -> Result<(), Box<dyn std::error::Error>> {
667 match op {
668 StatusOp::Sessions => show_status_scoped(addr, PbConnStatusReq::Sessions, namespace).await,
669 StatusOp::RemoteId => show_status_scoped(addr, PbConnStatusReq::RemoteId, namespace).await,
670 StatusOp::Keys => show_status_scoped(addr, PbConnStatusReq::Keys, namespace).await,
671 }
672}
673
674pub async fn show_status_scoped<A: ToSocketAddrs>(
675 remote_addr: A,
676 req: PbConnStatusReq,
677 namespace: Option<u64>,
678) -> Result<(), Box<dyn std::error::Error>> {
679 let remote_addr = resolve_all(remote_addr).await?;
680 let mut stream = crate::addr::connect_tcp(&remote_addr)
681 .await
682 .map_err(|error| format!("get status stream: {error}"))?;
683 let status = get_status_scoped(&mut stream, req, namespace).await?;
684 let status = serde_json::to_string_pretty(&status)?;
685 println!("Status:{status}");
686 Ok(())
687}
688
689#[cfg(test)]
690#[allow(clippy::unwrap_used, clippy::expect_used)]
691mod cancellation_audit {
692 use std::{
693 future::Future,
694 sync::{
695 Arc,
696 atomic::{AtomicBool, Ordering},
697 },
698 task::{Context, Wake, Waker},
699 time::Duration,
700 };
701 struct Notified(AtomicBool);
702 impl Wake for Notified {
703 fn wake(self: Arc<Self>) {
704 self.0.store(true, Ordering::SeqCst);
705 }
706 fn wake_by_ref(self: &Arc<Self>) {
707 self.0.store(true, Ordering::SeqCst);
708 }
709 }
710 #[tokio::test]
711 async fn cancelled_udp_accept_must_preserve_the_delivered_peer() {
712 let listener = uni_stream::udp::UdpListener::bind("127.0.0.1:0")
713 .await
714 .unwrap();
715 let addr = listener.local_addr().unwrap();
716 let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
717 let notified = Arc::new(Notified(AtomicBool::new(false)));
718 let waker = Waker::from(notified.clone());
719 let mut accept = Box::pin(listener.accept());
720 assert!(
721 accept
722 .as_mut()
723 .poll(&mut Context::from_waker(&waker))
724 .is_pending()
725 );
726 client.send_to(b"first datagram", addr).await.unwrap();
727 tokio::time::timeout(Duration::from_secs(1), async {
728 while !notified.0.load(Ordering::SeqCst) {
729 tokio::task::yield_now().await;
730 }
731 })
732 .await
733 .unwrap();
734 drop(accept);
735 let delivered = tokio::time::timeout(Duration::from_millis(100), listener.accept()).await;
736 assert!(
737 delivered.is_ok(),
738 "UDP peer/first datagram disappeared when a competing select branch cancelled accept"
739 );
740 }
741}
742
743#[cfg(test)]
744mod recovery_tests {
745 use super::*;
746 #[test]
747 fn absent_service_learns_response_latency_without_inflating_timeout() {
748 let mut timing = RecoveryTiming::default();
749 let missing = ProbeFailure {
750 reason: String::new(),
751 permanent: false,
752 kind: RecoveryFailure::ServiceUnavailable,
753 protocol_replied: true,
754 };
755 for _ in 0..20 {
756 missing.update_timing(&mut timing, Duration::from_millis(40));
757 }
758 assert_eq!(timing.timeout(), Duration::from_secs(1));
759 let refusal = ProbeFailure::transient("connection refused");
760 refusal.update_timing(&mut timing, Duration::from_millis(1));
761 assert_eq!(timing.timeout(), Duration::from_secs(1));
762 let timeout = ProbeFailure {
763 kind: RecoveryFailure::Timeout,
764 ..refusal
765 };
766 timeout.update_timing(&mut timing, Duration::from_secs(1));
767 assert_eq!(timing.timeout(), Duration::from_secs(2));
768 }
769}