Skip to main content

runlimit_core/
observation.rs

1use std::{
2    panic::{AssertUnwindSafe, catch_unwind},
3    time::Duration,
4};
5
6use crate::{PolicyId, ScopeId};
7
8/// Receives synchronous, backend-neutral operational observations.
9///
10/// Implementations must return quickly and should hand expensive work to
11/// another thread. Backends invoke observers only after releasing internal
12/// locks and finalizing database transactions. A panic from an observer is
13/// caught and ignored so telemetry cannot change an admission result.
14///
15/// Observations deliberately contain no subject keys, policy fingerprints,
16/// backend error text, or other high-cardinality sensitive values.
17pub trait Observer: Send + Sync + 'static {
18    /// Records one operational observation.
19    fn observe(&self, observation: &Observation<'_>);
20}
21
22/// A backend-neutral operational observation.
23#[derive(Clone, Copy, Debug, Eq, PartialEq)]
24#[non_exhaustive]
25pub enum Observation<'a> {
26    /// One admission operation completed.
27    Admission(AdmissionObservation<'a>),
28    /// A bounded cleanup pass completed.
29    Cleanup(CleanupObservation),
30    /// A bounded store reported local capacity use.
31    Capacity(CapacityObservation),
32}
33
34/// Whether an admission evaluated one check or an atomic batch.
35#[derive(Clone, Copy, Debug, Eq, PartialEq)]
36#[non_exhaustive]
37pub enum AdmissionOperation {
38    /// One check.
39    Check,
40    /// An atomic batch.
41    Batch,
42}
43
44/// Classification of a completed admission operation.
45#[derive(Clone, Copy, Debug, Eq, PartialEq)]
46#[non_exhaustive]
47pub enum AdmissionOutcome {
48    /// Quota was consumed successfully.
49    Allowed,
50    /// Quota exhaustion was enforced.
51    QuotaDenied,
52    /// Quota exhaustion was observed but not enforced.
53    ShadowDenied,
54    /// A hard storage bound denied admission.
55    CapacityDenied,
56    /// The backend or input validation failed.
57    Failed,
58}
59
60/// What a caller can know about quota consumption after an operation.
61#[derive(Clone, Copy, Debug, Eq, PartialEq)]
62#[non_exhaustive]
63pub enum ConsumptionStatus {
64    /// The operation definitely consumed quota.
65    Consumed,
66    /// The operation definitely did not consume quota.
67    NotConsumed,
68    /// A failure occurred after the backend may have committed consumption.
69    PossiblyConsumed,
70}
71
72/// Metadata for one completed admission operation.
73#[derive(Clone, Copy, Debug, Eq, PartialEq)]
74pub struct AdmissionObservation<'a> {
75    operation: AdmissionOperation,
76    batch_size: usize,
77    policy_id: Option<&'a PolicyId>,
78    scope_id: Option<&'a ScopeId>,
79    outcome: AdmissionOutcome,
80    consumption: ConsumptionStatus,
81    elapsed: Duration,
82}
83
84impl<'a> AdmissionObservation<'a> {
85    /// Constructs admission metadata.
86    pub const fn new(
87        operation: AdmissionOperation,
88        batch_size: usize,
89        policy_id: Option<&'a PolicyId>,
90        scope_id: Option<&'a ScopeId>,
91        outcome: AdmissionOutcome,
92        consumption: ConsumptionStatus,
93        elapsed: Duration,
94    ) -> Self {
95        Self {
96            operation,
97            batch_size,
98            policy_id,
99            scope_id,
100            outcome,
101            consumption,
102            elapsed,
103        }
104    }
105
106    /// Returns the operation kind.
107    pub const fn operation(self) -> AdmissionOperation {
108        self.operation
109    }
110
111    /// Returns the number of checks submitted.
112    pub const fn batch_size(self) -> usize {
113        self.batch_size
114    }
115
116    /// Returns a relevant policy identifier for a single or failing check.
117    pub const fn policy_id(self) -> Option<&'a PolicyId> {
118        self.policy_id
119    }
120
121    /// Returns a relevant scope identifier for a single or failing check.
122    pub const fn scope_id(self) -> Option<&'a ScopeId> {
123        self.scope_id
124    }
125
126    /// Returns the admission outcome class.
127    pub const fn outcome(self) -> AdmissionOutcome {
128        self.outcome
129    }
130
131    /// Returns the quota-consumption certainty.
132    pub const fn consumption(self) -> ConsumptionStatus {
133        self.consumption
134    }
135
136    /// Returns wall-clock evaluation latency measured by the backend process.
137    pub const fn elapsed(self) -> Duration {
138        self.elapsed
139    }
140}
141
142/// Metadata for one bounded cleanup pass.
143#[derive(Clone, Copy, Debug, Eq, PartialEq)]
144pub struct CleanupObservation {
145    requested: usize,
146    removed: Option<u64>,
147    elapsed: Duration,
148    consumption: ConsumptionStatus,
149}
150
151impl CleanupObservation {
152    /// Constructs cleanup metadata.
153    ///
154    /// `removed` is `None` when a failed database operation has an unknown
155    /// commit result. `consumption` describes the certainty of that effect.
156    pub const fn new(
157        requested: usize,
158        removed: Option<u64>,
159        elapsed: Duration,
160        consumption: ConsumptionStatus,
161    ) -> Self {
162        Self {
163            requested,
164            removed,
165            elapsed,
166            consumption,
167        }
168    }
169
170    /// Returns the configured maximum work for this cleanup pass.
171    pub const fn requested(self) -> usize {
172        self.requested
173    }
174
175    /// Returns confirmed rows or entries removed, when known.
176    pub const fn removed(self) -> Option<u64> {
177        self.removed
178    }
179
180    /// Returns cleanup latency.
181    pub const fn elapsed(self) -> Duration {
182        self.elapsed
183    }
184
185    /// Returns certainty about the reported cleanup effect.
186    pub const fn consumption(self) -> ConsumptionStatus {
187        self.consumption
188    }
189}
190
191/// Local capacity use reported by a bounded backend.
192#[derive(Clone, Copy, Debug, Eq, PartialEq)]
193pub struct CapacityObservation {
194    used: u64,
195    capacity: u64,
196    shard_index: Option<usize>,
197}
198
199impl CapacityObservation {
200    /// Constructs capacity metadata.
201    pub const fn new(used: u64, capacity: u64, shard_index: Option<usize>) -> Self {
202        Self {
203            used,
204            capacity,
205            shard_index,
206        }
207    }
208
209    /// Returns occupied capacity units.
210    pub const fn used(self) -> u64 {
211        self.used
212    }
213
214    /// Returns the local hard capacity.
215    pub const fn capacity(self) -> u64 {
216        self.capacity
217    }
218
219    /// Returns remaining capacity.
220    pub const fn headroom(self) -> u64 {
221        self.capacity.saturating_sub(self.used)
222    }
223
224    /// Returns the backend-local shard index, when applicable.
225    pub const fn shard_index(self) -> Option<usize> {
226        self.shard_index
227    }
228}
229
230/// Invokes an observer while isolating callback panics.
231///
232/// Backend implementations use this after releasing locks and finalizing
233/// transactions. Applications normally call [`Observer::observe`] only in
234/// their observer implementation.
235#[doc(hidden)]
236pub fn observe_safely(observer: &dyn Observer, observation: &Observation<'_>) {
237    let _ = catch_unwind(AssertUnwindSafe(|| observer.observe(observation)));
238}