Skip to main content

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}