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 crate::sdk::PortMapping::Off,
237 )
238 .await;
239}
240
241#[allow(clippy::too_many_arguments)]
242pub(crate) async fn run_client_side_cli_recovering_with_path<LocalListener: ListenerProvider>(
243 local_addr: ResolvedAddrs,
244 remote_addr: RelayEndpoint,
245 key: Arc<str>,
246 keep_alive: bool,
247 namespace: Option<u64>,
248 status_callback: Option<ClientStatusCallback>,
249 pinned_credential: Option<Credential>,
250 shutdown: CancellationToken,
251 diagnostics: Diagnostics,
252 path: crate::sdk::PathPolicy,
253 is_datagram: bool,
254 port_mapping: crate::sdk::PortMapping,
255) where
256 <LocalListener::Listener as StreamAccept>::Item: StreamForward,
257{
258 remote_addr.start();
259 let mut wake = remote_addr.wake();
260 diagnostics.network_seen(remote_addr.network_generation());
261 set_custom_timeout(Duration::from_secs(120));
262
263 let credential = match pinned_credential {
264 Some(credential) => credential,
265 None => match get_process_credential() {
266 Ok(credential) => credential,
267 Err(e) => {
268 tracing::error!("load client credential failed: {e}");
269 if let Some(ref callback) = status_callback {
270 callback("failed");
271 }
272 return;
273 }
274 },
275 };
276
277 #[cfg(feature = "p2p")]
278 let direct = (path == crate::sdk::PathPolicy::Auto).then(|| {
279 crate::direct::Consumer::new(
280 remote_addr.clone(),
281 credential,
282 namespace,
283 is_datagram,
284 keep_alive,
285 diagnostics.clone(),
286 port_mapping,
287 )
288 });
289 #[cfg(not(feature = "p2p"))]
290 let _ = (path, is_datagram, port_mapping);
291 let mut retry_backoff = RetryBackoff::new(Duration::from_millis(100), Duration::from_secs(2));
292 let mut stream_tasks = JoinSet::new();
293 let setup_slots = Arc::new(Semaphore::new(64));
294 let timing = Arc::new(Mutex::new(RecoveryTiming::default()));
295
296 'outer: loop {
297 let listener = tokio::select! {
300 () = shutdown.cancelled() => break,
301 result = LocalListener::bind(local_addr.as_slice()) => match result.context(BindLocalListenerSnafu) {
302 Ok(listener) => listener,
303 Err(error) => {
304 tracing::warn!(event = "client_local_bind_failed", %key, %local_addr, %error);
305 if let Some(callback) = &status_callback { callback("retrying"); }
306 tokio::select! {
307 () = shutdown.cancelled() => break,
308 () = tokio::time::sleep(jitter(retry_backoff.next_delay())) => {}
309 }
310 continue;
311 }
312 }
313 };
314 tracing::info!(event = "client_local_listener_bound", %key, %local_addr, "local listener bound; checking remote service");
315 let mut probes = JoinSet::new();
316 let mut next_probe = Instant::now();
317 let mut last_success = None;
318 let mut connected = false;
319 let mut consecutive_health_failures = 0usize;
320 let (stream_event_tx, mut stream_event_rx) = tokio::sync::mpsc::channel(1);
321
322 loop {
323 tokio::select! {
324 () = shutdown.cancelled() => break 'outer,
325 woke = wake.changed() => {
326 if let crate::endpoint::Wake::Network(generation) = woke {
327 diagnostics.network_changed(generation);
328 #[cfg(feature = "p2p")]
329 if let Some(direct) = &direct { direct.invalidate(); }
330 }
331 probes.abort_all();
332 next_probe = Instant::now();
333 retry_backoff.reset();
334 },
335 accepted = async {
336 let permit = setup_slots.clone().acquire_owned().await;
339 (permit, listener.accept().await)
340 } => {
341 let (Ok(permit), accepted) = accepted else { break 'outer; };
342 let (stream, _) = match accepted.context(AcceptLocalStreamSnafu) {
343 Ok(accepted) => accepted,
344 Err(error) => {
345 tracing::warn!(event = "client_local_accept_failed", %key, %local_addr, %error);
346 break;
347 }
348 };
349 let stream_key = key.clone();
350 let stream_remote = remote_addr.clone();
351 let stream_shutdown = shutdown.clone();
352 let event_tx = stream_event_tx.clone();
353 let setup = StreamSetup { permit, timing: timing.clone(), events: event_tx.clone(), diagnostics: diagnostics.clone(), #[cfg(feature = "p2p")] direct: direct.clone() };
354 stream_tasks.spawn(async move {
355 let result = tokio::select! {
356 () = stream_shutdown.cancelled() => return,
357 result = handle_local_stream(stream, stream_key, stream_remote, keep_alive, namespace, credential, setup) => result,
358 };
359 if let Err(error) = result {
360 tracing::warn!(event = "client_local_stream_failed_before_forward", reason = %snafu::Report::from_error(error));
361 let _ = event_tx.try_send(false);
362 }
363 });
364 }
365 () = tokio::time::sleep_until(next_probe), if probes.is_empty() => {
366 diagnostics.attempt();
367 diagnostics.phase(RecoveryPhase::Handshake);
368 let probe_remote = remote_addr.clone();
369 let probe_key = key.clone();
370 let budget = timing.lock().await.timeout().min(client_health_check_timeout());
371 probes.spawn(async move {
372 let _permit = probe_remote.control_permit().await;
373 let started = Instant::now();
374 let mut carrier = None;
375 let result = probe_remote_key(&probe_remote, &probe_key, namespace, credential, budget, &mut carrier).await;
376 (started, started.elapsed(), result, carrier)
377 });
378 }
379 Some(result) = probes.join_next() => {
380 let (started, elapsed, result, carrier) = match result {
381 Ok(result) => result,
382 Err(error) if error.is_cancelled() => continue,
383 Err(error) => (Instant::now(), Duration::ZERO, Err(ProbeFailure::transient(error.to_string())), None),
384 };
385 match result {
386 Ok(()) => {
387 timing.lock().await.record(elapsed);
388 remote_addr.protocol_succeeded();
389 diagnostics.succeeded(elapsed);
390 last_success = Some(Instant::now());
391 consecutive_health_failures = 0;
392 retry_backoff.reset();
393 next_probe = Instant::now() + client_health_check_interval();
394 if !connected {
395 connected = true;
396 if let Some(callback) = &status_callback { callback("connected"); }
397 }
398 }
399 Err(failure) if failure.permanent && diagnostics.has_active_direct() => {
400 diagnostics.failed(failure.kind, Duration::ZERO);
401 if let Some(callback) = &status_callback { callback("retrying"); }
402 next_probe = Instant::now() + Duration::from_secs(30);
403 }
404 Err(failure) if failure.permanent => {
405 if failure.protocol_replied {
406 remote_addr.protocol_succeeded();
407 diagnostics.responded(elapsed);
408 }
409 diagnostics.failed(failure.kind, Duration::ZERO);
410 if let Some(callback) = &status_callback { callback(&format!("failed: {failure}")); }
411 break 'outer;
412 }
413 Err(_) if last_success.is_some_and(|success| success > started) => {
414 next_probe = Instant::now() + client_health_check_interval();
416 }
417 Err(failure) => {
418 failure.update_timing(&mut *timing.lock().await, elapsed);
419 if failure.protocol_replied {
420 remote_addr.protocol_succeeded();
421 diagnostics.responded(elapsed);
422 } else { remote_addr.transport_failed(carrier); }
423 consecutive_health_failures = consecutive_health_failures.saturating_add(1);
424 if !connected || consecutive_health_failures >= client_health_failure_threshold() {
425 connected = false;
426 if let Some(callback) = &status_callback { callback("retrying"); }
427 }
428 let delay = jitter(retry_backoff.next_delay());
429 next_probe = Instant::now() + delay;
430 if diagnostics.failed(failure.kind, delay) {
431 tracing::warn!(event = "client_remote_probe_failed", %key, reason = %failure, consecutive_health_failures, retry_delay = ?delay, "remote probe failed; listener remains active");
432 }
433 }
434 }
435 }
436 Some(success) = stream_event_rx.recv() => {
437 if success {
438 last_success = Some(Instant::now());
439 consecutive_health_failures = 0;
440 retry_backoff.reset();
441 if !connected {
442 connected = true;
443 if let Some(callback) = &status_callback { callback("connected"); }
444 }
445 } else if probes.is_empty() {
446 next_probe = next_probe.min(Instant::now() + Duration::from_millis(100));
449 }
450 }
451 Some(_) = stream_tasks.join_next() => {}
452 }
453 }
454 drop(probes);
455 if let Some(callback) = &status_callback {
456 callback("retrying");
457 }
458 tokio::select! {
459 () = shutdown.cancelled() => break,
460 () = tokio::time::sleep(jitter(retry_backoff.next_delay())) => {}
461 }
462 }
463
464 stream_tasks.shutdown().await;
467 #[cfg(feature = "p2p")]
468 if let Some(direct) = direct {
469 direct.stop().await;
470 }
471}
472
473#[derive(Debug)]
480struct ProbeFailure {
481 reason: String,
482 permanent: bool,
483 kind: RecoveryFailure,
484 protocol_replied: bool,
485}
486
487impl ProbeFailure {
488 fn update_timing(&self, timing: &mut RecoveryTiming, elapsed: Duration) {
489 if self.kind == RecoveryFailure::Timeout {
490 timing.timed_out();
491 } else if self.protocol_replied {
492 timing.record(elapsed);
493 }
494 }
495
496 fn transient(reason: impl Into<String>) -> Self {
499 Self {
500 reason: reason.into(),
501 permanent: false,
502 kind: RecoveryFailure::Transport,
503 protocol_replied: false,
504 }
505 }
506
507 fn from_status_error(context: &str, error: crate::client::error::Error) -> Self {
510 let permanent = error.remote_retryable() == Some(false);
511 let protocol_replied = error.remote_retryable().is_some();
512 Self {
513 reason: format!("{context}: {}", snafu::Report::from_error(error)),
514 permanent,
515 kind: if protocol_replied {
516 RecoveryFailure::Rejected
517 } else {
518 RecoveryFailure::Transport
519 },
520 protocol_replied,
521 }
522 }
523}
524
525impl std::fmt::Display for ProbeFailure {
526 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
527 formatter.write_str(&self.reason)
528 }
529}
530
531async fn probe_remote_key(
532 remote_addr: &RelayEndpoint,
533 key: &str,
534 namespace: Option<u64>,
535 credential: Credential,
536 timeout: Duration,
537 carrier: &mut Option<CarrierId>,
538) -> std::result::Result<(), ProbeFailure> {
539 match tokio::time::timeout(timeout, async {
540 let addresses = remote_addr
543 .addresses()
544 .await
545 .map_err(|error| ProbeFailure {
546 reason: error.to_string(),
547 permanent: false,
548 kind: RecoveryFailure::Dns,
549 protocol_replied: false,
550 })?;
551 probe_remote_key_once(remote_addr, &addresses, key, namespace, credential, carrier).await
552 })
553 .await
554 {
555 Ok(result) => result,
556 Err(_) => Err(ProbeFailure {
557 reason: format!("remote key probe timed out after {timeout:?}"),
558 permanent: false,
559 kind: RecoveryFailure::Timeout,
560 protocol_replied: false,
561 }),
562 }
563}
564
565async fn probe_remote_key_once(
566 remote_addr: &RelayEndpoint,
567 addresses: &ResolvedAddrs,
568 key: &str,
569 namespace: Option<u64>,
570 credential: Credential,
571 carrier: &mut Option<CarrierId>,
572) -> std::result::Result<(), ProbeFailure> {
573 match fetch_remote_status(
574 remote_addr,
575 addresses,
576 PbConnStatusReq::Service {
577 direct_query: None,
578 key: key.to_string(),
579 },
580 namespace,
581 credential,
582 carrier,
583 )
584 .await
585 {
586 Ok(PbConnStatusResp::Service { connections, .. }) => {
587 if connections.iter().any(|conn| conn.healthy) {
588 return Ok(());
589 }
590 return Err(ProbeFailure {
591 reason: format!("client key `{key}` has no healthy remote server connections"),
592 permanent: false,
593 kind: RecoveryFailure::ServiceUnavailable,
594 protocol_replied: true,
595 });
596 }
597 Ok(status_resp) => {
598 return Err(ProbeFailure::transient(format!(
599 "expected service status response, got {status_resp:?}"
600 )));
601 }
602 Err(failure) if failure.permanent => return Err(failure),
605 Err(service_failure) => {
606 tracing::debug!(
607 event = "client_remote_service_probe_failed",
608 key = %key,
609 remote_addr = %remote_addr,
610 reason = %service_failure,
611 "service status probe failed; falling back to key status"
612 );
613 }
614 }
615
616 let status_resp = fetch_remote_status(
617 remote_addr,
618 addresses,
619 PbConnStatusReq::Keys,
620 namespace,
621 credential,
622 carrier,
623 )
624 .await?;
625 let PbConnStatusResp::Keys(keys) = status_resp else {
626 return Err(ProbeFailure::transient(format!(
627 "expected keys status response, got {status_resp:?}"
628 )));
629 };
630 if keys.iter().any(|candidate| candidate == key) {
631 Ok(())
632 } else {
633 Err(ProbeFailure {
634 reason: format!("client key `{key}` is not registered on remote server"),
635 permanent: false,
636 kind: RecoveryFailure::ServiceUnavailable,
637 protocol_replied: true,
638 })
639 }
640}
641
642async fn fetch_remote_status(
643 remote_addr: &RelayEndpoint,
644 addresses: &ResolvedAddrs,
645 req: PbConnStatusReq,
646 namespace: Option<u64>,
647 credential: Credential,
648 carrier: &mut Option<CarrierId>,
649) -> std::result::Result<PbConnStatusResp, ProbeFailure> {
650 let mut stream = remote_addr
651 .connect_addresses(addresses, credential, carrier)
652 .await
653 .map_err(|error| match error {
654 crate::relay::ConnectError::Rejected { response } => ProbeFailure {
655 reason: format!("{}: {}", response.code, response.message),
656 permanent: !response.retryable,
657 kind: RecoveryFailure::Rejected,
658 protocol_replied: true,
659 },
660 error => ProbeFailure::transient(format!("connect remote stream failed: {error}")),
661 })?;
662 get_status_with_credential(&mut stream, req, namespace, &credential)
663 .await
664 .map_err(|error| ProbeFailure::from_status_error("get status failed", error))
665}
666
667pub async fn handle_status_cli_scoped<A: ToSocketAddrs>(
668 op: StatusOp,
669 addr: A,
670 namespace: Option<u64>,
671) -> Result<(), Box<dyn std::error::Error>> {
672 match op {
673 StatusOp::Sessions => show_status_scoped(addr, PbConnStatusReq::Sessions, namespace).await,
674 StatusOp::RemoteId => show_status_scoped(addr, PbConnStatusReq::RemoteId, namespace).await,
675 StatusOp::Keys => show_status_scoped(addr, PbConnStatusReq::Keys, namespace).await,
676 }
677}
678
679pub async fn show_status_scoped<A: ToSocketAddrs>(
680 remote_addr: A,
681 req: PbConnStatusReq,
682 namespace: Option<u64>,
683) -> Result<(), Box<dyn std::error::Error>> {
684 let remote_addr = resolve_all(remote_addr).await?;
685 let mut stream = crate::addr::connect_tcp(&remote_addr)
686 .await
687 .map_err(|error| format!("get status stream: {error}"))?;
688 let status = get_status_scoped(&mut stream, req, namespace).await?;
689 let status = serde_json::to_string_pretty(&status)?;
690 println!("Status:{status}");
691 Ok(())
692}
693
694#[cfg(test)]
695#[allow(clippy::unwrap_used, clippy::expect_used)]
696mod cancellation_audit {
697 use std::{
698 future::Future,
699 sync::{
700 Arc,
701 atomic::{AtomicBool, Ordering},
702 },
703 task::{Context, Wake, Waker},
704 time::Duration,
705 };
706 struct Notified(AtomicBool);
707 impl Wake for Notified {
708 fn wake(self: Arc<Self>) {
709 self.0.store(true, Ordering::SeqCst);
710 }
711 fn wake_by_ref(self: &Arc<Self>) {
712 self.0.store(true, Ordering::SeqCst);
713 }
714 }
715 #[tokio::test]
716 async fn cancelled_udp_accept_must_preserve_the_delivered_peer() {
717 let listener = uni_stream::udp::UdpListener::bind("127.0.0.1:0")
718 .await
719 .unwrap();
720 let addr = listener.local_addr().unwrap();
721 let client = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
722 let notified = Arc::new(Notified(AtomicBool::new(false)));
723 let waker = Waker::from(notified.clone());
724 let mut accept = Box::pin(listener.accept());
725 assert!(
726 accept
727 .as_mut()
728 .poll(&mut Context::from_waker(&waker))
729 .is_pending()
730 );
731 client.send_to(b"first datagram", addr).await.unwrap();
732 tokio::time::timeout(Duration::from_secs(1), async {
733 while !notified.0.load(Ordering::SeqCst) {
734 tokio::task::yield_now().await;
735 }
736 })
737 .await
738 .unwrap();
739 drop(accept);
740 let delivered = tokio::time::timeout(Duration::from_millis(100), listener.accept()).await;
741 assert!(
742 delivered.is_ok(),
743 "UDP peer/first datagram disappeared when a competing select branch cancelled accept"
744 );
745 }
746}
747
748#[cfg(test)]
749mod recovery_tests {
750 use super::*;
751 #[test]
752 fn absent_service_learns_response_latency_without_inflating_timeout() {
753 let mut timing = RecoveryTiming::default();
754 let missing = ProbeFailure {
755 reason: String::new(),
756 permanent: false,
757 kind: RecoveryFailure::ServiceUnavailable,
758 protocol_replied: true,
759 };
760 for _ in 0..20 {
761 missing.update_timing(&mut timing, Duration::from_millis(40));
762 }
763 assert_eq!(timing.timeout(), Duration::from_secs(1));
764 let refusal = ProbeFailure::transient("connection refused");
765 refusal.update_timing(&mut timing, Duration::from_millis(1));
766 assert_eq!(timing.timeout(), Duration::from_secs(1));
767 let timeout = ProbeFailure {
768 kind: RecoveryFailure::Timeout,
769 ..refusal
770 };
771 timeout.update_timing(&mut timing, Duration::from_secs(1));
772 assert_eq!(timing.timeout(), Duration::from_secs(2));
773 }
774}