tor_log_ratelim/
logstate.rs

1//! The state for a single backend for a basic log_ratelim().
2
3use std::{error::Error as StdError, fmt, time::Duration};
4
5/// A type-erased error type with the minimum features we need.
6type DynError = Box<dyn StdError + Send + 'static>;
7
8/// The state for a single rate-limited log message.
9///
10/// This type is used as a common implementation helper for the
11/// [`log_ratelim!()`](crate::log_ratelim) macro.
12///
13/// Its role is to track successes and failures,
14/// to remember some error information,
15/// and produce Display-able messages when a [RateLim](crate::ratelim::RateLim)
16/// decides that it is time to log.
17///
18/// This type has to be `pub`, but it is hidden:
19/// using it directly will void your semver guarantees.
20pub struct LogState {
21    /// How many times has the activity failed since we last reset()?
22    n_fail: usize,
23    /// How many times has the activity succeeded since we last reset()?
24    n_ok: usize,
25    /// A string representing the activity itself.
26    activity: String,
27    /// If present, a message that we will along with `error`.
28    error_message: Option<String>,
29    /// If present, an error that we will log when reporting an error.
30    error: Option<DynError>,
31}
32impl LogState {
33    /// Create a new LogState with no recorded errors or successes.
34    pub fn new(activity: String) -> Self {
35        Self {
36            n_fail: 0,
37            n_ok: 0,
38            activity,
39            error_message: None,
40            error: None,
41        }
42    }
43    /// Discard all success and failure information in this LogState.
44    pub fn reset(&mut self) {
45        *self = Self::new(std::mem::take(&mut self.activity));
46    }
47    /// Record a single failure in this LogState.
48    ///
49    /// If this is the _first_ recorded failure, invoke `msg_fn` to get an
50    /// optional failure message and an optional error to be reported as an
51    /// example of the types of failures we are seeing.
52    pub fn note_fail(&mut self, msg_fn: impl FnOnce() -> (Option<String>, Option<DynError>)) {
53        if self.n_fail == 0 {
54            let (m, e) = msg_fn();
55            self.error_message = m;
56            self.error = e;
57        }
58        self.n_fail = self.n_fail.saturating_add(1);
59    }
60    /// Record a single success in this LogState.
61    pub fn note_ok(&mut self) {
62        self.n_ok = self.n_ok.saturating_add(1);
63    }
64    /// Check whether there is any activity to report from this LogState.
65    pub fn activity(&self) -> crate::Activity {
66        if self.n_fail == 0 {
67            crate::Activity::Dormant
68        } else {
69            crate::Activity::Active
70        }
71    }
72    /// Return a wrapper type for reporting that we have observed problems in
73    /// this LogState.
74    pub fn display_problem(&self, dur: Duration) -> impl fmt::Display + '_ {
75        DispProblem(self, dur)
76    }
77    /// Return a wrapper type for reporting that we have not observed problems in
78    /// this LogState.
79    pub fn display_recovery(&self, dur: Duration) -> impl fmt::Display + '_ {
80        DispWorking(self, dur)
81    }
82}
83
84/// Helper: wrapper for reporting problems via Display.
85struct DispProblem<'a>(&'a LogState, Duration);
86impl<'a> fmt::Display for DispProblem<'a> {
87    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88        write!(f, "{}: error", self.0.activity)?;
89        let n_total = self.0.n_fail.saturating_add(self.0.n_ok);
90        write!(
91            f,
92            " (problem occurred {}/{} times in the last {})",
93            self.0.n_fail,
94            n_total,
95            humantime::format_duration(self.1)
96        )?;
97        if let Some(msg) = self.0.error_message.as_ref() {
98            write!(f, ": {}", msg)?;
99        }
100        if let Some(err) = self.0.error.as_ref() {
101            let err = Adaptor(err);
102            write!(f, ": {}", tor_error::Report(&err))?;
103        }
104        Ok(())
105    }
106}
107/// Helper: wrapper for reporting a lack of problems via Display.
108struct DispWorking<'a>(&'a LogState, Duration);
109impl<'a> fmt::Display for DispWorking<'a> {
110    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111        write!(f, "{}: now working", self.0.activity)?;
112        write!(
113            f,
114            " (problem occurred 0/{} times in the last {})",
115            self.0.n_ok,
116            humantime::format_duration(self.1)
117        )?;
118        Ok(())
119    }
120}
121/// Helper struct to make Report work correctly.
122///
123/// We can't use ErrorReport since our `Box<>`ed error is not only `dyn Error`, but also `Send`.
124#[derive(Debug)]
125struct Adaptor<'a>(&'a DynError);
126impl<'a> AsRef<dyn StdError + 'static> for Adaptor<'a> {
127    fn as_ref(&self) -> &(dyn StdError + 'static) {
128        self.0.as_ref()
129    }
130}
131
132#[cfg(test)]
133mod tests {
134
135    #![allow(clippy::redundant_closure)]
136    use super::*;
137    use crate::Activity;
138    use std::time::Duration;
139    use thiserror::Error;
140
141    #[derive(Debug, Error)]
142    #[error("TestError is here!")]
143    struct TestError {
144        source: TestErrorBuddy,
145    }
146
147    #[derive(Debug, Error)]
148    #[error("TestErrorBuddy is here!")]
149    struct TestErrorBuddy;
150
151    #[test]
152    fn display_problem() {
153        let duration = Duration::from_millis(10);
154        let mut ls: LogState = LogState::new("test".to_string());
155        let mut activity: Activity = ls.activity();
156        assert_eq!(Activity::Dormant, activity);
157        assert_eq!(ls.n_fail, 0);
158        fn err_msg() -> (Option<String>, Option<DynError>) {
159            (
160                Some("test".to_string()),
161                Some(Box::new(TestError {
162                    source: TestErrorBuddy,
163                })),
164            )
165        }
166        ls.note_fail(|| err_msg());
167        assert_eq!(ls.n_fail, 1);
168        let problem = ls.display_problem(duration);
169        let expected_problem = String::from(
170            "test: error (problem occurred 1/1 times in the last 10ms): test: error: TestError is here!: TestErrorBuddy is here!"
171        );
172        let str_problem = format!("{problem}");
173        assert_eq!(expected_problem, str_problem);
174        activity = ls.activity();
175        assert_eq!(Activity::Active, activity);
176    }
177
178    #[test]
179    fn display_recovery() {
180        let duration = Duration::from_millis(10);
181        let mut ls = LogState::new("test".to_string());
182        ls.note_ok();
183        assert_eq!(ls.n_ok, 1);
184        {
185            let recovery = ls.display_recovery(duration);
186            let expected_recovery =
187                String::from("test: now working (problem occurred 0/1 times in the last 10ms)");
188            let str_recovery = format!("{recovery}");
189            assert_eq!(expected_recovery, str_recovery);
190        }
191        ls.reset();
192        assert_eq!(ls.n_ok, 0);
193    }
194}