emergency_brake/
lib.rs

1//! eBrake creates a moving sample window of the last N samples. If the number of
2//! failures in the sample window exceeds the threshold, the process or service
3//! will be terminated. The sample window is a circular buffer, so the oldest
4//! sample will be replaced by the newest sample.
5//! 
6//! # Examples
7//! 
8//! This will use the sample and trigger functions separately.
9//! ```
10//! use emergency_brake::*;
11//! let sample_window_size = 25;
12//! let failure_threshold = 3;
13//! let mut ebrake = EBrake::new(sample_window_size, failure_threshold);
14//! for _ in 0..sample_window_size {
15//!    ebrake.add_sample(true);
16//! }
17//! assert_eq!(ebrake.trigger(&Trigger::Panic), false);
18//! ```
19//! 
20//! This will use the trigger_on_sample function.
21//! ```
22//! use emergency_brake::*;
23//! let sample_window_size = 25;
24//! let failure_threshold = 3;
25//! let mut ebrake = EBrake::new(sample_window_size, failure_threshold);
26//! for _ in 0..sample_window_size {
27//!   ebrake.trigger_on_sample(true, &Trigger::Panic);
28//! }
29//! assert_eq!(ebrake.trigger(&Trigger::Panic), false);
30//! ```
31//! 
32//! 
33//! Kelsea Blackwell (c) 2023
34//! See LICENSE for licensing information.
35
36#![deny(missing_docs)]
37
38#[cfg(feature = "service_checker")]
39use async_trait::async_trait;
40
41
42use std::collections::VecDeque;
43use std::process;
44
45#[cfg(feature = "service_checker")]
46use reqwest;
47
48#[cfg(feature = "service_checker")]
49use tokio;
50
51use tracing::error;
52
53
54
55
56
57/// The EmergencyBrake trait is the interface for the emergency brake.
58pub trait EmergencyBrake {
59    /// Insert a sample into the emergency brake.
60    /// This will pop the oldest sample if the queue is full.
61    /// `true` indicates a success, `false` indicates a failure.
62    fn add_sample(&mut self, sample: bool);
63
64    /// Returns true if the emergency brake should be triggered.
65    /// Returns false if the emergency brake should not be triggered.
66    fn should_trigger(&self) -> bool;
67
68    /// Returns false if the emergency brake has not been triggered.
69    /// If the emergency brake has been triggered, the process supplied trigger action will be executed.
70    fn trigger(&self, trigger: &'static Trigger) -> bool;
71
72    /// Returns false if the emergency brake has not been triggered.
73    /// If the emergency brake has been triggered, the process will be aborted.
74    fn trigger_abort(&self) -> bool;
75
76    /// Returns false if the emergency brake has not been triggered.
77    /// If the emergency brake has been triggered, a panic will occur.
78    fn trigger_panic(&self) -> bool;
79
80    /// Insert a sample and check if the emergency brake should be triggered.
81    fn trigger_on_sample(&mut self, sample: bool, trigger: &'static Trigger) -> bool;
82}
83
84
85/// The ServiceCheck trait is the interface for checking or monitoring a service.
86#[cfg_attr(docsrs, doc(cfg(feature = "service_checker")))]
87#[cfg(feature = "service_checker")]
88#[async_trait]
89pub trait ServiceChecker {
90    /// Check if the service is running. This takes a URI as a parameter, and
91    /// performs a basic HTTP GET request to the URI. If the request is successful,
92    /// it will return true and assume the service is running, false otherwise.
93    async fn check_service_endpoint(&self, uri: &str) -> bool;
94
95    /// Similar to check_service_endpoint, but will check the service at a given
96    /// interval. This will spawn a background thread and consume the current
97    /// instance of the EBrake. If the service stops responding, the EBrake will
98    /// be triggered and the process will be aborted.
99    async fn watch_service_endpoint(mut self, uri: &'static str, interval: usize, trigger: &'static Trigger);
100}
101
102/// The Trigger enum defines the action to take when the emergency brake is triggered.
103#[derive(Clone, Debug, PartialEq)]
104pub enum Trigger {
105    /// Abort the process.
106    Abort,
107
108    /// Panic the process.
109    Panic,
110}
111
112/// The emergency brake is a circular queue of boolean samples with a defined size and tolerance.
113#[derive(Clone, Debug, Default)]
114pub struct EBrake {
115    data: VecDeque<bool>,
116    failures: usize,
117    samples: usize,
118    successes: usize,
119    tolerance: usize,
120}
121
122
123impl Default for Trigger {
124    fn default() -> Self {
125        Trigger::Panic
126    }
127}
128
129impl EmergencyBrake for EBrake {
130    fn add_sample(&mut self, sample: bool) {
131        if self.data.len() == self.samples {
132            match self.data.pop_front() {
133                Some(true) => self.successes -= 1,
134                Some(false) => self.failures -= 1,
135                None => {},
136            }
137        }
138        
139        match sample {
140            true => self.successes += 1,
141            false => self.failures += 1,
142        }
143
144        self.data.push_back(sample);
145    }
146
147    fn should_trigger(&self) -> bool {
148        if self.data.len() < self.tolerance {
149            return false;
150        }
151
152        self.failures > self.tolerance
153    }
154
155    fn trigger(&self, trigger: &'static Trigger) -> bool {
156        match self.should_trigger() {
157            true => {
158                error!("Emergency brake triggered!");
159                match trigger {
160                    Trigger::Abort => process::abort(),
161                    Trigger::Panic => panic!("Emergency brake triggered!"),
162                }
163            },
164            false => false,
165        }
166    }
167
168    fn trigger_abort(&self) -> bool {
169        match self.should_trigger() {
170            true => {
171                error!("Emergency brake abort triggered!");
172                process::abort();
173            },
174            false => false,
175        }
176    }
177
178    fn trigger_panic(&self) -> bool {
179        match self.should_trigger() {
180            true => {
181                error!("Emergency brake panic triggered!");
182                panic!("Emergency brake panic triggered!");
183            },
184            false => false,
185        }
186    }
187
188    fn trigger_on_sample(&mut self, sample: bool, trigger: &'static Trigger) -> bool {
189        self.add_sample(sample);
190        self.trigger(trigger)
191    }
192}
193
194
195
196#[cfg(feature = "service_checker")]
197#[async_trait]
198impl ServiceChecker for EBrake {
199    async fn check_service_endpoint(&self, uri: &str) -> bool {
200        let client = reqwest::Client::new();
201        let response = client.get(uri).send().await;
202        match response {
203            Ok(_) => true,
204            Err(_) => false,
205        }
206    }
207
208    async fn watch_service_endpoint(mut self, uri: &'static str, interval: usize, trigger: &'static Trigger) {
209        tokio::spawn(async move {
210            let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(interval as u64));
211            loop {
212                interval.tick().await;
213                let result = self.check_service_endpoint(uri).await;
214                self.trigger_on_sample(result, trigger);
215            }
216        });
217    }
218}
219
220
221impl EBrake {
222    /// Creates a new Emergency Brake with the given number of samples and tolerance.
223    /// ```
224    /// use emergency_brake::EBrake;
225    /// let ebrake = EBrake::new(10, 3);
226    /// ```
227    pub fn new(samples: usize, tolerance: usize) -> Self {
228        EBrake {
229            data: VecDeque::with_capacity(samples),
230            failures: 0,
231            samples: samples,
232            successes: 0,
233            tolerance: tolerance,
234        }
235    }
236}
237
238
239/// Test module for the Emergency Brake.
240#[cfg(test)]
241mod test;
242