1
//! The state for a single backend for a basic log_ratelim().
2

            
3
use std::{error::Error as StdError, fmt, time::Duration};
4

            
5
/// A type-erased error type with the minimum features we need.
6
type 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.
20
pub 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
}
32
impl LogState {
33
    /// Create a new LogState with no recorded errors or successes.
34
6
    pub fn new(activity: String) -> Self {
35
6
        Self {
36
6
            n_fail: 0,
37
6
            n_ok: 0,
38
6
            activity,
39
6
            error_message: None,
40
6
            error: None,
41
6
        }
42
6
    }
43
    /// Discard all success and failure information in this LogState.
44
2
    pub fn reset(&mut self) {
45
2
        *self = Self::new(std::mem::take(&mut self.activity));
46
2
    }
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
2
    pub fn note_fail(&mut self, msg_fn: impl FnOnce() -> (Option<String>, Option<DynError>)) {
53
2
        if self.n_fail == 0 {
54
2
            let (m, e) = msg_fn();
55
2
            self.error_message = m;
56
2
            self.error = e;
57
2
        }
58
2
        self.n_fail = self.n_fail.saturating_add(1);
59
2
    }
60
    /// Record a single success in this LogState.
61
2
    pub fn note_ok(&mut self) {
62
2
        self.n_ok = self.n_ok.saturating_add(1);
63
2
    }
64
    /// Check whether there is any activity to report from this LogState.
65
4
    pub fn activity(&self) -> crate::Activity {
66
4
        if self.n_fail == 0 {
67
2
            crate::Activity::Dormant
68
        } else {
69
2
            crate::Activity::Active
70
        }
71
4
    }
72
    /// Return a wrapper type for reporting that we have observed problems in
73
    /// this LogState.
74
2
    pub fn display_problem(&self, dur: Duration) -> impl fmt::Display + '_ {
75
2
        DispProblem(self, dur)
76
2
    }
77
    /// Return a wrapper type for reporting that we have not observed problems in
78
    /// this LogState.
79
2
    pub fn display_recovery(&self, dur: Duration) -> impl fmt::Display + '_ {
80
2
        DispWorking(self, dur)
81
2
    }
82
}
83

            
84
/// Helper: wrapper for reporting problems via Display.
85
struct DispProblem<'a>(&'a LogState, Duration);
86
impl<'a> fmt::Display for DispProblem<'a> {
87
2
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88
2
        write!(f, "{}: error", self.0.activity)?;
89
2
        let n_total = self.0.n_fail.saturating_add(self.0.n_ok);
90
2
        write!(
91
2
            f,
92
2
            " (problem occurred {}/{} times in the last {})",
93
2
            self.0.n_fail,
94
2
            n_total,
95
2
            humantime::format_duration(self.1)
96
2
        )?;
97
2
        if let Some(msg) = self.0.error_message.as_ref() {
98
2
            write!(f, ": {}", msg)?;
99
        }
100
2
        if let Some(err) = self.0.error.as_ref() {
101
2
            let err = Adaptor(err);
102
2
            write!(f, ": {}", tor_error::Report(&err))?;
103
        }
104
2
        Ok(())
105
2
    }
106
}
107
/// Helper: wrapper for reporting a lack of problems via Display.
108
struct DispWorking<'a>(&'a LogState, Duration);
109
impl<'a> fmt::Display for DispWorking<'a> {
110
2
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111
2
        write!(f, "{}: now working", self.0.activity)?;
112
2
        write!(
113
2
            f,
114
2
            " (problem occurred 0/{} times in the last {})",
115
2
            self.0.n_ok,
116
2
            humantime::format_duration(self.1)
117
2
        )?;
118
2
        Ok(())
119
2
    }
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)]
125
struct Adaptor<'a>(&'a DynError);
126
impl<'a> AsRef<dyn StdError + 'static> for Adaptor<'a> {
127
2
    fn as_ref(&self) -> &(dyn StdError + 'static) {
128
2
        self.0.as_ref()
129
2
    }
130
}
131

            
132
#[cfg(test)]
133
mod 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
}