1use std::collections::{HashMap, VecDeque};
12
13#[derive(Debug, Clone, PartialEq, Eq, Hash)]
19pub struct NamespaceId(pub String);
20
21impl NamespaceId {
22 pub fn new(s: impl Into<String>) -> Self {
24 Self(s.into())
25 }
26
27 pub fn as_str(&self) -> &str {
29 &self.0
30 }
31}
32
33impl std::fmt::Display for NamespaceId {
34 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35 f.write_str(&self.0)
36 }
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub struct QuotaLimit {
46 pub max_bytes: u64,
48 pub max_objects: u64,
50 pub max_object_size: u64,
52}
53
54impl QuotaLimit {
55 pub fn bytes_only(max_bytes: u64) -> Self {
57 Self {
58 max_bytes,
59 max_objects: 0,
60 max_object_size: 0,
61 }
62 }
63
64 pub fn full(max_bytes: u64, max_objects: u64, max_object_size: u64) -> Self {
66 Self {
67 max_bytes,
68 max_objects,
69 max_object_size,
70 }
71 }
72}
73
74#[derive(Debug, Clone)]
80pub struct QuotaUsage {
81 pub namespace: NamespaceId,
83 pub bytes_used: u64,
85 pub objects_used: u64,
87 pub last_updated: u64,
89}
90
91#[derive(Debug, Clone, PartialEq)]
97pub enum EnforcementPolicy {
98 Reject,
100 Evict,
103 Warn,
105 Throttle { factor: f64 },
108}
109
110#[derive(Debug, Clone, PartialEq, Eq)]
116pub enum ViolationKind {
117 BytesExceeded,
119 ObjectCountExceeded,
121 ObjectSizeTooLarge,
123}
124
125#[derive(Debug, Clone)]
131pub struct QuotaViolation {
132 pub namespace: NamespaceId,
134 pub kind: ViolationKind,
136 pub current: u64,
138 pub limit: u64,
140 pub timestamp: u64,
142}
143
144#[derive(Debug, Clone, Copy)]
150pub struct UsageSample {
151 pub timestamp: u64,
153 pub bytes: u64,
155 pub objects: u64,
157}
158
159#[derive(Debug, Clone)]
165pub struct GrowthForecast {
166 pub namespace: NamespaceId,
168 pub predicted_bytes_in_30d: u64,
170 pub days_until_quota: Option<u64>,
173 pub growth_rate_bytes_per_day: f64,
175}
176
177#[derive(Debug, Clone)]
183pub struct EnforcerConfig {
184 pub default_policy: EnforcementPolicy,
186 pub history_window: usize,
188 pub max_namespaces: usize,
190}
191
192impl Default for EnforcerConfig {
193 fn default() -> Self {
194 Self {
195 default_policy: EnforcementPolicy::Reject,
196 history_window: 100,
197 max_namespaces: 1000,
198 }
199 }
200}
201
202pub struct SqeStorageQuotaEnforcer {
213 config: EnforcerConfig,
214 quotas: HashMap<NamespaceId, QuotaLimit>,
215 usage: HashMap<NamespaceId, QuotaUsage>,
216 history: HashMap<NamespaceId, VecDeque<UsageSample>>,
217 violations: Vec<QuotaViolation>,
218 policies: HashMap<NamespaceId, EnforcementPolicy>,
219}
220
221impl SqeStorageQuotaEnforcer {
222 pub fn new(config: EnforcerConfig) -> Self {
228 Self {
229 config,
230 quotas: HashMap::new(),
231 usage: HashMap::new(),
232 history: HashMap::new(),
233 violations: Vec::new(),
234 policies: HashMap::new(),
235 }
236 }
237
238 pub fn set_quota(&mut self, ns: NamespaceId, limit: QuotaLimit) {
244 self.quotas.insert(ns, limit);
245 }
246
247 pub fn remove_quota(&mut self, ns: &NamespaceId) -> bool {
249 self.quotas.remove(ns).is_some()
250 }
251
252 pub fn set_policy(&mut self, ns: NamespaceId, policy: EnforcementPolicy) {
254 self.policies.insert(ns, policy);
255 }
256
257 pub fn check_write(&self, ns: &NamespaceId, object_size: u64) -> Result<(), QuotaViolation> {
268 let limit = match self.quotas.get(ns) {
269 Some(l) => l,
270 None => return Ok(()),
271 };
272
273 let usage = self.usage.get(ns);
274 let bytes_used = usage.map(|u| u.bytes_used).unwrap_or(0);
275 let objects_used = usage.map(|u| u.objects_used).unwrap_or(0);
276
277 let policy = self.policies.get(ns).unwrap_or(&self.config.default_policy);
278
279 if limit.max_object_size > 0 && object_size > limit.max_object_size {
281 let v = QuotaViolation {
282 namespace: ns.clone(),
283 kind: ViolationKind::ObjectSizeTooLarge,
284 current: object_size,
285 limit: limit.max_object_size,
286 timestamp: 0,
287 };
288 return Self::apply_policy(policy, v);
289 }
290
291 if limit.max_bytes > 0 {
293 let projected = bytes_used.saturating_add(object_size);
294 if projected > limit.max_bytes {
295 let v = QuotaViolation {
296 namespace: ns.clone(),
297 kind: ViolationKind::BytesExceeded,
298 current: projected,
299 limit: limit.max_bytes,
300 timestamp: 0,
301 };
302 return Self::apply_policy(policy, v);
303 }
304 }
305
306 if limit.max_objects > 0 {
308 let projected_objects = objects_used.saturating_add(1);
309 if projected_objects > limit.max_objects {
310 let v = QuotaViolation {
311 namespace: ns.clone(),
312 kind: ViolationKind::ObjectCountExceeded,
313 current: projected_objects,
314 limit: limit.max_objects,
315 timestamp: 0,
316 };
317 return Self::apply_policy(policy, v);
318 }
319 }
320
321 Ok(())
322 }
323
324 fn apply_policy(
329 policy: &EnforcementPolicy,
330 violation: QuotaViolation,
331 ) -> Result<(), QuotaViolation> {
332 match policy {
333 EnforcementPolicy::Reject => Err(violation),
334 EnforcementPolicy::Warn
335 | EnforcementPolicy::Throttle { .. }
336 | EnforcementPolicy::Evict => Ok(()),
337 }
338 }
339
340 pub fn record_write(&mut self, ns: &NamespaceId, object_size: u64, now: u64) {
350 if !self.usage.contains_key(ns) {
351 if self.usage.len() >= self.config.max_namespaces {
352 return;
353 }
354 self.usage.insert(
355 ns.clone(),
356 QuotaUsage {
357 namespace: ns.clone(),
358 bytes_used: 0,
359 objects_used: 0,
360 last_updated: now,
361 },
362 );
363 }
364
365 if let Some(u) = self.usage.get_mut(ns) {
366 u.bytes_used = u.bytes_used.saturating_add(object_size);
367 u.objects_used = u.objects_used.saturating_add(1);
368 u.last_updated = now;
369 }
370
371 self.append_history_sample(ns, now);
372 }
373
374 pub fn record_delete(&mut self, ns: &NamespaceId, object_size: u64, now: u64) {
378 if let Some(u) = self.usage.get_mut(ns) {
379 u.bytes_used = u.bytes_used.saturating_sub(object_size);
380 u.objects_used = u.objects_used.saturating_sub(1);
381 u.last_updated = now;
382 let (bytes, objects) = (u.bytes_used, u.objects_used);
383 self.append_history_sample_values(ns, now, bytes, objects);
384 }
385 }
386
387 fn append_history_sample(&mut self, ns: &NamespaceId, now: u64) {
389 let (bytes, objects) = match self.usage.get(ns) {
390 Some(u) => (u.bytes_used, u.objects_used),
391 None => return,
392 };
393 self.append_history_sample_values(ns, now, bytes, objects);
394 }
395
396 fn append_history_sample_values(
398 &mut self,
399 ns: &NamespaceId,
400 now: u64,
401 bytes: u64,
402 objects: u64,
403 ) {
404 let window = self.config.history_window;
405 let deque = self.history.entry(ns.clone()).or_default();
406
407 deque.push_back(UsageSample {
408 timestamp: now,
409 bytes,
410 objects,
411 });
412
413 while deque.len() > window {
415 deque.pop_front();
416 }
417 }
418
419 pub fn usage_for(&self, ns: &NamespaceId) -> Option<&QuotaUsage> {
425 self.usage.get(ns)
426 }
427
428 pub fn usage_percent_bytes(&self, ns: &NamespaceId) -> Option<f64> {
431 let limit = self.quotas.get(ns)?;
432 if limit.max_bytes == 0 {
433 return None;
434 }
435 let bytes_used = self.usage.get(ns).map(|u| u.bytes_used).unwrap_or(0);
436 Some(bytes_used as f64 / limit.max_bytes as f64 * 100.0)
437 }
438
439 pub fn forecast(&self, ns: &NamespaceId, now: u64) -> Option<GrowthForecast> {
444 let samples = self.history.get(ns)?;
445 if samples.len() < 2 {
446 return None;
447 }
448
449 let slope = Self::linear_regression_slope(samples);
450 let growth_rate_bytes_per_day = slope * 86_400.0;
452
453 let thirty_days_seconds: f64 = 30.0 * 86_400.0;
454 let current_bytes = self.usage.get(ns).map(|u| u.bytes_used).unwrap_or(0);
455
456 let predicted_bytes_f64 = current_bytes as f64 + slope * thirty_days_seconds;
457 let predicted_bytes_in_30d = if predicted_bytes_f64 > 0.0 {
458 predicted_bytes_f64 as u64
459 } else {
460 0
461 };
462
463 let days_until_quota = if let Some(limit) = self.quotas.get(ns) {
464 if limit.max_bytes > 0 && growth_rate_bytes_per_day > 0.0 {
465 let remaining = limit.max_bytes as f64 - current_bytes as f64;
466 if remaining <= 0.0 {
467 Some(0u64)
469 } else {
470 let days = remaining / growth_rate_bytes_per_day;
471 Some(days.floor() as u64)
472 }
473 } else {
474 None
475 }
476 } else {
477 None
478 };
479
480 let _ = now;
483
484 Some(GrowthForecast {
485 namespace: ns.clone(),
486 predicted_bytes_in_30d,
487 days_until_quota,
488 growth_rate_bytes_per_day,
489 })
490 }
491
492 pub fn violations(&self) -> &[QuotaViolation] {
494 &self.violations
495 }
496
497 pub fn record_violation(&mut self, v: QuotaViolation) {
500 self.violations.push(v);
501 }
502
503 pub fn top_namespaces_by_usage(&self, n: usize) -> Vec<(NamespaceId, u64)> {
505 let mut entries: Vec<(NamespaceId, u64)> = self
506 .usage
507 .iter()
508 .map(|(ns, u)| (ns.clone(), u.bytes_used))
509 .collect();
510
511 entries.sort_by_key(|b| std::cmp::Reverse(b.1));
512 entries.truncate(n);
513 entries
514 }
515
516 fn linear_regression_slope(samples: &VecDeque<UsageSample>) -> f64 {
531 let n = samples.len() as f64;
532 if n < 2.0 {
533 return 0.0;
534 }
535
536 let mut sum_x: f64 = 0.0;
537 let mut sum_y: f64 = 0.0;
538 let mut sum_xy: f64 = 0.0;
539 let mut sum_xx: f64 = 0.0;
540
541 for s in samples {
542 let x = s.timestamp as f64;
543 let y = s.bytes as f64;
544 sum_x += x;
545 sum_y += y;
546 sum_xy += x * y;
547 sum_xx += x * x;
548 }
549
550 let denom = n * sum_xx - sum_x * sum_x;
551 if denom == 0.0 {
552 return 0.0;
553 }
554
555 (n * sum_xy - sum_x * sum_y) / denom
556 }
557}
558
559impl Default for SqeStorageQuotaEnforcer {
560 fn default() -> Self {
561 Self::new(EnforcerConfig::default())
562 }
563}
564
565#[cfg(test)]
570mod tests {
571 use super::*;
572
573 fn small_config() -> EnforcerConfig {
574 EnforcerConfig {
575 default_policy: EnforcementPolicy::Reject,
576 history_window: 10,
577 max_namespaces: 5,
578 }
579 }
580
581 fn ns(s: &str) -> NamespaceId {
582 NamespaceId::new(s)
583 }
584
585 #[test]
590 fn namespace_id_new_and_as_str() {
591 let id = NamespaceId::new("tenant-1");
592 assert_eq!(id.as_str(), "tenant-1");
593 }
594
595 #[test]
596 fn namespace_id_display() {
597 let id = NamespaceId::new("my-ns");
598 assert_eq!(format!("{id}"), "my-ns");
599 }
600
601 #[test]
602 fn namespace_id_equality() {
603 assert_eq!(ns("a"), ns("a"));
604 assert_ne!(ns("a"), ns("b"));
605 }
606
607 #[test]
612 fn quota_limit_bytes_only() {
613 let l = QuotaLimit::bytes_only(1000);
614 assert_eq!(l.max_bytes, 1000);
615 assert_eq!(l.max_objects, 0);
616 assert_eq!(l.max_object_size, 0);
617 }
618
619 #[test]
620 fn quota_limit_full() {
621 let l = QuotaLimit::full(2000, 10, 500);
622 assert_eq!(l.max_bytes, 2000);
623 assert_eq!(l.max_objects, 10);
624 assert_eq!(l.max_object_size, 500);
625 }
626
627 #[test]
632 fn set_and_remove_quota() {
633 let mut e = SqeStorageQuotaEnforcer::new(small_config());
634 e.set_quota(ns("ns1"), QuotaLimit::bytes_only(1000));
635 assert!(e.remove_quota(&ns("ns1")));
636 assert!(!e.remove_quota(&ns("ns1")));
637 }
638
639 #[test]
640 fn remove_nonexistent_quota_returns_false() {
641 let mut e = SqeStorageQuotaEnforcer::new(small_config());
642 assert!(!e.remove_quota(&ns("ghost")));
643 }
644
645 #[test]
650 fn check_write_no_quota_always_ok() {
651 let e = SqeStorageQuotaEnforcer::new(small_config());
652 assert!(e.check_write(&ns("free"), 999_999).is_ok());
653 }
654
655 #[test]
660 fn check_write_object_size_too_large_reject() {
661 let mut e = SqeStorageQuotaEnforcer::new(small_config());
662 e.set_quota(ns("ns"), QuotaLimit::full(10_000, 0, 100));
663 let result = e.check_write(&ns("ns"), 101);
664 assert!(result.is_err());
665 let v = result.expect_err("should be violation");
666 assert_eq!(v.kind, ViolationKind::ObjectSizeTooLarge);
667 assert_eq!(v.current, 101);
668 assert_eq!(v.limit, 100);
669 }
670
671 #[test]
672 fn check_write_object_size_exactly_at_limit_ok() {
673 let mut e = SqeStorageQuotaEnforcer::new(small_config());
674 e.set_quota(ns("ns"), QuotaLimit::full(10_000, 0, 100));
675 assert!(e.check_write(&ns("ns"), 100).is_ok());
676 }
677
678 #[test]
679 fn check_write_object_size_zero_limit_means_unlimited() {
680 let mut e = SqeStorageQuotaEnforcer::new(small_config());
681 e.set_quota(ns("ns"), QuotaLimit::full(0, 0, 0));
683 assert!(e.check_write(&ns("ns"), 999_999).is_ok());
684 }
685
686 #[test]
691 fn check_write_bytes_exceeded_reject() {
692 let mut e = SqeStorageQuotaEnforcer::new(small_config());
693 e.set_quota(ns("ns"), QuotaLimit::bytes_only(500));
694 e.record_write(&ns("ns"), 400, 1000);
695 let result = e.check_write(&ns("ns"), 200);
696 assert!(result.is_err());
697 let v = result.expect_err("violation");
698 assert_eq!(v.kind, ViolationKind::BytesExceeded);
699 assert_eq!(v.limit, 500);
700 }
701
702 #[test]
703 fn check_write_bytes_exactly_at_limit_ok() {
704 let mut e = SqeStorageQuotaEnforcer::new(small_config());
705 e.set_quota(ns("ns"), QuotaLimit::bytes_only(500));
706 e.record_write(&ns("ns"), 400, 1000);
707 assert!(e.check_write(&ns("ns"), 100).is_ok());
709 }
710
711 #[test]
712 fn check_write_bytes_zero_limit_means_unlimited() {
713 let mut e = SqeStorageQuotaEnforcer::new(small_config());
714 e.set_quota(ns("ns"), QuotaLimit::full(0, 0, 0));
715 assert!(e.check_write(&ns("ns"), 999_999).is_ok());
716 }
717
718 #[test]
723 fn check_write_object_count_exceeded_reject() {
724 let mut e = SqeStorageQuotaEnforcer::new(small_config());
725 e.set_quota(ns("ns"), QuotaLimit::full(0, 3, 0));
726 e.record_write(&ns("ns"), 10, 1000);
727 e.record_write(&ns("ns"), 10, 1001);
728 e.record_write(&ns("ns"), 10, 1002);
729 let result = e.check_write(&ns("ns"), 10);
730 assert!(result.is_err());
731 let v = result.expect_err("violation");
732 assert_eq!(v.kind, ViolationKind::ObjectCountExceeded);
733 }
734
735 #[test]
736 fn check_write_object_count_at_limit_still_allows_final() {
737 let mut e = SqeStorageQuotaEnforcer::new(small_config());
738 e.set_quota(ns("ns"), QuotaLimit::full(0, 3, 0));
739 e.record_write(&ns("ns"), 10, 1000);
740 e.record_write(&ns("ns"), 10, 1001);
741 assert!(e.check_write(&ns("ns"), 10).is_ok());
743 }
744
745 #[test]
750 fn check_write_warn_policy_returns_ok() {
751 let mut e = SqeStorageQuotaEnforcer::new(small_config());
752 e.set_quota(ns("ns"), QuotaLimit::bytes_only(100));
753 e.set_policy(ns("ns"), EnforcementPolicy::Warn);
754 e.record_write(&ns("ns"), 80, 1000);
755 assert!(e.check_write(&ns("ns"), 50).is_ok());
757 }
758
759 #[test]
760 fn check_write_throttle_policy_returns_ok() {
761 let mut e = SqeStorageQuotaEnforcer::new(small_config());
762 e.set_quota(ns("ns"), QuotaLimit::bytes_only(100));
763 e.set_policy(ns("ns"), EnforcementPolicy::Throttle { factor: 0.8 });
764 e.record_write(&ns("ns"), 80, 1000);
765 assert!(e.check_write(&ns("ns"), 50).is_ok());
766 }
767
768 #[test]
769 fn check_write_evict_policy_returns_ok() {
770 let mut e = SqeStorageQuotaEnforcer::new(small_config());
771 e.set_quota(ns("ns"), QuotaLimit::bytes_only(100));
772 e.set_policy(ns("ns"), EnforcementPolicy::Evict);
773 e.record_write(&ns("ns"), 80, 1000);
774 assert!(e.check_write(&ns("ns"), 50).is_ok());
775 }
776
777 #[test]
778 fn check_write_reject_policy_returns_err() {
779 let mut e = SqeStorageQuotaEnforcer::new(small_config());
780 e.set_quota(ns("ns"), QuotaLimit::bytes_only(100));
781 e.set_policy(ns("ns"), EnforcementPolicy::Reject);
782 e.record_write(&ns("ns"), 80, 1000);
783 assert!(e.check_write(&ns("ns"), 50).is_err());
784 }
785
786 #[test]
791 fn record_write_increments_usage() {
792 let mut e = SqeStorageQuotaEnforcer::new(small_config());
793 e.record_write(&ns("ns"), 100, 1000);
794 let u = e.usage_for(&ns("ns")).expect("should exist");
795 assert_eq!(u.bytes_used, 100);
796 assert_eq!(u.objects_used, 1);
797 assert_eq!(u.last_updated, 1000);
798 }
799
800 #[test]
801 fn record_write_multiple_accumulates() {
802 let mut e = SqeStorageQuotaEnforcer::new(small_config());
803 e.record_write(&ns("ns"), 100, 1000);
804 e.record_write(&ns("ns"), 200, 2000);
805 let u = e.usage_for(&ns("ns")).expect("exists");
806 assert_eq!(u.bytes_used, 300);
807 assert_eq!(u.objects_used, 2);
808 }
809
810 #[test]
811 fn record_write_appends_history_sample() {
812 let mut e = SqeStorageQuotaEnforcer::new(small_config());
813 e.record_write(&ns("ns"), 100, 1000);
814 e.record_write(&ns("ns"), 50, 2000);
815 let history = e.history.get(&ns("ns")).expect("history should exist");
816 assert_eq!(history.len(), 2);
817 }
818
819 #[test]
820 fn record_write_truncates_history_to_window() {
821 let config = EnforcerConfig {
822 history_window: 3,
823 ..small_config()
824 };
825 let mut e = SqeStorageQuotaEnforcer::new(config);
826 for i in 0..6u64 {
827 e.record_write(&ns("ns"), 10, 1000 + i);
828 }
829 let history = e.history.get(&ns("ns")).expect("history should exist");
830 assert_eq!(history.len(), 3);
831 }
832
833 #[test]
834 fn record_write_respects_max_namespaces() {
835 let config = EnforcerConfig {
836 max_namespaces: 2,
837 ..small_config()
838 };
839 let mut e = SqeStorageQuotaEnforcer::new(config);
840 e.record_write(&ns("a"), 10, 1);
841 e.record_write(&ns("b"), 10, 2);
842 e.record_write(&ns("c"), 10, 3);
844 assert!(e.usage_for(&ns("c")).is_none());
845 }
846
847 #[test]
852 fn record_delete_decrements_usage() {
853 let mut e = SqeStorageQuotaEnforcer::new(small_config());
854 e.record_write(&ns("ns"), 300, 1000);
855 e.record_delete(&ns("ns"), 100, 2000);
856 let u = e.usage_for(&ns("ns")).expect("exists");
857 assert_eq!(u.bytes_used, 200);
858 assert_eq!(u.objects_used, 0);
859 assert_eq!(u.last_updated, 2000);
860 }
861
862 #[test]
863 fn record_delete_saturates_at_zero() {
864 let mut e = SqeStorageQuotaEnforcer::new(small_config());
865 e.record_write(&ns("ns"), 50, 1000);
866 e.record_delete(&ns("ns"), 9999, 2000);
867 let u = e.usage_for(&ns("ns")).expect("exists");
868 assert_eq!(u.bytes_used, 0);
869 assert_eq!(u.objects_used, 0);
870 }
871
872 #[test]
873 fn record_delete_unknown_namespace_noop() {
874 let mut e = SqeStorageQuotaEnforcer::new(small_config());
875 e.record_delete(&ns("ghost"), 100, 1000);
877 assert!(e.usage_for(&ns("ghost")).is_none());
878 }
879
880 #[test]
885 fn usage_percent_bytes_no_quota_returns_none() {
886 let mut e = SqeStorageQuotaEnforcer::new(small_config());
887 e.record_write(&ns("ns"), 100, 1);
888 assert!(e.usage_percent_bytes(&ns("ns")).is_none());
889 }
890
891 #[test]
892 fn usage_percent_bytes_zero_max_returns_none() {
893 let mut e = SqeStorageQuotaEnforcer::new(small_config());
894 e.set_quota(ns("ns"), QuotaLimit::full(0, 0, 0));
895 e.record_write(&ns("ns"), 100, 1);
896 assert!(e.usage_percent_bytes(&ns("ns")).is_none());
897 }
898
899 #[test]
900 fn usage_percent_bytes_half_quota() {
901 let mut e = SqeStorageQuotaEnforcer::new(small_config());
902 e.set_quota(ns("ns"), QuotaLimit::bytes_only(1000));
903 e.record_write(&ns("ns"), 500, 1);
904 let pct = e.usage_percent_bytes(&ns("ns")).expect("should be Some");
905 assert!((pct - 50.0).abs() < 1e-9, "expected 50%, got {pct}");
906 }
907
908 #[test]
909 fn usage_percent_bytes_full() {
910 let mut e = SqeStorageQuotaEnforcer::new(small_config());
911 e.set_quota(ns("ns"), QuotaLimit::bytes_only(1000));
912 e.record_write(&ns("ns"), 1000, 1);
913 let pct = e.usage_percent_bytes(&ns("ns")).expect("some");
914 assert!((pct - 100.0).abs() < 1e-9);
915 }
916
917 #[test]
922 fn forecast_requires_at_least_two_samples() {
923 let mut e = SqeStorageQuotaEnforcer::new(small_config());
924 e.record_write(&ns("ns"), 100, 1000);
925 assert!(e.forecast(&ns("ns"), 2000).is_none());
926 }
927
928 #[test]
929 fn forecast_returns_some_with_two_samples() {
930 let mut e = SqeStorageQuotaEnforcer::new(small_config());
931 e.record_write(&ns("ns"), 100, 0);
932 e.record_write(&ns("ns"), 200, 86_400); let fc = e.forecast(&ns("ns"), 2 * 86_400).expect("should forecast");
934 assert!(
936 fc.growth_rate_bytes_per_day > 0.0,
937 "positive growth expected"
938 );
939 }
940
941 #[test]
942 fn forecast_growth_rate_approximately_correct() {
943 let mut e = SqeStorageQuotaEnforcer::new(small_config());
945 let day = 86_400u64;
946 for i in 0..5u64 {
947 e.record_write(&ns("ns"), 100, i * day);
948 }
949 let fc = e.forecast(&ns("ns"), 5 * day).expect("forecast");
950 let rate = fc.growth_rate_bytes_per_day;
952 assert!(rate > 50.0 && rate < 200.0, "rate {rate} out of range");
953 }
954
955 #[test]
956 fn forecast_days_until_quota_set_correctly() {
957 let mut e = SqeStorageQuotaEnforcer::new(small_config());
958 let day = 86_400u64;
959 e.set_quota(ns("ns"), QuotaLimit::bytes_only(1_000));
960 e.record_write(&ns("ns"), 100, 0);
962 e.record_write(&ns("ns"), 100, day);
963 e.record_write(&ns("ns"), 100, 2 * day);
964 let fc = e.forecast(&ns("ns"), 3 * day).expect("forecast");
965 if let Some(days) = fc.days_until_quota {
967 assert!(days > 0, "should be some positive number of days");
968 }
969 }
970
971 #[test]
972 fn forecast_days_until_quota_none_when_no_quota() {
973 let mut e = SqeStorageQuotaEnforcer::new(small_config());
974 let day = 86_400u64;
975 e.record_write(&ns("ns"), 100, 0);
976 e.record_write(&ns("ns"), 100, day);
977 let fc = e.forecast(&ns("ns"), 2 * day).expect("forecast");
978 assert!(fc.days_until_quota.is_none());
979 }
980
981 #[test]
982 fn forecast_days_until_quota_zero_when_already_over() {
983 let mut e = SqeStorageQuotaEnforcer::new(small_config());
984 let day = 86_400u64;
985 e.set_quota(ns("ns"), QuotaLimit::bytes_only(50));
986 e.record_write(&ns("ns"), 100, 0);
987 e.record_write(&ns("ns"), 100, day);
988 let fc = e.forecast(&ns("ns"), 2 * day).expect("forecast");
989 if let Some(days) = fc.days_until_quota {
990 assert_eq!(days, 0, "already over quota should yield 0 days");
991 }
992 }
993
994 #[test]
995 fn forecast_negative_growth_days_until_quota_none() {
996 let mut e = SqeStorageQuotaEnforcer::new(small_config());
997 let day = 86_400u64;
998 e.set_quota(ns("ns"), QuotaLimit::bytes_only(1000));
999 e.record_write(&ns("ns"), 500, 0);
1001 e.record_delete(&ns("ns"), 100, day);
1002 let fc = e.forecast(&ns("ns"), 2 * day).expect("forecast");
1003 assert!(fc.days_until_quota.is_none());
1005 }
1006
1007 #[test]
1012 fn violations_initially_empty() {
1013 let e = SqeStorageQuotaEnforcer::new(small_config());
1014 assert!(e.violations().is_empty());
1015 }
1016
1017 #[test]
1018 fn record_violation_appends() {
1019 let mut e = SqeStorageQuotaEnforcer::new(small_config());
1020 e.record_violation(QuotaViolation {
1021 namespace: ns("ns"),
1022 kind: ViolationKind::BytesExceeded,
1023 current: 600,
1024 limit: 500,
1025 timestamp: 9999,
1026 });
1027 assert_eq!(e.violations().len(), 1);
1028 assert_eq!(e.violations()[0].kind, ViolationKind::BytesExceeded);
1029 }
1030
1031 #[test]
1036 fn top_namespaces_sorted_by_bytes_desc() {
1037 let mut e = SqeStorageQuotaEnforcer::new(small_config());
1038 e.record_write(&ns("a"), 100, 1);
1039 e.record_write(&ns("b"), 300, 2);
1040 e.record_write(&ns("c"), 200, 3);
1041 let top = e.top_namespaces_by_usage(3);
1042 assert_eq!(top[0].0, ns("b"));
1043 assert_eq!(top[1].0, ns("c"));
1044 assert_eq!(top[2].0, ns("a"));
1045 }
1046
1047 #[test]
1048 fn top_namespaces_truncated_to_n() {
1049 let mut e = SqeStorageQuotaEnforcer::new(small_config());
1050 e.record_write(&ns("a"), 100, 1);
1051 e.record_write(&ns("b"), 300, 2);
1052 e.record_write(&ns("c"), 200, 3);
1053 let top = e.top_namespaces_by_usage(2);
1054 assert_eq!(top.len(), 2);
1055 }
1056
1057 #[test]
1058 fn top_namespaces_empty_when_no_usage() {
1059 let e = SqeStorageQuotaEnforcer::new(small_config());
1060 assert!(e.top_namespaces_by_usage(5).is_empty());
1061 }
1062
1063 #[test]
1064 fn top_namespaces_n_larger_than_count() {
1065 let mut e = SqeStorageQuotaEnforcer::new(small_config());
1066 e.record_write(&ns("a"), 100, 1);
1067 let top = e.top_namespaces_by_usage(100);
1068 assert_eq!(top.len(), 1);
1069 }
1070
1071 #[test]
1076 fn default_enforcer_initialises_correctly() {
1077 let e = SqeStorageQuotaEnforcer::default();
1078 assert!(e.violations().is_empty());
1079 assert!(e.usage_for(&ns("any")).is_none());
1080 }
1081
1082 #[test]
1087 fn enforcer_config_default_values() {
1088 let cfg = EnforcerConfig::default();
1089 assert_eq!(cfg.history_window, 100);
1090 assert_eq!(cfg.max_namespaces, 1000);
1091 assert_eq!(cfg.default_policy, EnforcementPolicy::Reject);
1092 }
1093
1094 #[test]
1099 fn full_write_lifecycle_check_then_record() {
1100 let mut e = SqeStorageQuotaEnforcer::new(small_config());
1101 e.set_quota(ns("ns"), QuotaLimit::bytes_only(1000));
1102 assert!(e.check_write(&ns("ns"), 400).is_ok());
1104 e.record_write(&ns("ns"), 400, 1000);
1106 assert!(e.check_write(&ns("ns"), 400).is_ok());
1108 e.record_write(&ns("ns"), 400, 2000);
1110 assert!(e.check_write(&ns("ns"), 300).is_err());
1112 }
1113
1114 #[test]
1115 fn set_policy_overrides_default() {
1116 let mut e = SqeStorageQuotaEnforcer::new(small_config()); e.set_quota(ns("ns"), QuotaLimit::bytes_only(100));
1118 e.set_policy(ns("ns"), EnforcementPolicy::Warn);
1119 e.record_write(&ns("ns"), 90, 1);
1120 assert!(e.check_write(&ns("ns"), 50).is_ok());
1122 }
1123
1124 #[test]
1125 fn violation_violation_kind_object_size_too_large_fields() {
1126 let mut e = SqeStorageQuotaEnforcer::new(small_config());
1127 e.set_quota(ns("ns"), QuotaLimit::full(10_000, 0, 50));
1128 let v = e.check_write(&ns("ns"), 51).expect_err("should err");
1129 assert_eq!(v.kind, ViolationKind::ObjectSizeTooLarge);
1130 assert_eq!(v.current, 51);
1131 assert_eq!(v.limit, 50);
1132 assert_eq!(v.namespace, ns("ns"));
1133 }
1134}