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