exocore_chain/engine/
request_tracker.rs

1use std::time::Duration;
2
3use exocore_core::{
4    time::{Clock, Instant},
5    utils::backoff::{BackoffCalculator, BackoffConfig},
6};
7
8/// Handles time tracking of synchronization request and response for timeouts,
9/// retries and backoff.
10pub struct RequestTracker {
11    clock: Clock,
12    backoff_calculator: BackoffCalculator,
13
14    last_request_send: Option<Instant>,
15    last_response_receive: Option<Instant>,
16
17    // next can_send_request() will return true
18    force_next_request: Option<bool>,
19}
20
21impl RequestTracker {
22    pub fn new_with_clock(clock: Clock, config: RequestTrackerConfig) -> RequestTracker {
23        let backoff_calculator = BackoffCalculator::new(
24            clock.clone(),
25            BackoffConfig {
26                normal_constant: config.min_interval,
27                failure_constant: config.min_interval,
28                failure_exp_base: 2.0,
29                failure_exp_multiplier: Duration::from_secs(5),
30                failure_maximum: config.max_interval,
31            },
32        );
33
34        RequestTracker {
35            clock,
36            backoff_calculator,
37
38            last_request_send: None,
39            last_response_receive: None,
40
41            force_next_request: None,
42        }
43    }
44
45    pub fn set_last_send_now(&mut self) {
46        self.last_request_send = Some(self.clock.instant());
47    }
48
49    pub fn set_last_responded_now(&mut self) {
50        self.last_response_receive = Some(self.clock.instant());
51        self.backoff_calculator.reset();
52    }
53
54    pub fn can_send_request(&mut self) -> bool {
55        if let Some(_force) = self.force_next_request.take() {
56            return true;
57        }
58
59        let should_send_request = self.last_request_send.map_or(true, |send_time| {
60            (self.clock.instant() - send_time) >= self.backoff_calculator.backoff_duration()
61        });
62
63        if should_send_request {
64            if self.last_request_send.is_some() && !self.has_responded_last_request() {
65                self.backoff_calculator.increment_failure();
66            }
67
68            true
69        } else {
70            false
71        }
72    }
73
74    pub fn force_next_request(&mut self) {
75        self.force_next_request = Some(true);
76    }
77
78    pub fn reset(&mut self) {
79        self.last_request_send = None;
80        self.last_response_receive = None;
81        self.force_next_request = None;
82    }
83
84    fn has_responded_last_request(&self) -> bool {
85        matches!((self.last_request_send, self.last_response_receive), (Some(send), Some(resp)) if resp > send)
86    }
87
88    pub fn response_failure_count(&self) -> usize {
89        self.backoff_calculator.consecutive_failures_count() as usize
90    }
91
92    #[cfg(test)]
93    pub fn set_response_failure_count(&mut self, count: usize) {
94        for _ in 0..count {
95            self.backoff_calculator.increment_failure();
96        }
97    }
98}
99
100/// Configuration for RequestTracker
101#[derive(Clone, Copy, Debug)]
102pub struct RequestTrackerConfig {
103    pub min_interval: Duration,
104    pub max_interval: Duration,
105}
106
107impl Default for RequestTrackerConfig {
108    fn default() -> Self {
109        RequestTrackerConfig {
110            min_interval: Duration::from_secs(5),
111            max_interval: Duration::from_secs(30),
112        }
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use exocore_core::time::Clock;
119
120    use super::*;
121
122    #[test]
123    fn test_can_send_request_interval() {
124        let mock_clock = Clock::new_mocked();
125        let mut tracker =
126            RequestTracker::new_with_clock(mock_clock.clone(), RequestTrackerConfig::default());
127
128        // should be able to do request right away
129        assert!(tracker.can_send_request());
130
131        // if we have sent, we shouldn't be able to re-do a query until timeout
132        tracker.set_last_send_now();
133        assert!(!tracker.can_send_request());
134        assert!(!tracker.has_responded_last_request());
135
136        // after timeout, we should be able to make query, but then # failures is
137        // increased
138        mock_clock.set_fixed_instant(Instant::now() + Duration::from_millis(5001));
139        assert!(tracker.can_send_request());
140        assert!(!tracker.has_responded_last_request());
141        assert_eq!(tracker.response_failure_count(), 1);
142    }
143
144    #[test]
145    fn test_force_request() {
146        let mock_clock = Clock::new_mocked();
147        let mut tracker =
148            RequestTracker::new_with_clock(mock_clock, RequestTrackerConfig::default());
149
150        tracker.can_send_request();
151        tracker.set_last_send_now();
152
153        assert!(!tracker.can_send_request());
154        tracker.force_next_request();
155        assert!(tracker.can_send_request());
156    }
157
158    #[test]
159    fn test_reset() {
160        let mock_clock = Clock::new_mocked();
161        let mut tracker =
162            RequestTracker::new_with_clock(mock_clock, RequestTrackerConfig::default());
163
164        tracker.can_send_request();
165        tracker.set_last_send_now();
166
167        assert!(!tracker.can_send_request());
168        tracker.reset();
169        assert!(tracker.can_send_request());
170    }
171}