tor_guardmgr/dirstatus.rs
1//! Types and code to track the readiness status of a directory cache.
2
3use std::time::{Duration, Instant};
4use tor_basic_utils::retry::RetryDelay;
5
6/// Status information about whether a
7/// [`FallbackDir`](crate::fallback::FallbackDir) or
8/// [`Guard`](crate::guard::Guard) is currently usable as a directory cache.
9///
10/// This structure is used to track whether the cache has recently failed, and
11/// if so, when it can be retried.
12#[derive(Debug, Clone)]
13pub(crate) struct DirStatus {
14 /// Used to decide how long to delay before retrying a fallback cache
15 /// that has failed.
16 delay: RetryDelay,
17 /// A time before which we should assume that this fallback cache is broken.
18 ///
19 /// If None, then this fallback cache is ready to use right away.
20 retry_at: Option<Instant>,
21}
22
23impl DirStatus {
24 /// Construct a new DirStatus object with a given lower-bound for delays
25 /// after failure.
26 pub(crate) fn new(delay_floor: Duration) -> Self {
27 DirStatus {
28 delay: RetryDelay::from_duration(delay_floor),
29 retry_at: None,
30 }
31 }
32
33 /// Return true if this `Status` is usable at the time `now`.
34 pub(crate) fn usable_at(&self, now: Instant) -> bool {
35 match self.retry_at {
36 Some(ready) => now >= ready,
37 None => true,
38 }
39 }
40
41 /// Return the time at which this `Status` can next be retried.
42 ///
43 /// A return value of `None`, or of a time in the past, indicates that this
44 /// status can be used immediately.
45 pub(crate) fn next_retriable(&self) -> Option<Instant> {
46 self.retry_at
47 }
48
49 /// Record that the associated fallback directory has been used successfully.
50 ///
51 /// This should only be done after successfully handling a whole reply from the
52 /// directory.
53 pub(crate) fn note_success(&mut self) {
54 self.retry_at = None;
55 self.delay.reset();
56 }
57
58 /// Record that the associated fallback directory has failed.
59 pub(crate) fn note_failure(&mut self, now: Instant) {
60 let mut rng = rand::rng();
61 self.retry_at = Some(now + self.delay.next_delay(&mut rng));
62 }
63}
64
65#[cfg(test)]
66mod test {
67 // @@ begin test lint list maintained by maint/add_warning @@
68 #![allow(clippy::bool_assert_comparison)]
69 #![allow(clippy::clone_on_copy)]
70 #![allow(clippy::dbg_macro)]
71 #![allow(clippy::mixed_attributes_style)]
72 #![allow(clippy::print_stderr)]
73 #![allow(clippy::print_stdout)]
74 #![allow(clippy::single_char_pattern)]
75 #![allow(clippy::unwrap_used)]
76 #![allow(clippy::unchecked_duration_subtraction)]
77 #![allow(clippy::useless_vec)]
78 #![allow(clippy::needless_pass_by_value)]
79 //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
80 use super::*;
81
82 #[test]
83 fn status_basics() {
84 let now = Instant::now();
85
86 /// floor to use for testing.
87 const FLOOR: Duration = Duration::from_secs(99);
88
89 let mut status = DirStatus::new(FLOOR);
90 // newly created status is usable.
91 assert!(status.usable_at(now));
92
93 // no longer usable after a failure.
94 status.note_failure(now);
95 assert_eq!(status.next_retriable().unwrap(), now + FLOOR);
96 assert!(!status.usable_at(now));
97
98 // Not enough time has passed.
99 assert!(!status.usable_at(now + FLOOR / 2));
100
101 // Enough time has passed.
102 assert!(status.usable_at(now + FLOOR));
103
104 // Mark as failed again; the timeout will (probably) be longer.
105 status.note_failure(now + FLOOR);
106 assert!(status.next_retriable().unwrap() >= now + FLOOR * 2);
107 assert!(!status.usable_at(now + FLOOR));
108
109 // Mark as succeeded; it should be usable immediately.
110 status.note_success();
111 assert!(status.usable_at(now + FLOOR));
112 }
113}