Skip to main content

tor_proto/util/
tunnel_activity.rs

1//! Helpers for tracking whether a tunnel or circuit is still active.
2
3use derive_deftly::Deftly;
4use std::num::NonZeroUsize;
5use web_time_compat::{Instant, InstantExt};
6
7/// An object to track whether a tunnel or circuit should still be considered active.
8///
9/// The "active" status of a tunnel depends on whether it is in use for streams,
10/// and if not, how much time has passed since it was last in use for streams.
11///
12/// # Ordering and aggregation
13///
14/// We rely on the ordering for `TunnelActivity` structs.
15/// In particular, we depend on the property that a "more active"
16/// `TunnelActivity` is greater than a less active one.
17///
18/// Specifically,
19/// - a TunnelActivity with streams is "more active" than one without streams.
20/// - a TunnelActivity that was last used for streams recently is "more active"
21///   than one that was last used for streams more time ago.
22/// - a TunnelActivity that was ever in use for streams is "more active"
23///   than one that has never been used.
24///
25/// For implementation convenience, we do not generally keep a TunnelActivity
26/// for an entire tunnel.
27/// Instead, we keep a separate TunnelActivity for each hop of each circuit.
28/// When we need to find the TunnelActivity of the tunnel as a whole,
29/// we look for the TunnelActivity of the "most active" hop.
30/// This _does not_ give an accurate count of all the streams on the
31/// tunnel, but we don't generally care about that.
32///
33/// > We could instead _add_ the TunnelActivity for each hop,
34/// > but that would be a bit more implementation effort to little benefit,
35/// > and we'd need to avoid counting conflux join points twice.
36#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
37pub(crate) struct TunnelActivity {
38    /// Actual activity for the associated tunnel.
39    inner: Inner,
40}
41
42/// Inner enumeration used to implement [`TunnelActivity`].
43///
44/// This is a separate type to keep it private.
45//
46// NOTE: Don't re-order these: we rely on the specific behavior of
47// derive(PartialOrd) in order to get the behavior we want.
48#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
49enum Inner {
50    /// The tunnel has never been used for streams.
51    #[default]
52    NeverUsed,
53    /// The tunnel was in use for streams, but has no streams right now.
54    Disused {
55        /// The time at which the last stream was closed.
56        ///
57        /// See note on [`TunnelActivity::disused_since`].
58        since: Instant,
59    },
60    /// The tunnel has open streams.
61    InUse {
62        /// The number of open streams on this tunnel.
63        n_open_streams: NonZeroUsize,
64    },
65}
66
67/// A zero-sized token type returned for each call to [`TunnelActivity::inc_streams()`].
68///
69/// The caller is responsible for passing this object to [`TunnelActivity::dec_streams()`]
70/// when the stream is no longer in use.
71/// Otherwise, this type will panic when it is dropped.
72#[derive(Debug, Deftly)]
73#[must_use]
74#[derive_deftly_adhoc]
75pub(crate) struct InTunnelActivity {
76    /// Prevent this type from being created from other modules.
77    _prevent_create: (),
78}
79
80impl Drop for InTunnelActivity {
81    fn drop(&mut self) {
82        panic!("Dropped an InTunnelActivity without giving it to dec_streams()")
83    }
84}
85
86// Assert that no member of InTunnelActivity actually has meaningful drop semantics.
87//
88// (This lets us call std::mem::forget() below with confidence.)
89derive_deftly::derive_deftly_adhoc! {
90    InTunnelActivity:
91    const _ : () = {
92        $(
93            assert!(! std::mem::needs_drop::<$ftype>());
94        )
95    };
96}
97
98impl InTunnelActivity {
99    /// Consume this token safely, without triggering its drop panic.
100    ///
101    /// Calling this method directly will invalidate the corresponding TunnelActivity's counter.
102    /// Instead, you should usually pass this to [`TunnelActivity::dec_streams`]
103    pub(crate) fn consume_and_forget(self) {
104        std::mem::forget(self);
105    }
106}
107
108impl TunnelActivity {
109    /// Construct a new TunnelActivity for a tunnel that has never been used.
110    pub(crate) fn never_used() -> Self {
111        Self::default()
112    }
113
114    /// Increase the number of streams on this tunnel by one.
115    pub(crate) fn inc_streams(&mut self) -> InTunnelActivity {
116        self.inner = Inner::InUse {
117            n_open_streams: NonZeroUsize::new(self.n_open_streams() + 1)
118                .expect("overflow on stream count"),
119        };
120        InTunnelActivity {
121            _prevent_create: (),
122        }
123    }
124
125    /// Decrease the number of streams on this tunnel by one.
126    pub(crate) fn dec_streams(&mut self, token: InTunnelActivity) {
127        token.consume_and_forget();
128        let Inner::InUse { n_open_streams } = &mut self.inner else {
129            panic!("Tried to decrement 0!");
130        };
131
132        if let Some(new_value) = NonZeroUsize::new(n_open_streams.get() - 1) {
133            *n_open_streams = new_value;
134        } else {
135            self.inner = Inner::Disused {
136                since: Instant::get(),
137            };
138        }
139    }
140
141    /// Return the number of open streams on this tunnel.
142    ///
143    /// (But see note on [`TunnelActivity`] documentation)
144    pub(crate) fn n_open_streams(&self) -> usize {
145        match self.inner {
146            Inner::NeverUsed | Inner::Disused { .. } => 0,
147            Inner::InUse { n_open_streams } => n_open_streams.get(),
148        }
149    }
150
151    /// Return the time at which this tunnel was last in use.
152    ///
153    /// Returns None if the tunnel has open streams right now,
154    /// or if it has never had any open streams.
155    ///
156    /// # A note about time
157    ///
158    /// The returned Instant value is a direct result of an earlier call to `Instant::get()`.
159    /// It is not affected by any runtime mocking.
160    pub(crate) fn disused_since(&self) -> Option<Instant> {
161        match self.inner {
162            Inner::Disused { since } => Some(since),
163            Inner::NeverUsed | Inner::InUse { .. } => None,
164        }
165    }
166}
167
168#[cfg(test)]
169mod test {
170    // @@ begin test lint list maintained by maint/add_warning @@
171    #![allow(clippy::bool_assert_comparison)]
172    #![allow(clippy::clone_on_copy)]
173    #![allow(clippy::dbg_macro)]
174    #![allow(clippy::mixed_attributes_style)]
175    #![allow(clippy::print_stderr)]
176    #![allow(clippy::print_stdout)]
177    #![allow(clippy::single_char_pattern)]
178    #![allow(clippy::unwrap_used)]
179    #![allow(clippy::unchecked_time_subtraction)]
180    #![allow(clippy::useless_vec)]
181    #![allow(clippy::needless_pass_by_value)]
182    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
183
184    use std::time::Duration;
185
186    use super::*;
187    use rand::seq::SliceRandom as _;
188    use tor_basic_utils::test_rng::testing_rng;
189
190    #[test]
191    fn ordering() {
192        use Inner::*;
193        let t1 = Instant::get();
194        let t2 = t1 + Duration::new(60, 0);
195        let t3 = t2 + Duration::new(120, 0);
196        let sorted = vec![
197            NeverUsed,
198            NeverUsed,
199            Disused { since: t1 },
200            Disused { since: t2 },
201            Disused { since: t3 },
202            InUse {
203                n_open_streams: 5.try_into().unwrap(),
204            },
205            InUse {
206                n_open_streams: 10.try_into().unwrap(),
207            },
208        ];
209
210        let mut scrambled = sorted.clone();
211        let mut rng = testing_rng();
212        for _ in 0..=8 {
213            scrambled.shuffle(&mut rng);
214            scrambled.sort();
215            assert_eq!(&scrambled, &sorted);
216        }
217    }
218}