Skip to main content

crawlkit_engine/
resource_monitor.rs

1use std::sync::Arc;
2use std::time::{Duration, Instant};
3
4use parking_lot::RwLock;
5use serde::{Deserialize, Serialize};
6
7/// Resource limits for a crawl session.
8///
9/// Defines maximum thresholds for memory, CPU, disk, file descriptors,
10/// duration, and page count. When any limit is exceeded, the crawl
11/// should be terminated gracefully.
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct ResourceLimits {
14    /// Maximum memory usage in bytes.
15    pub max_memory_bytes: Option<u64>,
16    /// Maximum CPU time in seconds.
17    pub max_cpu_seconds: Option<u64>,
18    /// Maximum disk usage in bytes.
19    pub max_disk_bytes: Option<u64>,
20    /// Maximum number of open file descriptors.
21    pub max_open_files: Option<u32>,
22    /// Maximum crawl duration.
23    pub max_duration: Option<Duration>,
24    /// Maximum number of pages.
25    pub max_pages: Option<usize>,
26}
27
28impl Default for ResourceLimits {
29    fn default() -> Self {
30        Self {
31            max_memory_bytes: Some(512 * 1024 * 1024), // 500 MB
32            max_cpu_seconds: Some(3600),               // 1 hour
33            max_disk_bytes: Some(1024 * 1024 * 1024),  // 1 GB
34            max_open_files: Some(1024),
35            max_duration: Some(Duration::from_secs(3600)),
36            max_pages: Some(10000),
37        }
38    }
39}
40
41/// Current resource usage.
42///
43/// Snapshot of resource consumption at a point in time. Compared against
44/// [`ResourceLimits`] to determine if the crawl should be stopped.
45#[derive(Debug, Clone, Default)]
46pub struct ResourceUsage {
47    /// Current memory usage in bytes.
48    pub memory_bytes: u64,
49    /// CPU time used in seconds.
50    pub cpu_seconds: u64,
51    /// Disk usage in bytes.
52    pub disk_bytes: u64,
53    /// Number of open file descriptors.
54    pub open_files: u32,
55    /// Elapsed time since crawl start.
56    pub elapsed: Duration,
57    /// Number of pages processed.
58    pub pages_processed: usize,
59}
60
61/// Resource monitor for tracking and enforcing limits.
62///
63/// Thread-safe monitor that tracks resource usage and compares against
64/// configured limits. Use [`is_over_limit`](ResourceMonitor::is_over_limit)
65/// to check if the crawl should be terminated.
66///
67/// # Examples
68///
69/// ```rust
70/// use crawlkit_engine::{ResourceMonitor, ResourceLimits};
71///
72/// let limits = ResourceLimits { max_pages: Some(10), ..Default::default() };
73/// let monitor = ResourceMonitor::new(limits);
74/// monitor.record_page();
75/// assert!(!monitor.is_over_limit());
76/// ```
77pub struct ResourceMonitor {
78    limits: ResourceLimits,
79    usage: Arc<RwLock<ResourceUsage>>,
80    start_time: Instant,
81}
82
83impl ResourceMonitor {
84    /// Create a new resource monitor.
85    #[must_use]
86    pub fn new(limits: ResourceLimits) -> Self {
87        Self {
88            limits,
89            usage: Arc::new(RwLock::new(ResourceUsage::default())),
90            start_time: Instant::now(),
91        }
92    }
93
94    /// Create with default limits.
95    #[must_use]
96    pub fn with_default_limits() -> Self {
97        Self::new(ResourceLimits::default())
98    }
99
100    /// Update current usage.
101    pub fn update(&self, usage: ResourceUsage) {
102        *self.usage.write() = usage;
103    }
104
105    /// Record a page processed.
106    pub fn record_page(&self) {
107        self.usage.write().pages_processed += 1;
108    }
109
110    /// Get current usage.
111    #[must_use]
112    pub fn current_usage(&self) -> ResourceUsage {
113        let mut usage = self.usage.read().clone();
114        usage.elapsed = self.start_time.elapsed();
115        usage
116    }
117
118    /// Check if any limit is exceeded.
119    #[must_use]
120    pub fn is_over_limit(&self) -> bool {
121        let usage = self.current_usage();
122
123        if let Some(max_mem) = self.limits.max_memory_bytes {
124            if usage.memory_bytes > max_mem {
125                return true;
126            }
127        }
128
129        if let Some(max_cpu) = self.limits.max_cpu_seconds {
130            if usage.cpu_seconds > max_cpu {
131                return true;
132            }
133        }
134
135        if let Some(max_disk) = self.limits.max_disk_bytes {
136            if usage.disk_bytes > max_disk {
137                return true;
138            }
139        }
140
141        if let Some(max_files) = self.limits.max_open_files {
142            if usage.open_files > max_files {
143                return true;
144            }
145        }
146
147        if let Some(max_duration) = self.limits.max_duration {
148            if usage.elapsed > max_duration {
149                return true;
150            }
151        }
152
153        if let Some(max_pages) = self.limits.max_pages {
154            if usage.pages_processed > max_pages {
155                return true;
156            }
157        }
158
159        false
160    }
161
162    /// Get which limits are exceeded.
163    #[must_use]
164    pub fn exceeded_limits(&self) -> Vec<String> {
165        let usage = self.current_usage();
166        let mut exceeded = Vec::new();
167
168        if let Some(max_mem) = self.limits.max_memory_bytes {
169            if usage.memory_bytes > max_mem {
170                exceeded.push(format!(
171                    "Memory: {} / {} bytes",
172                    usage.memory_bytes, max_mem
173                ));
174            }
175        }
176
177        if let Some(max_cpu) = self.limits.max_cpu_seconds {
178            if usage.cpu_seconds > max_cpu {
179                exceeded.push(format!("CPU: {} / {} seconds", usage.cpu_seconds, max_cpu));
180            }
181        }
182
183        if let Some(max_pages) = self.limits.max_pages {
184            if usage.pages_processed > max_pages {
185                exceeded.push(format!("Pages: {} / {}", usage.pages_processed, max_pages));
186            }
187        }
188
189        if let Some(max_duration) = self.limits.max_duration {
190            if usage.elapsed > max_duration {
191                exceeded.push(format!(
192                    "Duration: {:?} / {:?}",
193                    usage.elapsed, max_duration
194                ));
195            }
196        }
197
198        exceeded
199    }
200}
201
202impl Default for ResourceMonitor {
203    fn default() -> Self {
204        Self::with_default_limits()
205    }
206}
207
208// ---------------------------------------------------------------------------
209// Tests
210// ---------------------------------------------------------------------------
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    #[test]
217    fn test_resource_monitor_default_limits() {
218        let monitor = ResourceMonitor::with_default_limits();
219        assert!(!monitor.is_over_limit());
220    }
221
222    #[test]
223    fn test_resource_monitor_page_limit() {
224        let limits = ResourceLimits {
225            max_pages: Some(2),
226            ..Default::default()
227        };
228        let monitor = ResourceMonitor::new(limits);
229
230        monitor.record_page();
231        assert!(!monitor.is_over_limit());
232
233        monitor.record_page();
234        assert!(!monitor.is_over_limit());
235
236        monitor.record_page();
237        assert!(monitor.is_over_limit());
238    }
239
240    #[test]
241    fn test_resource_monitor_exceeded_limits() {
242        let limits = ResourceLimits {
243            max_pages: Some(1),
244            ..Default::default()
245        };
246        let monitor = ResourceMonitor::new(limits);
247
248        monitor.record_page();
249        monitor.record_page();
250
251        let exceeded = monitor.exceeded_limits();
252        assert!(!exceeded.is_empty());
253        assert!(exceeded[0].contains("Pages"));
254    }
255}