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