Skip to main content

rustfs_io_core/
backpressure.rs

1// Copyright 2024 RustFS Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Backpressure management for I/O operations.
16//!
17//! This module provides backpressure mechanisms to prevent system overload
18//! and maintain stability under high load conditions.
19
20use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
21use std::time::{Duration, Instant};
22
23/// Backpressure configuration.
24#[derive(Debug, Clone)]
25pub struct BackpressureConfig {
26    /// Maximum concurrent operations.
27    pub max_concurrent: usize,
28    /// High water mark (percentage of max_concurrent).
29    pub high_water_mark: f64,
30    /// Low water mark (percentage of max_concurrent).
31    pub low_water_mark: f64,
32    /// Cooldown period after applying backpressure.
33    pub cooldown: Duration,
34    /// Whether backpressure is enabled.
35    pub enabled: bool,
36}
37
38impl Default for BackpressureConfig {
39    fn default() -> Self {
40        Self {
41            max_concurrent: 32,
42            high_water_mark: 0.8,
43            low_water_mark: 0.5,
44            cooldown: Duration::from_millis(100),
45            enabled: true,
46        }
47    }
48}
49
50impl BackpressureConfig {
51    /// Create new configuration.
52    pub fn new() -> Self {
53        Self::default()
54    }
55
56    /// Get the high water mark threshold.
57    pub fn high_threshold(&self) -> usize {
58        (self.max_concurrent as f64 * self.high_water_mark) as usize
59    }
60
61    /// Get the low water mark threshold.
62    pub fn low_threshold(&self) -> usize {
63        (self.max_concurrent as f64 * self.low_water_mark) as usize
64    }
65
66    /// Validate the configuration.
67    pub fn validate(&self) -> Result<(), BackpressureError> {
68        if self.max_concurrent == 0 {
69            return Err(BackpressureError::InvalidConfig("max_concurrent must be > 0".to_string()));
70        }
71        if self.high_water_mark <= self.low_water_mark || self.high_water_mark > 1.0 {
72            return Err(BackpressureError::InvalidConfig(
73                "high_water_mark must be > low_water_mark and <= 1.0".to_string(),
74            ));
75        }
76        if self.low_water_mark < 0.0 {
77            return Err(BackpressureError::InvalidConfig("low_water_mark must be >= 0.0".to_string()));
78        }
79        Ok(())
80    }
81}
82
83/// Backpressure error.
84#[derive(Debug, Clone, thiserror::Error)]
85pub enum BackpressureError {
86    /// Invalid configuration.
87    #[error("Invalid backpressure config: {0}")]
88    InvalidConfig(String),
89}
90
91/// Backpressure state.
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
93pub enum BackpressureState {
94    /// Normal operation.
95    #[default]
96    Normal,
97    /// Warning: approaching high water mark.
98    Warning,
99    /// Critical: backpressure applied.
100    Critical,
101}
102
103impl BackpressureState {
104    /// Get state as string.
105    pub fn as_str(&self) -> &'static str {
106        match self {
107            BackpressureState::Normal => "normal",
108            BackpressureState::Warning => "warning",
109            BackpressureState::Critical => "critical",
110        }
111    }
112}
113
114/// Backpressure monitor.
115pub struct BackpressureMonitor {
116    /// Configuration.
117    config: BackpressureConfig,
118    /// Current concurrent operations.
119    current: AtomicUsize,
120    /// Total operations processed.
121    total_processed: AtomicU64,
122    /// Total operations rejected.
123    total_rejected: AtomicU64,
124    /// Current state.
125    state: std::sync::Mutex<BackpressureState>,
126    /// Last state change time.
127    last_state_change: std::sync::Mutex<Option<Instant>>,
128    /// Whether backpressure is currently active.
129    active: AtomicBool,
130}
131
132impl BackpressureMonitor {
133    /// Create a new backpressure monitor.
134    pub fn new(config: BackpressureConfig) -> Self {
135        Self {
136            config,
137            current: AtomicUsize::new(0),
138            total_processed: AtomicU64::new(0),
139            total_rejected: AtomicU64::new(0),
140            state: std::sync::Mutex::new(BackpressureState::Normal),
141            last_state_change: std::sync::Mutex::new(None),
142            active: AtomicBool::new(false),
143        }
144    }
145
146    /// Create with default configuration.
147    pub fn with_defaults() -> Self {
148        Self::new(BackpressureConfig::default())
149    }
150
151    /// Get the configuration.
152    pub fn config(&self) -> &BackpressureConfig {
153        &self.config
154    }
155
156    /// Get current concurrent operations.
157    pub fn current(&self) -> usize {
158        self.current.load(Ordering::Relaxed)
159    }
160
161    /// Get current state.
162    pub fn state(&self) -> BackpressureState {
163        if let Ok(state) = self.state.lock() {
164            *state
165        } else {
166            BackpressureState::Normal
167        }
168    }
169
170    /// Check if backpressure is active.
171    pub fn is_active(&self) -> bool {
172        self.active.load(Ordering::Relaxed)
173    }
174
175    /// Try to acquire a slot for a new operation.
176    ///
177    /// Returns true if the operation should proceed, false if it should be rejected.
178    pub fn try_acquire(&self) -> bool {
179        if !self.config.enabled {
180            self.current.fetch_add(1, Ordering::Relaxed);
181            self.total_processed.fetch_add(1, Ordering::Relaxed);
182            return true;
183        }
184
185        let high_threshold = self.config.high_threshold();
186
187        // Use a CAS loop to ensure we never exceed `max_concurrent` under contention.
188        loop {
189            let current = self.current.load(Ordering::Relaxed);
190
191            if current >= self.config.max_concurrent {
192                // At capacity: reject
193                self.total_rejected.fetch_add(1, Ordering::Relaxed);
194                return false;
195            }
196
197            let new = current + 1;
198            match self
199                .current
200                .compare_exchange_weak(current, new, Ordering::Relaxed, Ordering::Relaxed)
201            {
202                Ok(_) => {
203                    // Successfully acquired a slot.
204                    self.total_processed.fetch_add(1, Ordering::Relaxed);
205
206                    // Update state if needed, based on the pre-increment value `current`.
207                    if current >= high_threshold {
208                        self.set_state(BackpressureState::Critical);
209                        self.active.store(true, Ordering::Relaxed);
210                    } else if current >= self.config.low_threshold() {
211                        self.set_state(BackpressureState::Warning);
212                    }
213
214                    return true;
215                }
216                Err(_) => {
217                    // Another thread raced with us; retry with the updated value.
218                    continue;
219                }
220            }
221        }
222    }
223
224    /// Release a slot after operation completes.
225    pub fn release(&self) {
226        // Guard against underflow: an unpaired release at 0 must not wrap to
227        // usize::MAX, which would permanently reject all future acquisitions.
228        let prev = match self
229            .current
230            .try_update(Ordering::Relaxed, Ordering::Relaxed, |current| current.checked_sub(1))
231        {
232            Ok(prev) => prev,
233            Err(_) => {
234                tracing::warn!("BackpressureMonitor::release called with no outstanding acquisition; ignoring");
235                0
236            }
237        };
238        let low_threshold = self.config.low_threshold();
239
240        // Update state if needed
241        if prev <= low_threshold + 1 {
242            self.set_state(BackpressureState::Normal);
243            self.active.store(false, Ordering::Relaxed);
244        }
245    }
246
247    /// Set the state.
248    fn set_state(&self, new_state: BackpressureState) {
249        if let Ok(mut state) = self.state.lock()
250            && *state != new_state
251        {
252            *state = new_state;
253            if let Ok(mut last) = self.last_state_change.lock() {
254                *last = Some(Instant::now());
255            }
256        }
257    }
258
259    /// Get total processed operations.
260    pub fn total_processed(&self) -> u64 {
261        self.total_processed.load(Ordering::Relaxed)
262    }
263
264    /// Get total rejected operations.
265    pub fn total_rejected(&self) -> u64 {
266        self.total_rejected.load(Ordering::Relaxed)
267    }
268
269    /// Get rejection rate.
270    pub fn rejection_rate(&self) -> f64 {
271        let processed = self.total_processed.load(Ordering::Relaxed);
272        let rejected = self.total_rejected.load(Ordering::Relaxed);
273        let total = processed + rejected;
274        if total == 0 { 0.0 } else { rejected as f64 / total as f64 }
275    }
276
277    /// Check if we should apply backpressure based on cooldown.
278    pub fn should_apply_backpressure(&self) -> bool {
279        if !self.config.enabled {
280            return false;
281        }
282
283        let current = self.current.load(Ordering::Relaxed);
284        if current < self.config.high_threshold() {
285            return false;
286        }
287
288        // Check cooldown
289        if let Ok(last) = self.last_state_change.lock()
290            && let Some(last_time) = *last
291            && last_time.elapsed() < self.config.cooldown
292        {
293            return false;
294        }
295
296        true
297    }
298
299    /// Reset statistics.
300    pub fn reset_stats(&self) {
301        self.total_processed.store(0, Ordering::Relaxed);
302        self.total_rejected.store(0, Ordering::Relaxed);
303    }
304}
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309
310    #[test]
311    fn test_backpressure_config() {
312        let config = BackpressureConfig::default();
313        assert!(config.validate().is_ok());
314        assert_eq!(config.high_threshold(), 25); // 32 * 0.8
315        assert_eq!(config.low_threshold(), 16); // 32 * 0.5
316    }
317
318    #[test]
319    fn test_backpressure_config_validation() {
320        let config = BackpressureConfig {
321            max_concurrent: 0,
322            ..Default::default()
323        };
324        assert!(config.validate().is_err());
325
326        let config = BackpressureConfig {
327            high_water_mark: 0.3,
328            low_water_mark: 0.5,
329            ..Default::default()
330        };
331        assert!(config.validate().is_err());
332    }
333
334    #[test]
335    fn test_backpressure_monitor() {
336        let config = BackpressureConfig {
337            max_concurrent: 4,
338            high_water_mark: 0.75, // high threshold = 3
339            low_water_mark: 0.5,   // low threshold = 2
340            ..Default::default()
341        };
342        let monitor = BackpressureMonitor::new(config);
343
344        // Acquire slots - current = 1 after acquire
345        assert!(monitor.try_acquire());
346        // State is Normal (1 < low_threshold=2)
347
348        // current = 2 after acquire
349        assert!(monitor.try_acquire());
350        // State should be Warning (2 >= low_threshold=2)
351
352        // current = 3 after acquire
353        assert!(monitor.try_acquire());
354        // State should be Critical (3 >= high_threshold=3)
355
356        // current = 4 after acquire
357        assert!(monitor.try_acquire());
358        // At capacity now
359
360        assert!(!monitor.try_acquire()); // Should be rejected
361
362        // Release slots - current = 3 after release
363        monitor.release();
364        // State is still Critical (3 >= high_threshold=3)
365
366        // current = 2 after release
367        monitor.release();
368        // State should be Warning (2 >= low_threshold=2 but < high_threshold=3)
369
370        // current = 1 after release
371        monitor.release();
372        // State should be Normal (1 < low_threshold=2)
373        assert_eq!(monitor.state(), BackpressureState::Normal);
374    }
375
376    #[test]
377    fn test_release_underflow_stays_at_zero() {
378        // An unpaired release at current==0 must not wrap to usize::MAX,
379        // which would make try_acquire reject everything forever.
380        let monitor = BackpressureMonitor::with_defaults();
381
382        monitor.release();
383        assert_eq!(monitor.current(), 0);
384
385        // The monitor must still be usable.
386        assert!(monitor.try_acquire());
387        assert_eq!(monitor.current(), 1);
388        monitor.release();
389        assert_eq!(monitor.current(), 0);
390    }
391
392    #[test]
393    fn test_rejection_rate() {
394        let config = BackpressureConfig {
395            max_concurrent: 2,
396            ..Default::default()
397        };
398        let monitor = BackpressureMonitor::new(config);
399
400        assert!(monitor.try_acquire());
401        assert!(monitor.try_acquire());
402        assert!(!monitor.try_acquire()); // Rejected
403
404        assert!((monitor.rejection_rate() - 0.3333333333333333).abs() < 0.01);
405    }
406
407    #[test]
408    fn test_disabled_backpressure() {
409        let config = BackpressureConfig {
410            max_concurrent: 1,
411            enabled: false,
412            ..Default::default()
413        };
414        let monitor = BackpressureMonitor::new(config);
415
416        // Should always succeed when disabled
417        assert!(monitor.try_acquire());
418        assert!(monitor.try_acquire());
419        assert!(monitor.try_acquire());
420    }
421}