templar_proxy_oracle_kernel/proxy/circuit_breaker/
rule.rs1mod cumulative_change;
2mod monotonic_run;
3mod stepwise_change;
4mod windowed_change_delta;
5
6#[cfg(feature = "schemars")]
7use alloc::borrow::ToOwned;
8#[cfg(any(feature = "borsh", feature = "schemars"))]
9use alloc::string::ToString;
10#[cfg(feature = "schemars")]
11use alloc::{boxed::Box, vec};
12
13pub use cumulative_change::CumulativeChange;
14pub use monotonic_run::MonotonicRun;
15pub use stepwise_change::StepwiseChange;
16pub use windowed_change_delta::WindowedChangeDelta;
17
18use super::{Observation, RingBuffer};
19
20serialize! {
21 #[derive(Debug, Clone, PartialEq, Eq)]
22 pub enum CircuitBreaker {
23 StepwiseChange(StepwiseChange),
24 MonotonicRun(MonotonicRun),
25 WindowedChangeDelta(WindowedChangeDelta),
26 CumulativeChange(CumulativeChange),
27 }
28}
29
30impl CircuitBreaker {
31 fn rule(&self) -> &dyn CircuitBreakerRule {
32 match self {
33 Self::StepwiseChange(inner) => inner,
34 Self::MonotonicRun(inner) => inner,
35 Self::WindowedChangeDelta(inner) => inner,
36 Self::CumulativeChange(inner) => inner,
37 }
38 }
39}
40
41impl CircuitBreakerRule for CircuitBreaker {
42 fn should_trip(&self, history: &RingBuffer<Observation>) -> bool {
43 self.rule().should_trip(history)
44 }
45
46 fn is_valid_for(
47 &self,
48 sample_interval_ns: templar_primitives::Nanoseconds,
49 history_len: u32,
50 ) -> bool {
51 let valid_threshold = |threshold: templar_primitives::Decimal| {
52 threshold != templar_primitives::Decimal::ZERO
53 && threshold <= templar_primitives::Decimal::ONE
54 };
55 match self {
56 Self::StepwiseChange(rule) => {
57 history_len >= 2 && valid_threshold(rule.max_relative_change)
58 }
59 Self::MonotonicRun(rule) => {
60 rule.max_streak > 0
61 && rule.max_streak < history_len
62 && sample_interval_ns == templar_primitives::Nanoseconds::zero()
63 && valid_threshold(rule.min_relative_step_change)
64 }
65 Self::WindowedChangeDelta(rule) => {
66 rule.window_len >= 2
67 && rule.lookback_windows > 0
68 && rule
69 .lookback_windows
70 .checked_add(1)
71 .and_then(|windows| rule.window_len.checked_mul(windows))
72 .is_some_and(|required| required <= history_len)
73 && valid_threshold(rule.max_relative_mean_change)
74 }
75 Self::CumulativeChange(rule) => {
76 history_len >= 1
77 && rule.baseline.has_strictly_positive_confidence_interval()
78 && valid_threshold(rule.max_relative_change)
79 }
80 }
81 }
82}
83
84pub trait CircuitBreakerRule {
90 fn should_trip(&self, history: &RingBuffer<Observation>) -> bool;
91
92 fn is_valid_for(
93 &self,
94 _sample_interval_ns: templar_primitives::Nanoseconds,
95 _history_len: u32,
96 ) -> bool {
97 true
98 }
99}