1use alloc::collections::BTreeMap;
2use alloc::vec;
3use alloc::vec::Vec;
4
5#[cfg(any(feature = "borsh", feature = "schemars"))]
6use alloc::format;
7#[cfg(any(feature = "borsh", feature = "schemars"))]
8use alloc::string::ToString;
9#[cfg(feature = "schemars")]
10use alloc::{borrow::ToOwned, boxed::Box};
11use templar_primitives::Nanoseconds;
12
13use crate::{primitive::AccountId, Price};
14
15use super::{
16 CircuitBreaker, CircuitBreakerError, CircuitBreakerRule, CircuitBreakerStatus, Observation,
17 RingBuffer,
18};
19
20serialize! {
21 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
22 pub struct CircuitBreakerSetConfig {
24 pub sample_interval_ns: Nanoseconds,
29 pub history_len: u32,
35 }
36}
37
38serialize! {
39 #[derive(Debug, Clone, PartialEq, Eq)]
40 pub enum CircuitBreakerEvent {
41 ManualTripSet {
42 is_manually_tripped: bool,
43 actor: AccountId,
44 metadata: Option<Vec<u8>>,
45 },
46 ConfigSet {
47 config: CircuitBreakerSetConfig,
48 },
49 Added {
50 breaker_id: u32,
51 breaker: CircuitBreaker,
52 },
53 Removed {
54 breaker_id: u32,
55 },
56 EnforcementSet {
57 breaker_id: u32,
58 is_enforced: bool,
59 },
60 Rearmed {
61 breaker_id: u32,
62 armed_after_ns: Nanoseconds,
63 },
64 Tripped {
65 breaker_id: u32,
66 tripped_at_ns: Nanoseconds,
67 price_update: Observation,
68 is_enforced: bool,
69 },
70 }
71}
72
73serialize! {
74 #[derive(Debug, Clone, PartialEq, Eq)]
75 pub struct CircuitBreakerOutcome<T = ()> {
76 pub value: T,
77 pub events: Vec<CircuitBreakerEvent>,
78 }
79}
80
81impl<T> CircuitBreakerOutcome<T> {
82 #[must_use]
83 pub fn new(value: T) -> Self {
84 Self {
85 value,
86 events: Vec::new(),
87 }
88 }
89
90 #[must_use]
91 pub fn map<U>(self, f: impl FnOnce(T) -> U) -> CircuitBreakerOutcome<U> {
92 CircuitBreakerOutcome {
93 value: f(self.value),
94 events: self.events,
95 }
96 }
97
98 #[must_use]
99 pub fn with_events(self, events: Vec<CircuitBreakerEvent>) -> Self {
100 Self { events, ..self }
101 }
102}
103
104impl CircuitBreakerOutcome<()> {
105 #[must_use]
106 pub const fn empty() -> Self {
107 Self {
108 value: (),
109 events: Vec::new(),
110 }
111 }
112
113 #[must_use]
114 pub fn with_value<T>(self, value: T) -> CircuitBreakerOutcome<T> {
115 CircuitBreakerOutcome {
116 value,
117 events: self.events,
118 }
119 }
120}
121
122serialize! {
123 #[derive(Debug, Clone, PartialEq, Eq)]
124 pub enum PriceBlockedReason {
125 ManuallyTripped,
126 BreakerTripped { blocking_breaker_ids: Vec<u32> },
127 }
128}
129
130pub type PriceAcceptance = Result<Price, PriceBlockedReason>;
131
132serialize! {
133 #[derive(Debug, Clone, PartialEq, Eq)]
134 pub struct UncheckedCircuitBreakerSet<R = CircuitBreaker> {
135 pub sample_interval_ns: Nanoseconds,
136 pub accepted_history: RingBuffer<Observation>,
137 pub observed_history: RingBuffer<Observation>,
138 pub next_id: u32,
139 pub is_manually_tripped: bool,
140 pub breakers: BTreeMap<u32, CircuitBreakerState<R>>,
141 }
142}
143
144#[cfg_attr(
145 feature = "serde",
146 derive(::serde::Deserialize, ::serde::Serialize),
147 serde(
148 try_from = "UncheckedCircuitBreakerSet<R>",
149 into = "UncheckedCircuitBreakerSet<R>",
150 bound(
151 serialize = "R: Clone + ::serde::Serialize",
152 deserialize = "R: ::serde::Deserialize<'de>"
153 )
154 )
155)]
156#[cfg_attr(
157 feature = "schemars",
158 derive(::schemars::JsonSchema),
159 schemars(transparent)
160)]
161#[cfg_attr(
162 feature = "borsh",
163 derive(::borsh::BorshSerialize, ::borsh::BorshSchema)
164)]
165#[derive(Debug, Clone, PartialEq, Eq)]
166pub struct CircuitBreakerSet<R = CircuitBreaker>(UncheckedCircuitBreakerSet<R>);
167
168#[derive(Debug, Clone, Copy, PartialEq, Eq)]
169pub enum CircuitBreakerSetParseError {
170 BreakerIdOutOfRange,
171 HistoryCapacityMismatch,
172 InvalidConfiguration,
173}
174
175impl core::fmt::Display for CircuitBreakerSetParseError {
176 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
177 match self {
178 Self::BreakerIdOutOfRange => write!(f, "circuit breaker ID is out of range"),
179 Self::HistoryCapacityMismatch => {
180 write!(f, "circuit breaker histories have mismatched capacities")
181 }
182 Self::InvalidConfiguration => write!(f, "invalid circuit breaker configuration"),
183 }
184 }
185}
186
187fn validate_structure<R>(
188 value: &UncheckedCircuitBreakerSet<R>,
189) -> Result<(), CircuitBreakerSetParseError> {
190 if value
191 .breakers
192 .keys()
193 .next_back()
194 .is_some_and(|breaker_id| *breaker_id >= value.next_id)
195 {
196 return Err(CircuitBreakerSetParseError::BreakerIdOutOfRange);
197 }
198 if value.accepted_history.capacity() != value.observed_history.capacity() {
199 return Err(CircuitBreakerSetParseError::HistoryCapacityMismatch);
200 }
201 Ok(())
202}
203
204impl<R> TryFrom<UncheckedCircuitBreakerSet<R>> for CircuitBreakerSet<R> {
205 type Error = CircuitBreakerSetParseError;
206
207 fn try_from(value: UncheckedCircuitBreakerSet<R>) -> Result<Self, Self::Error> {
208 validate_structure(&value)?;
209 Ok(Self(value))
210 }
211}
212
213impl<R> From<CircuitBreakerSet<R>> for UncheckedCircuitBreakerSet<R> {
214 fn from(value: CircuitBreakerSet<R>) -> Self {
215 value.0
216 }
217}
218
219impl<R> CircuitBreakerSet<R> {
225 #[must_use]
226 pub fn empty() -> Self {
231 Self::new(CircuitBreakerSetConfig {
232 sample_interval_ns: Nanoseconds::zero(),
233 history_len: 0,
234 })
235 }
236
237 #[must_use]
238 pub fn new(config: CircuitBreakerSetConfig) -> Self {
239 Self(UncheckedCircuitBreakerSet {
240 sample_interval_ns: config.sample_interval_ns,
241 accepted_history: RingBuffer::new(config.history_len),
242 observed_history: RingBuffer::new(config.history_len),
243 next_id: 0,
244 is_manually_tripped: false,
245 breakers: BTreeMap::new(),
246 })
247 }
248
249 pub fn set_manual_trip(
250 &mut self,
251 is_manually_tripped: bool,
252 actor: AccountId,
253 metadata: Option<Vec<u8>>,
254 ) -> CircuitBreakerOutcome {
255 if self.0.is_manually_tripped == is_manually_tripped {
256 return CircuitBreakerOutcome::empty();
257 }
258
259 self.set_manual_trip_state(is_manually_tripped);
260 CircuitBreakerOutcome::empty().with_events(vec![CircuitBreakerEvent::ManualTripSet {
261 is_manually_tripped,
262 actor,
263 metadata,
264 }])
265 }
266
267 fn set_manual_trip_state(&mut self, is_manually_tripped: bool) {
268 self.0.is_manually_tripped = is_manually_tripped;
269 }
270
271 fn add_state(&mut self, breaker_id: u32, breaker: R) -> Result<(), CircuitBreakerError> {
272 if breaker_id != self.0.next_id {
273 return Err(CircuitBreakerError::UnexpectedBreakerId {
274 expected: self.0.next_id,
275 actual: breaker_id,
276 });
277 }
278
279 self.0.next_id = self
280 .0
281 .next_id
282 .checked_add(1)
283 .ok_or(CircuitBreakerError::TooManyBreakers)?;
284 self.0
285 .breakers
286 .insert(breaker_id, CircuitBreakerState::new(breaker));
287 Ok(())
288 }
289
290 pub fn remove(
291 &mut self,
292 breaker_id: u32,
293 ) -> Result<CircuitBreakerOutcome, CircuitBreakerError> {
294 self.remove_state(breaker_id)?;
295 Ok(CircuitBreakerOutcome::empty()
296 .with_events(vec![CircuitBreakerEvent::Removed { breaker_id }]))
297 }
298
299 fn remove_state(&mut self, breaker_id: u32) -> Result<(), CircuitBreakerError> {
300 self.0
301 .breakers
302 .remove(&breaker_id)
303 .ok_or(CircuitBreakerError::BreakerNotFound { breaker_id })?;
304 Ok(())
305 }
306
307 fn get_mut(
308 &mut self,
309 breaker_id: u32,
310 ) -> Result<&mut CircuitBreakerState<R>, CircuitBreakerError> {
311 self.0
312 .breakers
313 .get_mut(&breaker_id)
314 .ok_or(CircuitBreakerError::BreakerNotFound { breaker_id })
315 }
316
317 #[must_use]
318 pub fn sample_interval_ns(&self) -> Nanoseconds {
319 self.0.sample_interval_ns
320 }
321
322 #[must_use]
323 pub fn accepted_history(&self) -> &RingBuffer<Observation> {
324 &self.0.accepted_history
325 }
326
327 #[must_use]
328 pub fn observed_history(&self) -> &RingBuffer<Observation> {
329 &self.0.observed_history
330 }
331
332 #[must_use]
333 pub fn next_id(&self) -> u32 {
334 self.0.next_id
335 }
336
337 #[must_use]
338 pub fn is_manually_tripped(&self) -> bool {
339 self.0.is_manually_tripped
340 }
341
342 #[must_use]
343 pub fn breakers(&self) -> &BTreeMap<u32, CircuitBreakerState<R>> {
344 &self.0.breakers
345 }
346
347 #[must_use]
348 pub fn breaker_count(&self) -> usize {
349 self.0.breakers.len()
350 }
351
352 #[must_use]
353 pub fn is_blocking(&self) -> bool {
354 self.0.is_manually_tripped
355 || self
356 .0
357 .breakers
358 .values()
359 .any(CircuitBreakerState::is_blocking)
360 }
361
362 #[must_use]
363 pub fn blocking_reason(&self) -> Option<PriceBlockedReason> {
364 if self.0.is_manually_tripped {
365 return Some(PriceBlockedReason::ManuallyTripped);
366 }
367 let blocking_breaker_ids = self.blocking_breaker_ids();
368 (!blocking_breaker_ids.is_empty()).then_some(PriceBlockedReason::BreakerTripped {
369 blocking_breaker_ids,
370 })
371 }
372
373 fn should_persist_sample(&self, now: Nanoseconds) -> bool {
374 self.0
375 .observed_history
376 .last()
377 .is_none_or(|last| now.saturating_sub(last.observed_at_ns) >= self.0.sample_interval_ns)
378 }
379
380 fn blocking_breaker_ids(&self) -> Vec<u32> {
381 self.0
382 .breakers
383 .iter()
384 .filter_map(|(id, breaker)| breaker.is_blocking().then_some(*id))
385 .collect()
386 }
387}
388
389impl<R: CircuitBreakerRule> CircuitBreakerSet<R> {
390 pub fn validate(&self) -> Result<(), CircuitBreakerSetParseError> {
391 let config = CircuitBreakerSetConfig {
392 sample_interval_ns: self.0.sample_interval_ns,
393 history_len: self.0.accepted_history.capacity(),
394 };
395 if self.0.observed_history.capacity() != config.history_len
396 || self.0.breakers.values().any(|state| {
397 !state
398 .breaker
399 .is_valid_for(config.sample_interval_ns, config.history_len)
400 })
401 {
402 return Err(CircuitBreakerSetParseError::InvalidConfiguration);
403 }
404 Ok(())
405 }
406
407 pub fn set_config(
408 &mut self,
409 config: CircuitBreakerSetConfig,
410 ) -> Result<CircuitBreakerOutcome, CircuitBreakerError> {
411 let current = CircuitBreakerSetConfig {
412 sample_interval_ns: self.0.sample_interval_ns,
413 history_len: self.0.accepted_history.capacity(),
414 };
415 if self.0.breakers.values().any(|state| {
416 !state
417 .breaker
418 .is_valid_for(config.sample_interval_ns, config.history_len)
419 }) {
420 return Err(CircuitBreakerError::InvalidConfiguration);
421 }
422 if config == current {
423 return Ok(CircuitBreakerOutcome::empty());
424 }
425 self.0.sample_interval_ns = config.sample_interval_ns;
426
427 self.0.accepted_history.set_capacity(config.history_len);
428 self.0.observed_history.set_capacity(config.history_len);
429 Ok(CircuitBreakerOutcome::empty()
430 .with_events(vec![CircuitBreakerEvent::ConfigSet { config }]))
431 }
432}
433
434impl CircuitBreakerSet<CircuitBreaker> {
435 pub fn add(
436 &mut self,
437 breaker_id: u32,
438 breaker: CircuitBreaker,
439 ) -> Result<CircuitBreakerOutcome, CircuitBreakerError> {
440 let config = CircuitBreakerSetConfig {
441 sample_interval_ns: self.0.sample_interval_ns,
442 history_len: self.0.accepted_history.capacity(),
443 };
444 if !breaker.is_valid_for(config.sample_interval_ns, config.history_len) {
445 return Err(CircuitBreakerError::InvalidConfiguration);
446 }
447 self.add_state(breaker_id, breaker.clone())?;
448 Ok(
449 CircuitBreakerOutcome::empty().with_events(vec![CircuitBreakerEvent::Added {
450 breaker_id,
451 breaker,
452 }]),
453 )
454 }
455
456 pub fn set_enforced(
457 &mut self,
458 breaker_id: u32,
459 is_enforced: bool,
460 ) -> Result<CircuitBreakerOutcome, CircuitBreakerError> {
461 let breaker = self.get_mut(breaker_id)?;
462 if breaker.is_enforced == is_enforced {
463 return Ok(CircuitBreakerOutcome::empty());
464 }
465 breaker.is_enforced = is_enforced;
466 Ok(
467 CircuitBreakerOutcome::empty().with_events(vec![CircuitBreakerEvent::EnforcementSet {
468 breaker_id,
469 is_enforced,
470 }]),
471 )
472 }
473
474 pub fn rearm(
475 &mut self,
476 breaker_id: u32,
477 armed_after_ns: Nanoseconds,
478 ) -> Result<CircuitBreakerOutcome, CircuitBreakerError> {
479 let breaker = self.get_mut(breaker_id)?;
480 let status = CircuitBreakerStatus::ArmedAfter {
481 timestamp_ns: armed_after_ns,
482 };
483 if breaker.status == status {
484 return Ok(CircuitBreakerOutcome::empty());
485 }
486 breaker.status = status;
487 Ok(
488 CircuitBreakerOutcome::empty().with_events(vec![CircuitBreakerEvent::Rearmed {
489 breaker_id,
490 armed_after_ns,
491 }]),
492 )
493 }
494}
495
496#[cfg(feature = "borsh")]
497impl<R: ::borsh::BorshDeserialize> ::borsh::BorshDeserialize for CircuitBreakerSet<R> {
498 fn deserialize_reader<Reader: ::borsh::io::Read>(
499 reader: &mut Reader,
500 ) -> ::borsh::io::Result<Self> {
501 let unchecked =
502 <UncheckedCircuitBreakerSet<R> as ::borsh::BorshDeserialize>::deserialize_reader(
503 reader,
504 )?;
505 unchecked.try_into().map_err(|_| {
506 ::borsh::io::Error::new(
507 ::borsh::io::ErrorKind::InvalidData,
508 "could not parse circuit breaker set",
509 )
510 })
511 }
512}
513
514impl<R: CircuitBreakerRule> CircuitBreakerSet<R> {
515 pub fn try_accept_price(
516 &mut self,
517 price: Price,
518 now: Nanoseconds,
519 ) -> Result<CircuitBreakerOutcome<PriceAcceptance>, CircuitBreakerError> {
520 if !price.has_strictly_positive_confidence_interval() {
521 return Err(CircuitBreakerError::InvalidPrice);
522 }
523
524 let price_update = Observation {
525 price,
526 observed_at_ns: now,
527 };
528 let accepted_history_should_advance = self
529 .0
530 .accepted_history
531 .last()
532 .is_none_or(|last| price.publish_time_ns > last.price.publish_time_ns);
533 let should_persist_sample = self.should_persist_sample(now);
534 let observed_history_should_advance = self
535 .0
536 .observed_history
537 .last()
538 .is_none_or(|last| price.publish_time_ns > last.price.publish_time_ns);
539 if should_persist_sample && observed_history_should_advance {
540 self.0.observed_history.push(price_update);
541 }
542
543 if self.0.is_manually_tripped {
544 return Ok(CircuitBreakerOutcome::new(Err(
545 PriceBlockedReason::ManuallyTripped,
546 )));
547 }
548
549 let blocking_breaker_ids = self.blocking_breaker_ids();
550
551 if !blocking_breaker_ids.is_empty() {
554 return Ok(CircuitBreakerOutcome::new(Err(
555 PriceBlockedReason::BreakerTripped {
556 blocking_breaker_ids,
557 },
558 )));
559 }
560
561 let proposed_acceptance =
562 ProposedPriceAcceptance::new(&self.0.accepted_history, price_update);
563
564 let acceptance = self.apply_armed_breaker_transitions(&proposed_acceptance, now);
565
566 if acceptance.value.is_ok() && should_persist_sample && accepted_history_should_advance {
567 self.0.accepted_history = proposed_acceptance.accepted_history;
568 }
569 Ok(acceptance)
570 }
571
572 fn apply_armed_breaker_transitions(
573 &mut self,
574 proposed_acceptance: &ProposedPriceAcceptance,
575 now: Nanoseconds,
576 ) -> CircuitBreakerOutcome<PriceAcceptance> {
577 let mut events = vec![];
578 let mut blocking_breaker_ids = vec![];
579
580 for (breaker_id, breaker) in &mut self.0.breakers {
581 if let Some(event) =
582 breaker.apply_armed_transition(*breaker_id, proposed_acceptance, now)
583 {
584 events.push(event);
585 }
586 if breaker.is_blocking() {
587 blocking_breaker_ids.push(*breaker_id);
588 }
589 }
590
591 let value = if blocking_breaker_ids.is_empty() {
592 Ok(proposed_acceptance.price_update.price)
593 } else {
594 Err(PriceBlockedReason::BreakerTripped {
595 blocking_breaker_ids,
596 })
597 };
598 CircuitBreakerOutcome::new(value).with_events(events)
599 }
600}
601
602pub(crate) struct ProposedPriceAcceptance {
603 price_update: Observation,
604 accepted_history: RingBuffer<Observation>,
605}
606
607impl ProposedPriceAcceptance {
608 pub(crate) fn new(
609 accepted_history: &RingBuffer<Observation>,
610 price_update: Observation,
611 ) -> Self {
612 let mut accepted_history = accepted_history.clone();
613 accepted_history.push(price_update);
614 Self {
615 price_update,
616 accepted_history,
617 }
618 }
619}
620
621serialize! {
622 #[derive(Debug, Clone, PartialEq, Eq)]
623 pub struct CircuitBreakerState<R = CircuitBreaker> {
624 pub breaker: R,
625 pub is_enforced: bool,
626 pub status: CircuitBreakerStatus,
627 }
628}
629
630impl<R> CircuitBreakerState<R> {
631 #[must_use]
632 pub fn new(breaker: R) -> Self {
633 Self {
634 breaker,
635 is_enforced: true,
636 status: CircuitBreakerStatus::ArmedAfter {
637 timestamp_ns: Nanoseconds::zero(),
638 },
639 }
640 }
641
642 pub fn is_blocking(&self) -> bool {
643 self.is_enforced && matches!(self.status, CircuitBreakerStatus::Tripped { .. })
644 }
645}
646
647impl<R: CircuitBreakerRule> CircuitBreakerState<R> {
648 pub(crate) fn apply_armed_transition(
649 &mut self,
650 breaker_id: u32,
651 proposed_acceptance: &ProposedPriceAcceptance,
652 now: Nanoseconds,
653 ) -> Option<CircuitBreakerEvent> {
654 if self.is_armed_at(now)
655 && self
656 .breaker
657 .should_trip(&proposed_acceptance.accepted_history)
658 {
659 Some(self.trip(breaker_id, proposed_acceptance.price_update, now))
660 } else {
661 None
662 }
663 }
664
665 fn is_armed_at(&self, now: Nanoseconds) -> bool {
666 matches!(
667 self.status,
668 CircuitBreakerStatus::ArmedAfter { timestamp_ns } if now >= timestamp_ns
669 )
670 }
671
672 fn trip(
673 &mut self,
674 breaker_id: u32,
675 price_update: Observation,
676 now: Nanoseconds,
677 ) -> CircuitBreakerEvent {
678 self.status = CircuitBreakerStatus::Tripped {
679 tripped_at_ns: now,
680 price_update,
681 };
682 CircuitBreakerEvent::Tripped {
683 breaker_id,
684 tripped_at_ns: now,
685 price_update,
686 is_enforced: self.is_enforced,
687 }
688 }
689}