Skip to main content

crawlkit_engine/
backpressure.rs

1use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
2use std::sync::Arc;
3
4use tokio::sync::{mpsc, Semaphore};
5
6/// Backpressure controller for bounded pipeline channels.
7///
8/// Ensures producers block when consumers are overwhelmed,
9/// preventing memory explosion and maintaining system stability.
10/// Uses tokio semaphores for concurrency limiting.
11///
12/// # Examples
13///
14/// ```rust
15/// # async fn example() {
16/// use crawlkit_engine::BackpressureController;
17///
18/// let controller = BackpressureController::new(10);
19/// let permit = controller.acquire().await.unwrap();
20/// assert_eq!(controller.active_count(), 1);
21/// drop(permit);
22/// assert_eq!(controller.active_count(), 0);
23/// # }
24/// ```
25pub struct BackpressureController {
26    semaphore: Arc<Semaphore>,
27    #[allow(dead_code)]
28    bounded_tx: Option<mpsc::Sender<()>>,
29    active_tasks: Arc<AtomicUsize>,
30    shutdown: Arc<AtomicBool>,
31}
32
33impl BackpressureController {
34    /// Create a new backpressure controller.
35    ///
36    /// # Arguments
37    /// * `max_concurrent` - Maximum concurrent tasks allowed
38    #[must_use]
39    pub fn new(max_concurrent: usize) -> Self {
40        Self {
41            semaphore: Arc::new(Semaphore::new(max_concurrent)),
42            bounded_tx: None,
43            active_tasks: Arc::new(AtomicUsize::new(0)),
44            shutdown: Arc::new(AtomicBool::new(false)),
45        }
46    }
47
48    /// Create with bounded channel for additional backpressure.
49    #[must_use]
50    pub fn with_channel(max_concurrent: usize, channel_size: usize) -> Self {
51        let (tx, _rx) = mpsc::channel(channel_size);
52        Self {
53            semaphore: Arc::new(Semaphore::new(max_concurrent)),
54            bounded_tx: Some(tx),
55            active_tasks: Arc::new(AtomicUsize::new(0)),
56            shutdown: Arc::new(AtomicBool::new(false)),
57        }
58    }
59
60    /// Acquire a permit (blocks if at capacity).
61    ///
62    /// # Errors
63    /// Returns error if controller is shut down.
64    pub async fn acquire(&self) -> Result<BackpressurePermit<'_>, BackpressureError> {
65        if self.shutdown.load(Ordering::Acquire) {
66            return Err(BackpressureError::ShutDown);
67        }
68
69        let permit = self
70            .semaphore
71            .clone()
72            .acquire_owned()
73            .await
74            .map_err(|_| BackpressureError::ShutDown)?;
75
76        self.active_tasks.fetch_add(1, Ordering::AcqRel);
77
78        Ok(BackpressurePermit {
79            controller: self,
80            _permit: permit,
81        })
82    }
83
84    /// Try to acquire a permit without blocking.
85    #[must_use]
86    pub fn try_acquire(&self) -> Option<BackpressurePermit<'_>> {
87        if self.shutdown.load(Ordering::Acquire) {
88            return None;
89        }
90
91        let permit = self.semaphore.clone().try_acquire_owned().ok()?;
92
93        self.active_tasks.fetch_add(1, Ordering::AcqRel);
94
95        Some(BackpressurePermit {
96            controller: self,
97            _permit: permit,
98        })
99    }
100
101    /// Get number of active tasks.
102    #[must_use]
103    pub fn active_count(&self) -> usize {
104        self.active_tasks.load(Ordering::Acquire)
105    }
106
107    /// Check if at capacity.
108    #[must_use]
109    pub fn is_at_capacity(&self) -> bool {
110        self.active_tasks.load(Ordering::Acquire) >= self.semaphore.available_permits()
111    }
112
113    /// Initiate graceful shutdown.
114    pub fn shutdown(&self) {
115        self.shutdown.store(true, Ordering::Release);
116    }
117
118    /// Check if shut down.
119    #[must_use]
120    pub fn is_shut_down(&self) -> bool {
121        self.shutdown.load(Ordering::Acquire)
122    }
123}
124
125impl Default for BackpressureController {
126    fn default() -> Self {
127        Self::new(10)
128    }
129}
130
131/// RAII permit for backpressure.
132pub struct BackpressurePermit<'a> {
133    controller: &'a BackpressureController,
134    _permit: tokio::sync::OwnedSemaphorePermit,
135}
136
137impl Drop for BackpressurePermit<'_> {
138    fn drop(&mut self) {
139        self.controller.active_tasks.fetch_sub(1, Ordering::AcqRel);
140    }
141}
142
143/// Errors from backpressure operations.
144#[derive(Debug, thiserror::Error)]
145pub enum BackpressureError {
146    #[error("backpressure controller is shut down")]
147    ShutDown,
148}
149
150/// Bounded channel wrapper with backpressure.
151///
152/// Provides a typed mpsc channel with a fixed buffer size. Producers
153/// block when the buffer is full, naturally implementing backpressure.
154///
155/// # Examples
156///
157/// ```rust
158/// use crawlkit_engine::BoundedPipeline;
159///
160/// let mut pipeline = BoundedPipeline::<i32>::new(10);
161/// assert_eq!(pipeline.capacity(), 10);
162/// let tx = pipeline.sender();
163/// tx.try_send(42).unwrap();
164/// let mut rx = pipeline.receiver().unwrap();
165/// assert_eq!(rx.try_recv().unwrap(), 42);
166/// ```
167pub struct BoundedPipeline<T> {
168    tx: mpsc::Sender<T>,
169    rx: Option<mpsc::Receiver<T>>,
170    max_size: usize,
171}
172
173impl<T> BoundedPipeline<T> {
174    /// Create a new bounded pipeline.
175    #[must_use]
176    pub fn new(max_size: usize) -> Self {
177        let (tx, rx) = mpsc::channel(max_size);
178        Self {
179            tx,
180            rx: Some(rx),
181            max_size,
182        }
183    }
184
185    /// Get sender (can be cloned for multiple producers).
186    #[must_use]
187    pub fn sender(&self) -> mpsc::Sender<T> {
188        self.tx.clone()
189    }
190
191    /// Take receiver (can only be called once).
192    pub fn receiver(&mut self) -> Option<mpsc::Receiver<T>> {
193        self.rx.take()
194    }
195
196    /// Get channel capacity.
197    #[must_use]
198    pub fn capacity(&self) -> usize {
199        self.max_size
200    }
201
202    /// Check if channel is full.
203    #[must_use]
204    pub fn is_full(&self) -> bool {
205        self.tx.capacity() == 0
206    }
207}
208
209// ---------------------------------------------------------------------------
210// Tests
211// ---------------------------------------------------------------------------
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216
217    #[tokio::test]
218    async fn test_backpressure_controller_acquire() {
219        let controller = BackpressureController::new(2);
220
221        let permit1 = controller.acquire().await.unwrap();
222        assert_eq!(controller.active_count(), 1);
223
224        let permit2 = controller.acquire().await.unwrap();
225        assert_eq!(controller.active_count(), 2);
226
227        drop(permit1);
228        assert_eq!(controller.active_count(), 1);
229
230        drop(permit2);
231        assert_eq!(controller.active_count(), 0);
232    }
233
234    #[tokio::test]
235    async fn test_backpressure_controller_shutdown() {
236        let controller = BackpressureController::new(1);
237        let _permit = controller.acquire().await.unwrap();
238
239        controller.shutdown();
240        assert!(controller.is_shut_down());
241        assert!(controller.acquire().await.is_err());
242    }
243
244    #[test]
245    fn test_bounded_pipeline() {
246        let mut pipeline = BoundedPipeline::<i32>::new(10);
247        assert_eq!(pipeline.capacity(), 10);
248
249        let tx = pipeline.sender();
250        assert!(tx.try_send(42).is_ok());
251
252        let mut rx = pipeline.receiver().unwrap();
253        assert_eq!(rx.try_recv().unwrap(), 42);
254    }
255}