Skip to main content

pb_mapper_client/client/
mod.rs

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
36// Callback for notifying status changes to external systems
37pub type ClientStatusCallback = Box<dyn Fn(&str) + Send + Sync>;
38
39/// Resolve both ends, reporting a bad address through `status_callback`.
40///
41/// The callback is the only channel a caller has here — these entry points return
42/// `()` — so an address that never resolves has to settle the tunnel as failed
43/// rather than just leaving a log line behind.
44async 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/// Same as [`run_client_side_cli_with_callback_scoped`], but the retry loop
153/// returns when `shutdown` is cancelled.
154///
155/// Takes both ends already resolved, because its caller — the SDK — resolves them
156/// itself in order to report a bad address as an error rather than a log line.
157#[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        // Listener lifetime follows the local endpoint, not a remote health
295        // snapshot. Only a local bind/accept error requires replacing it.
296        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                    // Backpressure stays in the listener backlog while setup is
332                    // saturated. Established forwarding releases its permit.
333                    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                            // Actual traffic completed after this probe began.
410                            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                        // Coalesce failures and rate limit probes independently
442                        // of the number of callers arriving during an outage.
443                        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    // Wait for the forwarding sessions to observe the cancellation, so returning
460    // from here means no stream of this tunnel is still moving bytes.
461    stream_tasks.shutdown().await;
462    #[cfg(feature = "p2p")]
463    if let Some(direct) = direct {
464        direct.stop().await;
465    }
466}
467
468/// Why a remote probe did not confirm the service, and whether waiting could
469/// change that.
470///
471/// The distinction is what keeps the retry loop from spinning: a service that has
472/// not registered yet may still appear, but a namespace the credential does not
473/// own never will.
474#[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    /// A failure worth retrying: a transport error, or a service that is simply
492    /// not registered yet.
493    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    /// A status failure classified by the relay's own `retryable` verdict, which
503    /// is the only party that knows whether the refusal is final.
504    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        // Resolved once, here, so a name that does not resolve is reported
536        // as such rather than as the relay being unreachable.
537        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        // A refusal the relay calls final applies to the keys probe just as much,
598        // so there is nothing to fall back to.
599        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}