Skip to main content

tokio_too_busy/
lib.rs

1//! Identify when the Tokio workers are too busy to handle more work.
2//!
3//! ### Monitor the Tokio workers' busy ratio
4//! [TooBusy] allows you to track the busy ratio of the Tokio workers and react accordingly
5//! when they are too busy.
6//!
7//! In the example below, a [`TooBusy`] is [built][TooBusy::builder] and used to
8//! [evaluate][TooBusy::eval] the busy ratio of the Tokio workers and progressively reject requests
9//! when the busy ratio goes above the [low watermark][TooBusyBuilder::low_watermark] threshold:
10//! ```
11//!use axum::{
12//!   extract::{Request, State},
13//!   http::StatusCode,
14//!   middleware::{self, Next},
15//!   response::{IntoResponse, Response},
16//!   routing::get,
17//!   Router,
18//!};
19//!use tokio::time::Duration;
20//!use tokio_too_busy::*;
21//!
22//!#[derive(Clone)]
23//!struct AppState {
24//!    too_busy: TooBusy,
25//!}
26//!
27//!async fn my_middleware(State(state): State<AppState>, request: Request, next: Next) -> Response {
28//!
29//!   if state.too_busy.eval() {
30//!       return (StatusCode::SERVICE_UNAVAILABLE, "Server is too busy").into_response();
31//!   }
32//!   next.run(request).await
33//!}
34//!
35//!async fn root() -> &'static str {
36//!    "Hello, World!"
37//!}
38//!
39//!#[tokio::main(flavor = "multi_thread", worker_threads = 1)]
40//!async fn main() {
41//!   let too_busy = TooBusy::builder()
42//!       .interval(Duration::from_secs(1))
43//!       .ewma_alpha(0.1)
44//!       .high_watermark(95)
45//!       .low_watermark(90)
46//!       .build();
47//!
48//!   let state = AppState { too_busy };
49//!
50//!   let app = Router::new()
51//!      .route("/", get(root))
52//!      .route_layer(middleware::from_fn_with_state(state.clone(), my_middleware))
53//!      .with_state(state);
54//!
55//!  let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap();
56//!  axum::serve(listener, app).await.unwrap();
57//!}
58//! ```
59//! Check the full example in the [examples] directory.
60//!
61//! Tokio too busy leverages the [worker_total_busy_duration] interface provided by the Tokio [Runtime] for knowledge.
62//! and its only available when Tokio is compiled using the following flags ```RUSTFLAGS="--cfg tokio_unstable"```.
63//!
64//! Busy ratio is calculated by averaging the busy duration of all workers since latest iteration and using an
65//! exponential moving average (EWMA).
66//!
67//! [examples]: https://github.com/pfreixes/tokio-too-busy/tree/main/examples
68//! [worker_total_busy_duration]: https://docs.rs/tokio/1.0.1/tokio/runtime/struct.Runtime.html#method.worker_total_busy_duration
69//! [Runtime]: https://docs.rs/tokio/1.0.1/tokio/runtime/struct.Runtime.html
70mod inner;
71use crate::inner::{LoadFeeder, TooBusyShared};
72use std::sync::Arc;
73use tokio::time::Duration;
74
75/// Track how busy are the tokio workers.
76///
77/// This type is internally reference-counted and can be freely cloned.
78pub struct TooBusy {
79    inner: Arc<TooBusyShared>,
80}
81
82impl TooBusy {
83    /// Create a new builder to configure the [`TooBusy`] instance.
84    ///
85    /// # Examples
86    ///
87    /// ```
88    /// use tokio_too_busy::TooBusy;
89    /// use std::time::Duration;
90    ///
91    /// let too_busy = TooBusy::builder()
92    ///     .interval(Duration::from_secs(1))
93    ///     .ewma_alpha(0.1)
94    ///     .high_watermark(95)
95    ///     .low_watermark(90)
96    ///     .build();
97    /// ```
98    pub fn builder() -> TooBusyBuilder {
99        TooBusyBuilder::default()
100    }
101
102    /// Get the current busy ratio of the Tokio workers.
103    pub fn ratio_busy_ewma(&self) -> u32 {
104        self.inner.ratio_busy_ewma()
105    }
106
107    /// Evaluate if the Tokio workers are too busy.
108    ///
109    /// Returns `true` if the busy ratio is above the low watermark threshold using
110    /// a progressive rejection strategy until reach the high watermark threshold, above
111    /// the high watermark all evaluations will return `true`. Otherwise, it returns `false`.
112    pub fn eval(&self) -> bool {
113        self.inner.eval()
114    }
115}
116
117impl Clone for TooBusy {
118    fn clone(&self) -> Self {
119        TooBusy {
120            inner: self.inner.clone(),
121        }
122    }
123}
124
125/// This builder allows you to configure the [`TooBusy`] instance.
126pub struct TooBusyBuilder {
127    interval: Duration,
128    low_watermark: u32,
129    high_watermark: u32,
130    ewma_alpha: f32,
131}
132
133impl TooBusyBuilder {
134    /// Set the interval to update the busy ratio.
135    ///
136    /// By default, it is 1 seconds.
137    pub fn interval(mut self, interval: Duration) -> TooBusyBuilder {
138        self.interval = interval;
139        self
140    }
141    /// Set the low watermark threshold to start considering the workers too busy.
142    ///
143    /// By default, it is 85%.
144    pub fn low_watermark(mut self, low_watermark: u32) -> TooBusyBuilder {
145        assert!(
146            low_watermark < self.high_watermark,
147            "low_watermark must be lower than high_watermark!"
148        );
149        self.low_watermark = low_watermark;
150        self
151    }
152    /// Set the high watermark threshold to start considering the workers too busy all the time.
153    ///
154    /// By default, it is 95%.
155    pub fn high_watermark(mut self, high_watermark: u32) -> TooBusyBuilder {
156        assert!(
157            high_watermark > self.low_watermark,
158            "high_watermark must be greater than low_watermark!"
159        );
160        self.high_watermark = high_watermark;
161        self
162    }
163    /// Set the alpha value for the EWMA calculation.
164    ///
165    /// By default, it is 0.1 (10%) for having quick reactions to changes. If you want to smooth the changes
166    /// you can set a higher value.
167    pub fn ewma_alpha(mut self, ewma_alpha: f32) -> TooBusyBuilder {
168        assert!(
169            ewma_alpha > 0.0 && ewma_alpha < 1.0,
170            "ewma_alpha must be between the range (0, 1)!"
171        );
172        self.ewma_alpha = ewma_alpha;
173        self
174    }
175    /// Build the [`TooBusy`] instance.
176    pub fn build(self) -> TooBusy {
177        let inner = Arc::new(TooBusyShared::new(self.low_watermark, self.high_watermark));
178        let weak_reference = Arc::downgrade(&inner);
179
180        tokio::spawn(async move {
181            LoadFeeder::new(weak_reference, self.interval, self.ewma_alpha)
182                .run()
183                .await;
184        });
185
186        TooBusy { inner }
187    }
188}
189
190impl Default for TooBusyBuilder {
191    fn default() -> Self {
192        TooBusyBuilder {
193            interval: Duration::from_secs(1),
194            low_watermark: 85,
195            high_watermark: 95,
196            ewma_alpha: 0.1,
197        }
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    #[test]
206    #[should_panic]
207    fn too_busy_builder_invalid_low_watermark() {
208        TooBusyBuilder::default()
209            .high_watermark(90)
210            .low_watermark(90);
211    }
212
213    #[test]
214    #[should_panic]
215    fn too_busy_builder_invalid_high_watermark() {
216        TooBusyBuilder::default()
217            .low_watermark(90)
218            .high_watermark(90);
219    }
220
221    #[test]
222    #[should_panic]
223    fn too_busy_builder_invalid_ewma_alpha_too_low() {
224        TooBusyBuilder::default().ewma_alpha(0.0);
225    }
226
227    #[test]
228    #[should_panic]
229    fn too_busy_builder_invalid_ewma_alpha_too_high() {
230        TooBusyBuilder::default().ewma_alpha(1.0);
231    }
232}