1use std::collections::HashMap;
7use std::sync::atomic::{AtomicU64, Ordering};
8use std::sync::{Arc, RwLock};
9
10use thiserror::Error;
11use tracing::warn;
12
13#[derive(Debug, Error)]
19pub enum QuotaError {
20 #[error("quota exceeded for namespace '{namespace}': used {used} bytes, limit {limit} bytes")]
22 QuotaExceeded {
23 namespace: String,
24 used: u64,
25 limit: u64,
26 },
27
28 #[error("namespace not found: '{0}'")]
30 NamespaceNotFound(String),
31
32 #[error("invalid limit: limit must be greater than 0")]
34 InvalidLimit,
35}
36
37#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum QuotaPolicy {
44 HardLimit,
46 SoftLimit,
48 NoLimit,
50}
51
52impl QuotaPolicy {
53 pub fn as_str(&self) -> &'static str {
55 match self {
56 QuotaPolicy::HardLimit => "hard",
57 QuotaPolicy::SoftLimit => "soft",
58 QuotaPolicy::NoLimit => "none",
59 }
60 }
61}
62
63pub struct NamespaceQuota {
69 pub namespace: String,
71 pub limit_bytes: u64,
73 pub used_bytes: Arc<AtomicU64>,
75 pub policy: QuotaPolicy,
77 pub block_count: Arc<AtomicU64>,
79}
80
81impl NamespaceQuota {
82 pub fn new(namespace: String, limit_bytes: u64, policy: QuotaPolicy) -> Self {
84 Self {
85 namespace,
86 limit_bytes,
87 used_bytes: Arc::new(AtomicU64::new(0)),
88 policy,
89 block_count: Arc::new(AtomicU64::new(0)),
90 }
91 }
92
93 pub fn available_bytes(&self) -> u64 {
97 self.limit_bytes
98 .saturating_sub(self.used_bytes.load(Ordering::Relaxed))
99 }
100
101 pub fn usage_ratio(&self) -> f64 {
105 if self.limit_bytes == 0 {
106 return 0.0;
107 }
108 self.used_bytes.load(Ordering::Relaxed) as f64 / self.limit_bytes as f64
109 }
110
111 pub fn is_over_soft_threshold(&self, threshold: f64) -> bool {
113 self.usage_ratio() > threshold
114 }
115}
116
117#[derive(Debug, Clone)]
123pub struct QuotaStats {
124 pub namespace: String,
126 pub limit_bytes: u64,
128 pub used_bytes: u64,
130 pub available_bytes: u64,
132 pub block_count: u64,
134 pub usage_pct: f64,
136 pub policy: String,
138}
139
140impl QuotaStats {
141 fn from_quota(q: &NamespaceQuota) -> Self {
142 let used = q.used_bytes.load(Ordering::Relaxed);
143 let limit = q.limit_bytes;
144 let available = limit.saturating_sub(used);
145 let usage_pct = if limit == 0 {
146 0.0
147 } else {
148 used as f64 / limit as f64 * 100.0
149 };
150 Self {
151 namespace: q.namespace.clone(),
152 limit_bytes: limit,
153 used_bytes: used,
154 available_bytes: available,
155 block_count: q.block_count.load(Ordering::Relaxed),
156 usage_pct,
157 policy: q.policy.as_str().to_owned(),
158 }
159 }
160}
161
162pub struct StorageQuotaManager {
180 quotas: RwLock<HashMap<String, NamespaceQuota>>,
181 soft_threshold: f64,
182}
183
184impl StorageQuotaManager {
185 pub fn new(soft_threshold: f64) -> Self {
191 Self {
192 quotas: RwLock::new(HashMap::new()),
193 soft_threshold,
194 }
195 }
196
197 pub fn register_namespace(
202 &self,
203 namespace: String,
204 limit_bytes: u64,
205 policy: QuotaPolicy,
206 ) -> Result<(), QuotaError> {
207 if limit_bytes == 0 && policy != QuotaPolicy::NoLimit {
208 return Err(QuotaError::InvalidLimit);
209 }
210 let quota = NamespaceQuota::new(namespace.clone(), limit_bytes, policy);
211 let mut map = self.quotas.write().unwrap_or_else(|e| e.into_inner());
212 map.insert(namespace, quota);
213 Ok(())
214 }
215
216 pub fn check_write(&self, namespace: &str, size_bytes: u64) -> Result<(), QuotaError> {
226 let map = self.quotas.read().unwrap_or_else(|e| e.into_inner());
227 let quota = map
228 .get(namespace)
229 .ok_or_else(|| QuotaError::NamespaceNotFound(namespace.to_owned()))?;
230
231 match quota.policy {
232 QuotaPolicy::NoLimit => Ok(()),
233 QuotaPolicy::HardLimit => {
234 let used = quota.used_bytes.load(Ordering::Relaxed);
235 let projected = used.saturating_add(size_bytes);
236 if projected > quota.limit_bytes {
237 Err(QuotaError::QuotaExceeded {
238 namespace: namespace.to_owned(),
239 used,
240 limit: quota.limit_bytes,
241 })
242 } else {
243 Ok(())
244 }
245 }
246 QuotaPolicy::SoftLimit => {
247 let used = quota.used_bytes.load(Ordering::Relaxed);
248 let projected = used.saturating_add(size_bytes);
249 if projected > quota.limit_bytes {
250 warn!(
251 namespace = %namespace,
252 used = used,
253 size = size_bytes,
254 limit = quota.limit_bytes,
255 "soft quota exceeded — write allowed but usage is over limit"
256 );
257 }
258 Ok(())
259 }
260 }
261 }
262
263 pub fn record_write(&self, namespace: &str, size_bytes: u64) -> Result<(), QuotaError> {
267 let map = self.quotas.read().unwrap_or_else(|e| e.into_inner());
268 let quota = map
269 .get(namespace)
270 .ok_or_else(|| QuotaError::NamespaceNotFound(namespace.to_owned()))?;
271 quota.used_bytes.fetch_add(size_bytes, Ordering::Relaxed);
272 quota.block_count.fetch_add(1, Ordering::Relaxed);
273 Ok(())
274 }
275
276 pub fn record_delete(&self, namespace: &str, size_bytes: u64) {
281 let map = self.quotas.read().unwrap_or_else(|e| e.into_inner());
282 if let Some(quota) = map.get(namespace) {
283 let prev =
285 quota
286 .used_bytes
287 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
288 Some(current.saturating_sub(size_bytes))
289 });
290 let _ = prev;
292
293 let _ =
295 quota
296 .block_count
297 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
298 Some(current.saturating_sub(1))
299 });
300 }
301 }
302
303 pub fn stats(&self, namespace: &str) -> Result<QuotaStats, QuotaError> {
305 let map = self.quotas.read().unwrap_or_else(|e| e.into_inner());
306 map.get(namespace)
307 .map(QuotaStats::from_quota)
308 .ok_or_else(|| QuotaError::NamespaceNotFound(namespace.to_owned()))
309 }
310
311 pub fn all_stats(&self) -> Vec<QuotaStats> {
313 let map = self.quotas.read().unwrap_or_else(|e| e.into_inner());
314 let mut stats: Vec<QuotaStats> = map.values().map(QuotaStats::from_quota).collect();
315 stats.sort_by(|a, b| a.namespace.cmp(&b.namespace));
316 stats
317 }
318
319 pub fn namespaces_over_threshold(&self) -> Vec<String> {
321 let map = self.quotas.read().unwrap_or_else(|e| e.into_inner());
322 let mut over: Vec<String> = map
323 .values()
324 .filter(|q| q.is_over_soft_threshold(self.soft_threshold))
325 .map(|q| q.namespace.clone())
326 .collect();
327 over.sort();
328 over
329 }
330
331 pub fn reset_namespace(&self, namespace: &str) -> Result<(), QuotaError> {
333 let map = self.quotas.read().unwrap_or_else(|e| e.into_inner());
334 let quota = map
335 .get(namespace)
336 .ok_or_else(|| QuotaError::NamespaceNotFound(namespace.to_owned()))?;
337 quota.used_bytes.store(0, Ordering::Relaxed);
338 quota.block_count.store(0, Ordering::Relaxed);
339 Ok(())
340 }
341
342 pub fn remove_namespace(&self, namespace: &str) -> bool {
346 let mut map = self.quotas.write().unwrap_or_else(|e| e.into_inner());
347 map.remove(namespace).is_some()
348 }
349}
350
351#[cfg(test)]
356mod tests {
357 use super::*;
358
359 fn make_mgr() -> StorageQuotaManager {
360 StorageQuotaManager::new(0.8)
361 }
362
363 #[test]
365 fn test_new_custom_soft_threshold() {
366 let mgr = StorageQuotaManager::new(0.5);
367 assert_eq!(mgr.soft_threshold, 0.5);
368 }
369
370 #[test]
372 fn test_register_hard_limit() {
373 let mgr = make_mgr();
374 mgr.register_namespace("ns".to_owned(), 1024, QuotaPolicy::HardLimit)
375 .expect("should register");
376 let stats = mgr.stats("ns").expect("should have stats");
377 assert_eq!(stats.limit_bytes, 1024);
378 assert_eq!(stats.policy, "hard");
379 }
380
381 #[test]
383 fn test_register_soft_limit() {
384 let mgr = make_mgr();
385 mgr.register_namespace("ns".to_owned(), 2048, QuotaPolicy::SoftLimit)
386 .expect("should register");
387 let stats = mgr.stats("ns").expect("should have stats");
388 assert_eq!(stats.policy, "soft");
389 }
390
391 #[test]
393 fn test_register_no_limit() {
394 let mgr = make_mgr();
395 mgr.register_namespace("ns".to_owned(), 0, QuotaPolicy::NoLimit)
396 .expect("NoLimit with 0 bytes should succeed");
397 let stats = mgr.stats("ns").expect("should have stats");
398 assert_eq!(stats.policy, "none");
399 }
400
401 #[test]
403 fn test_check_write_under_limit_ok() {
404 let mgr = make_mgr();
405 mgr.register_namespace("ns".to_owned(), 1000, QuotaPolicy::HardLimit)
406 .unwrap();
407 mgr.check_write("ns", 500).expect("should be under limit");
408 }
409
410 #[test]
412 fn test_check_write_over_hard_limit_err() {
413 let mgr = make_mgr();
414 mgr.register_namespace("ns".to_owned(), 100, QuotaPolicy::HardLimit)
415 .unwrap();
416 mgr.record_write("ns", 90).unwrap(); let err = mgr
418 .check_write("ns", 20)
419 .expect_err("should exceed hard limit");
420 matches!(err, QuotaError::QuotaExceeded { .. });
421 }
422
423 #[test]
425 fn test_check_write_over_soft_limit_ok() {
426 let mgr = make_mgr();
427 mgr.register_namespace("ns".to_owned(), 100, QuotaPolicy::SoftLimit)
428 .unwrap();
429 mgr.record_write("ns", 90).unwrap(); mgr.check_write("ns", 50)
431 .expect("SoftLimit should return Ok even over limit");
432 }
433
434 #[test]
436 fn test_check_write_no_limit_always_ok() {
437 let mgr = make_mgr();
438 mgr.register_namespace("ns".to_owned(), 0, QuotaPolicy::NoLimit)
439 .unwrap();
440 mgr.check_write("ns", u64::MAX)
441 .expect("NoLimit should always be Ok");
442 }
443
444 #[test]
446 fn test_check_write_unknown_namespace() {
447 let mgr = make_mgr();
448 let err = mgr
449 .check_write("missing", 100)
450 .expect_err("unknown namespace should error");
451 matches!(err, QuotaError::NamespaceNotFound(_));
452 }
453
454 #[test]
456 fn test_record_write_increments() {
457 let mgr = make_mgr();
458 mgr.register_namespace("ns".to_owned(), 10000, QuotaPolicy::HardLimit)
459 .unwrap();
460 mgr.record_write("ns", 300).unwrap();
461 mgr.record_write("ns", 200).unwrap();
462 let stats = mgr.stats("ns").unwrap();
463 assert_eq!(stats.used_bytes, 500);
464 assert_eq!(stats.block_count, 2);
465 }
466
467 #[test]
469 fn test_record_delete_decrements() {
470 let mgr = make_mgr();
471 mgr.register_namespace("ns".to_owned(), 10000, QuotaPolicy::HardLimit)
472 .unwrap();
473 mgr.record_write("ns", 1000).unwrap();
474 mgr.record_write("ns", 500).unwrap();
475 mgr.record_delete("ns", 300);
476 let stats = mgr.stats("ns").unwrap();
477 assert_eq!(stats.used_bytes, 1200);
478 assert_eq!(stats.block_count, 1);
479 }
480
481 #[test]
483 fn test_record_delete_underflow_safe() {
484 let mgr = make_mgr();
485 mgr.register_namespace("ns".to_owned(), 10000, QuotaPolicy::HardLimit)
486 .unwrap();
487 mgr.record_delete("ns", 999_999);
489 let stats = mgr.stats("ns").unwrap();
490 assert_eq!(stats.used_bytes, 0);
491 assert_eq!(stats.block_count, 0);
492 }
493
494 #[test]
496 fn test_stats_correct_snapshot() {
497 let mgr = make_mgr();
498 mgr.register_namespace("ns".to_owned(), 1000, QuotaPolicy::SoftLimit)
499 .unwrap();
500 mgr.record_write("ns", 400).unwrap();
501 let s = mgr.stats("ns").unwrap();
502 assert_eq!(s.namespace, "ns");
503 assert_eq!(s.limit_bytes, 1000);
504 assert_eq!(s.used_bytes, 400);
505 assert_eq!(s.available_bytes, 600);
506 assert_eq!(s.block_count, 1);
507 assert!((s.usage_pct - 40.0).abs() < 1e-9);
508 assert_eq!(s.policy, "soft");
509 }
510
511 #[test]
513 fn test_all_stats_sorted() {
514 let mgr = make_mgr();
515 mgr.register_namespace("zebra".to_owned(), 500, QuotaPolicy::HardLimit)
516 .unwrap();
517 mgr.register_namespace("alpha".to_owned(), 500, QuotaPolicy::HardLimit)
518 .unwrap();
519 mgr.register_namespace("mango".to_owned(), 500, QuotaPolicy::HardLimit)
520 .unwrap();
521 let all = mgr.all_stats();
522 assert_eq!(all.len(), 3);
523 assert_eq!(all[0].namespace, "alpha");
524 assert_eq!(all[1].namespace, "mango");
525 assert_eq!(all[2].namespace, "zebra");
526 }
527
528 #[test]
530 fn test_namespaces_over_threshold() {
531 let mgr = StorageQuotaManager::new(0.8);
532 mgr.register_namespace("low".to_owned(), 1000, QuotaPolicy::HardLimit)
533 .unwrap();
534 mgr.register_namespace("high".to_owned(), 1000, QuotaPolicy::SoftLimit)
535 .unwrap();
536 mgr.record_write("high", 900).unwrap();
538 mgr.record_write("low", 100).unwrap();
540 let over = mgr.namespaces_over_threshold();
541 assert_eq!(over, vec!["high".to_owned()]);
542 }
543
544 #[test]
546 fn test_reset_namespace() {
547 let mgr = make_mgr();
548 mgr.register_namespace("ns".to_owned(), 10000, QuotaPolicy::HardLimit)
549 .unwrap();
550 mgr.record_write("ns", 5000).unwrap();
551 mgr.reset_namespace("ns").unwrap();
552 let stats = mgr.stats("ns").unwrap();
553 assert_eq!(stats.used_bytes, 0);
554 assert_eq!(stats.block_count, 0);
555 }
556
557 #[test]
559 fn test_remove_namespace() {
560 let mgr = make_mgr();
561 mgr.register_namespace("ns".to_owned(), 1000, QuotaPolicy::HardLimit)
562 .unwrap();
563 assert!(mgr.remove_namespace("ns"));
564 assert!(!mgr.remove_namespace("ns")); let err = mgr.stats("ns").expect_err("should be gone");
566 matches!(err, QuotaError::NamespaceNotFound(_));
567 }
568
569 #[test]
571 fn test_invalid_limit_zero() {
572 let mgr = make_mgr();
573 let err = mgr
574 .register_namespace("ns".to_owned(), 0, QuotaPolicy::HardLimit)
575 .expect_err("limit=0 with HardLimit should fail");
576 matches!(err, QuotaError::InvalidLimit);
577 }
578
579 #[test]
581 fn test_invalid_limit_zero_soft() {
582 let mgr = make_mgr();
583 let err = mgr
584 .register_namespace("ns".to_owned(), 0, QuotaPolicy::SoftLimit)
585 .expect_err("limit=0 with SoftLimit should fail");
586 matches!(err, QuotaError::InvalidLimit);
587 }
588}