ssh_cli/tunnel/stats.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2#![forbid(unsafe_code)]
3//! Shared tunnel counters and the close-reason they resolve to.
4//!
5//! Extracted from `tunnel.rs` so that module stays under the component budget: the
6//! counters are one responsibility (observable state of a running tunnel) and the
7//! orchestration around them is another. `TunnelStats` is re-exported from
8//! `crate::tunnel`, so every existing path keeps working unchanged.
9//!
10//! Workload: lock-free atomics only. Written by the accept loop, read by the
11//! deadline wrapper on a different task, hence `Relaxed` for pure counters and
12//! `Acquire`/`Release` for the flags whose ordering decides the emitted reason.
13
14use std::sync::atomic::{AtomicBool, Ordering};
15
16/// Counters shared between the accept loop and the deadline wrapper.
17///
18/// These deliberately live *outside* the future passed to [`tokio::time::timeout`].
19/// On the deadline path — the most common ending — that future is dropped mid-poll and
20/// never reaches its own tail, so anything owned by it would be lost exactly when the
21/// shutdown event matters most. Holding the counters in an `Arc` the wrapper also owns
22/// lets `tunnel_closed` be emitted on every ending.
23#[derive(Debug, Default)]
24pub struct TunnelStats {
25 /// Connections accepted and handed to a forward task.
26 pub forwards_served: std::sync::atomic::AtomicU64,
27 /// Times a connection waited for a concurrency permit.
28 pub capacity_waits: std::sync::atomic::AtomicU64,
29 /// OS-assigned local port after bind (0 until the listener is up).
30 pub effective_port: std::sync::atomic::AtomicU32,
31 /// Set when the accept loop stopped because of a signal.
32 pub stopped_by_signal: AtomicBool,
33 /// Set when the accept loop stopped because of a fatal accept error.
34 pub stopped_by_accept_error: AtomicBool,
35}
36
37impl TunnelStats {
38 /// Resolves the close reason from the flags the loop managed to set.
39 ///
40 /// Defaults to [`crate::json_wire::TunnelCloseReason::Deadline`]: if neither flag is
41 /// set the loop was still serving when it was cancelled, which is precisely the
42 /// deadline ending.
43 #[must_use]
44 pub fn close_reason(&self) -> crate::json_wire::TunnelCloseReason {
45 use crate::json_wire::TunnelCloseReason;
46 if self.stopped_by_accept_error.load(Ordering::Acquire) {
47 TunnelCloseReason::AcceptError
48 } else if self.stopped_by_signal.load(Ordering::Acquire) {
49 TunnelCloseReason::Signal
50 } else {
51 TunnelCloseReason::Deadline
52 }
53 }
54}
55
56#[cfg(test)]
57mod tests {
58 use super::TunnelStats;
59 use std::sync::atomic::Ordering;
60
61 /// `tunnel_closed` and its counters once existed only inside the printer.
62 ///
63 /// The 0.5.4 audit found `tunnel_closed`, `forwards_served` and `capacity_waits`
64 /// mentioned in exactly one place outside the emitter: a documentation test checking
65 /// the CHANGELOG names them. Deleting the emission entirely would not have turned the
66 /// suite red — the same failure mode `tests/test_quality.rs` exists to prevent, one
67 /// step removed: prose validating prose instead of an assertion between two literals.
68 #[test]
69 fn close_reason_is_derived_from_the_flags_the_loop_managed_to_set() {
70 let stats = TunnelStats::default();
71 assert_eq!(
72 stats.close_reason(),
73 crate::json_wire::TunnelCloseReason::Deadline,
74 "neither flag set means the loop was still serving when it was cancelled"
75 );
76
77 let signalled = TunnelStats::default();
78 signalled.stopped_by_signal.store(true, Ordering::Release);
79 assert_eq!(
80 signalled.close_reason(),
81 crate::json_wire::TunnelCloseReason::Signal
82 );
83
84 let failed = TunnelStats::default();
85 failed
86 .stopped_by_accept_error
87 .store(true, Ordering::Release);
88 assert_eq!(
89 failed.close_reason(),
90 crate::json_wire::TunnelCloseReason::AcceptError
91 );
92
93 // Accept error wins: a loop that died early and *then* saw a signal stopped for
94 // the reason that actually needs operator attention.
95 let both = TunnelStats::default();
96 both.stopped_by_signal.store(true, Ordering::Release);
97 both.stopped_by_accept_error.store(true, Ordering::Release);
98 assert_eq!(
99 both.close_reason(),
100 crate::json_wire::TunnelCloseReason::AcceptError
101 );
102 }
103}