1use std::sync::atomic::{AtomicI64, AtomicUsize, Ordering};
16use std::sync::{Arc, Mutex};
17use std::time::{Duration, Instant};
18
19use super::error::RuntimeError;
20use super::sync_lock;
21
22#[derive(Clone, Copy, Debug)]
24pub struct QuotaConfig {
25 pub cpu_budget: i64,
26 pub mem_limit: usize,
27 pub spawn_burst: i64,
28 pub spawn_per_sec: i64,
29 pub send_burst: i64,
30 pub send_per_sec: i64,
31}
32
33impl QuotaConfig {
34 pub fn permissive() -> Self {
36 Self {
37 cpu_budget: 1_000_000_000,
38 mem_limit: 64 * 1024 * 1024,
39 spawn_burst: 10_000,
40 spawn_per_sec: 10_000,
41 send_burst: 50_000,
42 send_per_sec: 50_000,
43 }
44 }
45
46 pub fn sandbox() -> Self {
49 Self {
50 cpu_budget: 50_000,
51 mem_limit: 16 * 1024 * 1024,
52 spawn_burst: 32,
53 spawn_per_sec: 32,
54 send_burst: 256,
55 send_per_sec: 64,
56 }
57 }
58}
59
60impl Default for QuotaConfig {
61 fn default() -> Self {
62 Self::permissive()
63 }
64}
65
66pub struct TokenBucket {
67 capacity: i64,
68 tokens: AtomicI64,
69 refill_per_sec: i64,
70 last_refill: Mutex<Instant>,
71}
72
73impl TokenBucket {
74 pub fn new(capacity: i64, refill_per_sec: i64) -> Self {
75 Self {
76 capacity,
77 tokens: AtomicI64::new(capacity),
78 refill_per_sec,
79 last_refill: Mutex::new(Instant::now()),
80 }
81 }
82
83 fn refill(&self) -> Result<(), RuntimeError> {
85 let mut last = sync_lock::lock(&self.last_refill, "TokenBucket::refill")?;
86 let elapsed = last.elapsed();
87 if elapsed >= Duration::from_millis(50) {
88 let add = (elapsed.as_secs_f64() * self.refill_per_sec as f64) as i64;
89 if add > 0 {
90 let cur = self.tokens.load(Ordering::Relaxed);
91 let new = (cur + add).min(self.capacity);
92 self.tokens.store(new, Ordering::Relaxed);
93 *last = Instant::now();
94 }
95 }
96 Ok(())
97 }
98
99 pub fn try_consume(&self) -> bool {
101 if self.refill().is_err() {
102 return false;
103 }
104 loop {
105 let cur = self.tokens.load(Ordering::Relaxed);
106 if cur <= 0 {
107 return false;
108 }
109 if self
110 .tokens
111 .compare_exchange_weak(cur, cur - 1, Ordering::Relaxed, Ordering::Relaxed)
112 .is_ok()
113 {
114 return true;
115 }
116 }
117 }
118
119 pub fn credit(&self, extra: i64) {
121 if extra <= 0 {
122 return;
123 }
124 self.tokens.fetch_add(extra, Ordering::Relaxed);
125 }
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub enum QuotaError {
130 CpuExhausted,
131 MemoryExhausted { requested: usize, limit: usize },
132 SpawnRateExceeded,
133 SendRateExceeded,
134}
135
136impl std::fmt::Display for QuotaError {
137 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138 match self {
139 QuotaError::CpuExhausted => f.write_str("flow CPU budget exhausted"),
140 QuotaError::MemoryExhausted { requested, limit } => {
141 write!(f, "flow memory exhausted (requested {requested}, limit {limit})")
142 }
143 QuotaError::SpawnRateExceeded => f.write_str("spawn rate exceeded"),
144 QuotaError::SendRateExceeded => f.write_str("send rate exceeded"),
145 }
146 }
147}
148
149impl std::error::Error for QuotaError {}
150
151pub struct FlowQuota {
152 cpu_budget: AtomicI64,
153 mem_limit: AtomicUsize,
154 mem_used: AtomicUsize,
155 spawn_bucket: TokenBucket,
156 send_bucket: TokenBucket,
157}
158
159impl FlowQuota {
160 pub fn from_config(cfg: QuotaConfig) -> Self {
161 Self::new(
162 cfg.cpu_budget,
163 cfg.mem_limit,
164 cfg.spawn_burst,
165 cfg.spawn_per_sec,
166 cfg.send_burst,
167 cfg.send_per_sec,
168 )
169 }
170
171 pub fn new(
172 cpu_budget: i64,
173 mem_limit: usize,
174 spawn_burst: i64,
175 spawn_per_sec: i64,
176 send_burst: i64,
177 send_per_sec: i64,
178 ) -> Self {
179 Self {
180 cpu_budget: AtomicI64::new(cpu_budget),
181 mem_limit: AtomicUsize::new(mem_limit),
182 mem_used: AtomicUsize::new(0),
183 spawn_bucket: TokenBucket::new(spawn_burst, spawn_per_sec),
184 send_bucket: TokenBucket::new(send_burst, send_per_sec),
185 }
186 }
187
188 pub fn remaining_cpu(&self) -> i64 {
189 self.cpu_budget.load(Ordering::Relaxed).max(0)
190 }
191
192 pub fn charge_cpu(&self, cost: i64) -> Result<(), QuotaError> {
195 let remaining = self.cpu_budget.fetch_sub(cost, Ordering::Relaxed) - cost;
196 if remaining < 0 {
197 self.cpu_budget.fetch_add(cost, Ordering::Relaxed);
198 return Err(QuotaError::CpuExhausted);
199 }
200 Ok(())
201 }
202
203 pub fn alloc(&self, bytes: usize) -> Result<(), QuotaError> {
204 loop {
205 let cur = self.mem_used.load(Ordering::Relaxed);
206 let next = cur.saturating_add(bytes);
207 let limit = self.mem_limit.load(Ordering::Relaxed);
208 if next > limit {
209 return Err(QuotaError::MemoryExhausted {
210 requested: bytes,
211 limit,
212 });
213 }
214 if self
215 .mem_used
216 .compare_exchange_weak(cur, next, Ordering::Relaxed, Ordering::Relaxed)
217 .is_ok()
218 {
219 return Ok(());
220 }
221 }
222 }
223
224 pub fn free(&self, bytes: usize) {
225 loop {
226 let cur = self.mem_used.load(Ordering::Relaxed);
227 let next = cur.saturating_sub(bytes);
228 if self
229 .mem_used
230 .compare_exchange_weak(cur, next, Ordering::Relaxed, Ordering::Relaxed)
231 .is_ok()
232 {
233 return;
234 }
235 }
236 }
237
238 pub fn check_spawn(&self) -> Result<(), QuotaError> {
239 if self.spawn_bucket.try_consume() {
240 Ok(())
241 } else {
242 Err(QuotaError::SpawnRateExceeded)
243 }
244 }
245
246 pub fn check_send(&self) -> Result<(), QuotaError> {
247 if self.send_bucket.try_consume() {
248 Ok(())
249 } else {
250 Err(QuotaError::SendRateExceeded)
251 }
252 }
253
254 pub fn top_up_cpu(&self, extra: i64) {
256 self.cpu_budget.fetch_add(extra, Ordering::Relaxed);
257 }
258
259 pub fn top_up_mem(&self, extra: usize) {
260 self.mem_limit.fetch_add(extra, Ordering::Relaxed);
261 }
262
263 pub fn top_up_send(&self, extra: i64) {
264 self.send_bucket.credit(extra);
265 }
266
267 pub fn mem_used(&self) -> usize {
268 self.mem_used.load(Ordering::Relaxed)
269 }
270}
271
272pub struct QuotaTable {
274 inner: Mutex<std::collections::HashMap<u64, Arc<FlowQuota>>>,
275}
276
277impl QuotaTable {
278 pub fn new() -> Self {
279 Self {
280 inner: Mutex::new(std::collections::HashMap::new()),
281 }
282 }
283
284 pub fn insert(&self, id: super::process::FlowId, quota: Arc<FlowQuota>) -> Result<(), RuntimeError> {
285 sync_lock::lock(&self.inner, "QuotaTable::insert")?.insert(id.as_u64(), quota);
286 Ok(())
287 }
288
289 pub fn get(&self, id: super::process::FlowId) -> Result<Option<Arc<FlowQuota>>, RuntimeError> {
290 Ok(sync_lock::lock(&self.inner, "QuotaTable::get")?
291 .get(&id.as_u64())
292 .cloned())
293 }
294
295 pub fn remove(&self, id: super::process::FlowId) -> Result<(), RuntimeError> {
296 sync_lock::lock(&self.inner, "QuotaTable::remove")?.remove(&id.as_u64());
297 Ok(())
298 }
299}
300
301impl Default for QuotaTable {
302 fn default() -> Self {
303 Self::new()
304 }
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310
311 #[test]
312 fn cpu_budget_fails_closed() {
313 let q = FlowQuota::new(10, 1024, 1, 1, 1, 1);
314 assert!(q.charge_cpu(7).is_ok());
315 assert!(q.charge_cpu(4).is_err());
316 assert!(q.charge_cpu(3).is_ok());
317 }
318
319 #[test]
320 fn memory_never_exceeds_limit() {
321 let q = FlowQuota::new(1000, 100, 1, 1, 1, 1);
322 assert!(q.alloc(60).is_ok());
323 assert!(q.alloc(60).is_err());
324 q.free(60);
325 assert!(q.alloc(60).is_ok());
326 }
327
328 #[test]
329 fn sandbox_is_tighter_than_permissive() {
330 let p = QuotaConfig::permissive();
331 let s = QuotaConfig::sandbox();
332 assert!(s.cpu_budget < p.cpu_budget);
333 assert!(s.mem_limit < p.mem_limit);
334 assert!(s.send_burst < p.send_burst);
335 assert_eq!(QuotaConfig::default().cpu_budget, p.cpu_budget);
336 }
337
338 #[test]
339 fn top_up_mem_raises_limit() {
340 let q = FlowQuota::new(1000, 100, 1, 1, 1, 1);
341 assert!(q.alloc(60).is_ok());
342 assert!(q.alloc(60).is_err());
343 q.top_up_mem(50);
344 assert!(q.alloc(60).is_ok());
345 }
346
347 #[test]
348 fn spawn_bucket_does_not_over_issue() {
349 let q = FlowQuota::new(1000, 100, 3, 0, 1, 1);
350 assert!(q.check_spawn().is_ok());
351 assert!(q.check_spawn().is_ok());
352 assert!(q.check_spawn().is_ok());
353 assert!(q.check_spawn().is_err());
354 }
355}