Skip to main content

communitas_core/
resource_limits.rs

1// Copyright (c) 2025 Saorsa Labs Limited
2//
3// Licensed under the AGPL-3.0 license
4
5//! Resource Limits and Management
6//!
7//! Implements MESH_CAPABILITIES.md Section 8.3: Resource management and limits
8//! to prevent OOM, connection exhaustion, and bandwidth saturation.
9
10use serde::{Deserialize, Serialize};
11use std::time::Duration;
12use thiserror::Error;
13
14/// Resource limit errors
15#[derive(Error, Debug)]
16pub enum ResourceLimitError {
17    #[error("Peer connection limit exceeded: {current}/{limit}")]
18    PeerLimitExceeded { current: usize, limit: usize },
19
20    #[error("Memory limit exceeded: {current}MB/{limit_mb}MB")]
21    MemoryLimitExceeded { current: usize, limit_mb: usize },
22
23    #[error("Document size too large: {size_mb}MB/{limit_mb}MB")]
24    DocumentTooLarge { size_mb: usize, limit_mb: usize },
25
26    #[error("Upload rate limit exceeded: {current:.2}Mbps/{max:.2}Mbps")]
27    UploadRateExceeded { current: f64, max: f64 },
28
29    #[error("Download rate limit exceeded: {current:.2}Mbps/{max:.2}Mbps")]
30    DownloadRateExceeded { current: f64, max: f64 },
31
32    #[error("Connection timeout: {0:?}")]
33    Timeout(Duration),
34}
35
36/// Result type for resource limit operations
37pub type ResourceLimitResult<T> = Result<T, ResourceLimitError>;
38
39/// Resource limits configuration (loadable from TOML)
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct ResourceLimitsConfig {
42    /// Maximum number of concurrent peer connections
43    pub max_peer_connections: usize,
44
45    /// Maximum number of relay connections
46    pub max_relay_connections: usize,
47
48    /// Maximum memory usage in megabytes
49    pub max_memory_mb: usize,
50
51    /// Maximum CRDT document size in megabytes
52    pub crdt_document_limit_mb: usize,
53
54    /// Connection timeout in seconds
55    pub connection_timeout_secs: u64,
56
57    /// Maximum anti-entropy sync interval in seconds
58    pub anti_entropy_max_interval_secs: u64,
59
60    /// Optional upload rate limit in Mbps
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub max_upload_rate_mbps: Option<u64>,
63
64    /// Optional download rate limit in Mbps
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub max_download_rate_mbps: Option<u64>,
67}
68
69impl Default for ResourceLimitsConfig {
70    fn default() -> Self {
71        Self {
72            max_peer_connections: 50,
73            max_relay_connections: 3,
74            max_memory_mb: 2048,
75            crdt_document_limit_mb: 50,
76            connection_timeout_secs: 30,
77            anti_entropy_max_interval_secs: 300,
78            max_upload_rate_mbps: None,
79            max_download_rate_mbps: None,
80        }
81    }
82}
83
84/// Current resource usage snapshot
85#[derive(Debug, Clone)]
86pub struct ResourceUsage {
87    pub peer_connections: usize,
88    pub memory_mb: usize,
89    pub upload_rate_mbps: f64,
90    pub download_rate_mbps: f64,
91}
92
93/// Resource limits manager
94///
95/// Enforces resource constraints to prevent:
96/// - Connection exhaustion
97/// - Out-of-memory conditions
98/// - Bandwidth saturation
99/// - Excessive sync intervals
100#[derive(Debug, Clone)]
101pub struct ResourceLimits {
102    pub max_peer_connections: usize,
103    pub max_relay_connections: usize,
104    pub max_memory_mb: usize,
105    pub crdt_document_limit_mb: usize,
106    pub connection_timeout: Duration,
107    pub anti_entropy_max_interval: Duration,
108    pub upload_rate_limit_mbps: Option<u64>,
109    pub download_rate_limit_mbps: Option<u64>,
110}
111
112impl Default for ResourceLimits {
113    fn default() -> Self {
114        Self::from_config(ResourceLimitsConfig::default())
115    }
116}
117
118impl ResourceLimits {
119    /// Create resource limits from configuration
120    pub fn from_config(config: ResourceLimitsConfig) -> Self {
121        Self {
122            max_peer_connections: config.max_peer_connections,
123            max_relay_connections: config.max_relay_connections,
124            max_memory_mb: config.max_memory_mb,
125            crdt_document_limit_mb: config.crdt_document_limit_mb,
126            connection_timeout: Duration::from_secs(config.connection_timeout_secs),
127            anti_entropy_max_interval: Duration::from_secs(config.anti_entropy_max_interval_secs),
128            upload_rate_limit_mbps: config.max_upload_rate_mbps,
129            download_rate_limit_mbps: config.max_download_rate_mbps,
130        }
131    }
132
133    /// Create low-resource preset for constrained devices
134    pub fn low_resource() -> Self {
135        Self::from_config(ResourceLimitsConfig {
136            max_peer_connections: 20,
137            max_relay_connections: 1,
138            max_memory_mb: 512,
139            crdt_document_limit_mb: 10,
140            connection_timeout_secs: 15,
141            anti_entropy_max_interval_secs: 600, // 10 min
142            max_upload_rate_mbps: Some(5),
143            max_download_rate_mbps: Some(20),
144        })
145    }
146
147    /// Create high-performance preset for powerful devices
148    pub fn high_performance() -> Self {
149        Self::from_config(ResourceLimitsConfig {
150            max_peer_connections: 200,
151            max_relay_connections: 10,
152            max_memory_mb: 8192,
153            crdt_document_limit_mb: 200,
154            connection_timeout_secs: 60,
155            anti_entropy_max_interval_secs: 60, // 1 min
156            max_upload_rate_mbps: None,
157            max_download_rate_mbps: None,
158        })
159    }
160
161    /// Enforce peer connection limit
162    pub fn enforce_peer_limit(&self, current: usize) -> ResourceLimitResult<()> {
163        if current >= self.max_peer_connections {
164            Err(ResourceLimitError::PeerLimitExceeded {
165                current,
166                limit: self.max_peer_connections,
167            })
168        } else {
169            Ok(())
170        }
171    }
172
173    /// Enforce memory limit
174    pub fn enforce_memory_limit(&self, current_mb: usize) -> ResourceLimitResult<()> {
175        if current_mb > self.max_memory_mb {
176            Err(ResourceLimitError::MemoryLimitExceeded {
177                current: current_mb,
178                limit_mb: self.max_memory_mb,
179            })
180        } else {
181            Ok(())
182        }
183    }
184
185    /// Enforce relay connection limit
186    pub fn enforce_relay_limit(&self, current: usize) -> ResourceLimitResult<()> {
187        if current >= self.max_relay_connections {
188            Err(ResourceLimitError::PeerLimitExceeded {
189                current,
190                limit: self.max_relay_connections,
191            })
192        } else {
193            Ok(())
194        }
195    }
196
197    /// Enforce CRDT document size limit
198    pub fn enforce_document_limit(&self, size_mb: usize) -> ResourceLimitResult<()> {
199        if size_mb > self.crdt_document_limit_mb {
200            Err(ResourceLimitError::DocumentTooLarge {
201                size_mb,
202                limit_mb: self.crdt_document_limit_mb,
203            })
204        } else {
205            Ok(())
206        }
207    }
208
209    /// Check memory usage against limit
210    pub fn check_memory_usage(&self, current_mb: usize) -> ResourceLimitResult<()> {
211        self.enforce_memory_limit(current_mb)
212    }
213
214    /// Enforce upload rate limit
215    pub fn enforce_upload_rate(&self, current_mbps: f64) -> ResourceLimitResult<()> {
216        if let Some(max) = self.upload_rate_limit_mbps {
217            let max_f64 = max as f64;
218            if current_mbps > max_f64 {
219                return Err(ResourceLimitError::UploadRateExceeded {
220                    current: current_mbps,
221                    max: max_f64,
222                });
223            }
224        }
225        Ok(())
226    }
227
228    /// Enforce download rate limit
229    pub fn enforce_download_rate(&self, current_mbps: f64) -> ResourceLimitResult<()> {
230        if let Some(max) = self.download_rate_limit_mbps {
231            let max_f64 = max as f64;
232            if current_mbps > max_f64 {
233                return Err(ResourceLimitError::DownloadRateExceeded {
234                    current: current_mbps,
235                    max: max_f64,
236                });
237            }
238        }
239        Ok(())
240    }
241
242    /// Convert upload rate limit to bytes per second
243    pub fn upload_rate_bytes_per_sec(&self) -> Option<u64> {
244        self.upload_rate_limit_mbps.map(|mbps| mbps * 125_000)
245    }
246
247    /// Convert download rate limit to bytes per second
248    pub fn download_rate_bytes_per_sec(&self) -> Option<u64> {
249        self.download_rate_limit_mbps.map(|mbps| mbps * 125_000)
250    }
251
252    /// Validate configuration for consistency
253    pub fn validate(&self) -> ResourceLimitResult<()> {
254        // Peer connections must be positive
255        if self.max_peer_connections == 0 {
256            return Err(ResourceLimitError::PeerLimitExceeded {
257                current: 0,
258                limit: 0,
259            });
260        }
261
262        // Memory must be positive
263        if self.max_memory_mb == 0 {
264            return Err(ResourceLimitError::MemoryLimitExceeded {
265                current: 0,
266                limit_mb: 0,
267            });
268        }
269
270        // Document limit should not exceed memory limit
271        if self.crdt_document_limit_mb > self.max_memory_mb {
272            return Err(ResourceLimitError::MemoryLimitExceeded {
273                current: self.crdt_document_limit_mb,
274                limit_mb: self.max_memory_mb,
275            });
276        }
277
278        Ok(())
279    }
280
281    /// Check all limits against current usage
282    pub fn check_all(&self, usage: &ResourceUsage) -> ResourceLimitResult<()> {
283        self.enforce_peer_limit(usage.peer_connections)?;
284        self.enforce_memory_limit(usage.memory_mb)?;
285        self.enforce_upload_rate(usage.upload_rate_mbps)?;
286        self.enforce_download_rate(usage.download_rate_mbps)?;
287        Ok(())
288    }
289
290    /// Get current resource usage from system
291    pub fn measure_current_usage(&self) -> ResourceUsage {
292        ResourceUsage {
293            peer_connections: 0,
294            memory_mb: 0,
295            upload_rate_mbps: 0.0,
296            download_rate_mbps: 0.0,
297        }
298    }
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304
305    #[test]
306    fn test_config_serialization() {
307        let config = ResourceLimitsConfig::default();
308        let toml = toml::to_string(&config).expect("Serialize");
309        let parsed: ResourceLimitsConfig = toml::from_str(&toml).expect("Deserialize");
310
311        assert_eq!(parsed.max_peer_connections, 50);
312        assert_eq!(parsed.max_memory_mb, 2048);
313    }
314
315    #[test]
316    fn test_zero_limits() {
317        let config = ResourceLimitsConfig {
318            max_peer_connections: 0,
319            max_relay_connections: 0,
320            max_memory_mb: 0,
321            crdt_document_limit_mb: 0,
322            connection_timeout_secs: 1,
323            anti_entropy_max_interval_secs: 1,
324            max_upload_rate_mbps: None,
325            max_download_rate_mbps: None,
326        };
327
328        let limits = ResourceLimits::from_config(config);
329
330        assert!(limits.enforce_peer_limit(0).is_err());
331        assert!(limits.enforce_memory_limit(1).is_err());
332    }
333}