Skip to main content

ipfrs_storage/
storage_quota_enforcer.rs

1//! Storage Quota Enforcer — per-namespace quota enforcement with forecasting
2//!
3//! Provides:
4//! - Per-namespace storage quota limits (bytes, objects, max object size)
5//! - Configurable enforcement policies: Reject, Evict, Warn, Throttle
6//! - Usage tracking over time with sliding history window
7//! - Linear-regression growth forecasting (days until quota)
8//! - Violation log for audit and monitoring
9//! - Top-N namespace ranking by usage
10
11use std::collections::{HashMap, VecDeque};
12
13// ---------------------------------------------------------------------------
14// NamespaceId
15// ---------------------------------------------------------------------------
16
17/// Newtype wrapper for namespace identifiers.
18#[derive(Debug, Clone, PartialEq, Eq, Hash)]
19pub struct NamespaceId(pub String);
20
21impl NamespaceId {
22    /// Creates a new `NamespaceId` from any string-like value.
23    pub fn new(s: impl Into<String>) -> Self {
24        Self(s.into())
25    }
26
27    /// Returns the inner string slice.
28    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// ---------------------------------------------------------------------------
40// QuotaLimit
41// ---------------------------------------------------------------------------
42
43/// Quota limits for a namespace. A value of 0 means unlimited for that field.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub struct QuotaLimit {
46    /// Maximum total bytes. 0 = unlimited.
47    pub max_bytes: u64,
48    /// Maximum number of objects. 0 = unlimited.
49    pub max_objects: u64,
50    /// Maximum size of a single object. 0 = unlimited.
51    pub max_object_size: u64,
52}
53
54impl QuotaLimit {
55    /// Creates a quota with only a byte limit; objects and size are unlimited.
56    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    /// Creates a quota with bytes, objects, and max object size limits.
65    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// ---------------------------------------------------------------------------
75// QuotaUsage
76// ---------------------------------------------------------------------------
77
78/// Current usage snapshot for a single namespace.
79#[derive(Debug, Clone)]
80pub struct QuotaUsage {
81    /// Namespace this usage belongs to.
82    pub namespace: NamespaceId,
83    /// Total bytes currently stored.
84    pub bytes_used: u64,
85    /// Total number of objects currently stored.
86    pub objects_used: u64,
87    /// Unix timestamp (seconds) when this record was last updated.
88    pub last_updated: u64,
89}
90
91// ---------------------------------------------------------------------------
92// EnforcementPolicy
93// ---------------------------------------------------------------------------
94
95/// Defines how quota violations are handled.
96#[derive(Debug, Clone, PartialEq)]
97pub enum EnforcementPolicy {
98    /// Deny the write operation when the quota is exceeded.
99    Reject,
100    /// Evict least-recently-used data to make space; the caller is responsible
101    /// for performing the actual eviction.
102    Evict,
103    /// Allow the write but record the violation as a warning.
104    Warn,
105    /// Allow the write but signal the caller to throttle at the given factor
106    /// (0.0 = no throttle, 1.0 = full throttle / stop).
107    Throttle { factor: f64 },
108}
109
110// ---------------------------------------------------------------------------
111// ViolationKind
112// ---------------------------------------------------------------------------
113
114/// Describes why a quota violation occurred.
115#[derive(Debug, Clone, PartialEq, Eq)]
116pub enum ViolationKind {
117    /// The total stored bytes would exceed `max_bytes`.
118    BytesExceeded,
119    /// The total number of stored objects would exceed `max_objects`.
120    ObjectCountExceeded,
121    /// The size of the individual object exceeds `max_object_size`.
122    ObjectSizeTooLarge,
123}
124
125// ---------------------------------------------------------------------------
126// QuotaViolation (module-internal name; exported as SqeQuotaViolation)
127// ---------------------------------------------------------------------------
128
129/// Describes a single quota violation event.
130#[derive(Debug, Clone)]
131pub struct QuotaViolation {
132    /// Namespace in which the violation occurred.
133    pub namespace: NamespaceId,
134    /// What kind of limit was breached.
135    pub kind: ViolationKind,
136    /// The observed value that exceeded the limit.
137    pub current: u64,
138    /// The limit that was breached.
139    pub limit: u64,
140    /// Unix timestamp (seconds) when the violation was detected.
141    pub timestamp: u64,
142}
143
144// ---------------------------------------------------------------------------
145// UsageSample
146// ---------------------------------------------------------------------------
147
148/// A point-in-time usage measurement used for growth forecasting.
149#[derive(Debug, Clone, Copy)]
150pub struct UsageSample {
151    /// Unix timestamp (seconds) of the measurement.
152    pub timestamp: u64,
153    /// Bytes in use at this timestamp.
154    pub bytes: u64,
155    /// Objects in use at this timestamp.
156    pub objects: u64,
157}
158
159// ---------------------------------------------------------------------------
160// GrowthForecast
161// ---------------------------------------------------------------------------
162
163/// Growth forecast for a namespace derived from historical samples.
164#[derive(Debug, Clone)]
165pub struct GrowthForecast {
166    /// Namespace this forecast is for.
167    pub namespace: NamespaceId,
168    /// Predicted bytes in 30 days from `now`.
169    pub predicted_bytes_in_30d: u64,
170    /// Days until the namespace hits its quota, or `None` if no quota is set
171    /// or growth rate is non-positive (never fills up).
172    pub days_until_quota: Option<u64>,
173    /// Estimated bytes added per day (may be negative if shrinking).
174    pub growth_rate_bytes_per_day: f64,
175}
176
177// ---------------------------------------------------------------------------
178// EnforcerConfig
179// ---------------------------------------------------------------------------
180
181/// Configuration for [`SqeStorageQuotaEnforcer`].
182#[derive(Debug, Clone)]
183pub struct EnforcerConfig {
184    /// Default enforcement policy applied when no per-namespace policy is set.
185    pub default_policy: EnforcementPolicy,
186    /// Number of historical [`UsageSample`] entries kept per namespace.
187    pub history_window: usize,
188    /// Maximum number of namespaces the enforcer will track.
189    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
202// ---------------------------------------------------------------------------
203// SqeStorageQuotaEnforcer
204// ---------------------------------------------------------------------------
205
206/// Enforces per-namespace storage quotas with configurable policies and
207/// historical growth forecasting.
208///
209/// This is the main entry point for the storage quota enforcement subsystem.
210/// The type name is prefixed with `Sqe` to avoid collision with the existing
211/// `StorageQuotaEnforcer` in `quota_enforcer.rs`.
212pub 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    // -----------------------------------------------------------------------
223    // Construction
224    // -----------------------------------------------------------------------
225
226    /// Creates a new enforcer with the supplied configuration.
227    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    // -----------------------------------------------------------------------
239    // Quota management
240    // -----------------------------------------------------------------------
241
242    /// Registers or updates the quota limit for `ns`.
243    pub fn set_quota(&mut self, ns: NamespaceId, limit: QuotaLimit) {
244        self.quotas.insert(ns, limit);
245    }
246
247    /// Removes the quota for `ns`. Returns `true` if a quota existed.
248    pub fn remove_quota(&mut self, ns: &NamespaceId) -> bool {
249        self.quotas.remove(ns).is_some()
250    }
251
252    /// Overrides the enforcement policy for `ns`.
253    pub fn set_policy(&mut self, ns: NamespaceId, policy: EnforcementPolicy) {
254        self.policies.insert(ns, policy);
255    }
256
257    // -----------------------------------------------------------------------
258    // Write checking
259    // -----------------------------------------------------------------------
260
261    /// Checks whether a write of `object_size` bytes is permitted for `ns`.
262    ///
263    /// - If no quota is configured for `ns`, the write is always allowed.
264    /// - Violations are appended to the internal log regardless of policy.
265    /// - `Reject` → returns `Err(violation)`.
266    /// - `Warn` / `Throttle` / `Evict` → returns `Ok(())`.
267    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        // 1. Check per-object size limit.
280        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        // 2. Check total bytes limit.
292        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        // 3. Check total objects limit.
307        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    /// Applies the enforcement policy to a violation.
325    ///
326    /// - `Reject` → `Err(violation)`
327    /// - anything else → `Ok(())` (caller may inspect `violations()` separately)
328    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    // -----------------------------------------------------------------------
341    // Usage recording
342    // -----------------------------------------------------------------------
343
344    /// Records a successful write of `object_size` bytes at time `now` (Unix
345    /// seconds) for namespace `ns`, then appends a history sample.
346    ///
347    /// If `ns` has never been seen before and `max_namespaces` would be
348    /// exceeded, the write is silently ignored to avoid unbounded growth.
349    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    /// Records a deletion of `object_size` bytes at time `now` (Unix seconds).
375    ///
376    /// Usage floors at zero; an unknown namespace is silently ignored.
377    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    /// Appends a history sample using the current stored usage values.
388    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    /// Appends a history sample with explicit byte and object counts.
397    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        // Truncate to window size from the front (oldest entries removed).
414        while deque.len() > window {
415            deque.pop_front();
416        }
417    }
418
419    // -----------------------------------------------------------------------
420    // Queries
421    // -----------------------------------------------------------------------
422
423    /// Returns the current usage for `ns`, or `None` if unknown.
424    pub fn usage_for(&self, ns: &NamespaceId) -> Option<&QuotaUsage> {
425        self.usage.get(ns)
426    }
427
428    /// Returns the percentage of the byte quota consumed as a value in
429    /// `[0.0, ∞)`, or `None` if no quota is set or `max_bytes` is 0.
430    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    /// Forecasts the byte growth for `ns` over the next 30 days using linear
440    /// regression of the history window.
441    ///
442    /// Returns `None` if fewer than 2 history samples are available.
443    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        // slope is bytes-per-second; convert to bytes-per-day.
451        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                    // Already over quota.
468                    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        // `now` is accepted for API completeness; the forecast uses the samples
481        // themselves to derive the slope so `now` is not strictly needed here.
482        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    /// Returns all recorded violations in insertion order.
493    pub fn violations(&self) -> &[QuotaViolation] {
494        &self.violations
495    }
496
497    /// Records a violation into the internal log (useful for callers using
498    /// non-`Reject` policies who still want audit history).
499    pub fn record_violation(&mut self, v: QuotaViolation) {
500        self.violations.push(v);
501    }
502
503    /// Returns the top `n` namespaces sorted by bytes used (descending).
504    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    // -----------------------------------------------------------------------
517    // Internal helpers
518    // -----------------------------------------------------------------------
519
520    /// Computes the ordinary least-squares slope (bytes per second) for the
521    /// samples using the formula:
522    ///
523    /// ```text
524    /// slope = (n·Σxy − Σx·Σy) / (n·Σx² − (Σx)²)
525    /// ```
526    ///
527    /// where `x = timestamp` and `y = bytes`.
528    ///
529    /// Returns `0.0` if the denominator is zero (constant x values).
530    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// ---------------------------------------------------------------------------
566// Tests
567// ---------------------------------------------------------------------------
568
569#[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    // -----------------------------------------------------------------------
586    // NamespaceId
587    // -----------------------------------------------------------------------
588
589    #[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    // -----------------------------------------------------------------------
608    // QuotaLimit helpers
609    // -----------------------------------------------------------------------
610
611    #[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    // -----------------------------------------------------------------------
628    // set_quota / remove_quota
629    // -----------------------------------------------------------------------
630
631    #[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    // -----------------------------------------------------------------------
646    // check_write — no quota
647    // -----------------------------------------------------------------------
648
649    #[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    // -----------------------------------------------------------------------
656    // check_write — object size limit
657    // -----------------------------------------------------------------------
658
659    #[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        // max_bytes=0 (unlimited), max_objects=0 (unlimited), max_object_size=0 (unlimited).
682        e.set_quota(ns("ns"), QuotaLimit::full(0, 0, 0));
683        assert!(e.check_write(&ns("ns"), 999_999).is_ok());
684    }
685
686    // -----------------------------------------------------------------------
687    // check_write — bytes exceeded
688    // -----------------------------------------------------------------------
689
690    #[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        // 400 + 100 = 500, which is NOT > 500, so Ok.
708        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    // -----------------------------------------------------------------------
719    // check_write — object count exceeded
720    // -----------------------------------------------------------------------
721
722    #[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        // Third write should succeed (objects_used would become 3 which is NOT > 3).
742        assert!(e.check_write(&ns("ns"), 10).is_ok());
743    }
744
745    // -----------------------------------------------------------------------
746    // check_write — policy variants
747    // -----------------------------------------------------------------------
748
749    #[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        // Would exceed, but Warn returns Ok.
756        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    // -----------------------------------------------------------------------
787    // record_write
788    // -----------------------------------------------------------------------
789
790    #[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        // Third namespace should be silently ignored.
843        e.record_write(&ns("c"), 10, 3);
844        assert!(e.usage_for(&ns("c")).is_none());
845    }
846
847    // -----------------------------------------------------------------------
848    // record_delete
849    // -----------------------------------------------------------------------
850
851    #[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        // Should not panic.
876        e.record_delete(&ns("ghost"), 100, 1000);
877        assert!(e.usage_for(&ns("ghost")).is_none());
878    }
879
880    // -----------------------------------------------------------------------
881    // usage_percent_bytes
882    // -----------------------------------------------------------------------
883
884    #[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    // -----------------------------------------------------------------------
918    // forecast
919    // -----------------------------------------------------------------------
920
921    #[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); // 1 day later
933        let fc = e.forecast(&ns("ns"), 2 * 86_400).expect("should forecast");
934        // Growth rate ~ 100 bytes/day.
935        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        // Set up uniform daily samples: +100 bytes per day.
944        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        // 100 bytes per write per day, 4 intervals → slope ≈ 100 bytes/day.
951        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        // Simulate 3 days of 100 bytes/day growth.
961        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        // bytes_used = 300, max_bytes = 1000, growth ≈ 100/day → ~7 days remaining.
966        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        // Simulate shrinking: first record then delete.
1000        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        // Negative growth → never fills up.
1004        assert!(fc.days_until_quota.is_none());
1005    }
1006
1007    // -----------------------------------------------------------------------
1008    // violations
1009    // -----------------------------------------------------------------------
1010
1011    #[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    // -----------------------------------------------------------------------
1032    // top_namespaces_by_usage
1033    // -----------------------------------------------------------------------
1034
1035    #[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    // -----------------------------------------------------------------------
1072    // Default trait
1073    // -----------------------------------------------------------------------
1074
1075    #[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    // -----------------------------------------------------------------------
1083    // EnforcerConfig default
1084    // -----------------------------------------------------------------------
1085
1086    #[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    // -----------------------------------------------------------------------
1095    // Edge-case: check_write then record_write lifecycle
1096    // -----------------------------------------------------------------------
1097
1098    #[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        // Check passes.
1103        assert!(e.check_write(&ns("ns"), 400).is_ok());
1104        // Record the write.
1105        e.record_write(&ns("ns"), 400, 1000);
1106        // Check still passes.
1107        assert!(e.check_write(&ns("ns"), 400).is_ok());
1108        // Second record.
1109        e.record_write(&ns("ns"), 400, 2000);
1110        // Now 800 bytes used; adding 300 would put us at 1100 > 1000.
1111        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()); // default = Reject
1117        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        // Would exceed (90 + 50 > 100) but policy is Warn → Ok.
1121        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}