switchyard_libsy/algorithms/vgr/
safety.rs1use std::sync::Arc;
5use std::sync::atomic::{AtomicBool, Ordering};
6use std::time::{Duration, Instant};
7
8use http::StatusCode;
9use parking_lot::Mutex;
10use switchyard_protocol::LlmClientError;
11
12use crate::LibsyError;
13
14#[derive(Clone, Debug, Default)]
16pub struct KillSwitch(Arc<AtomicBool>);
17
18impl KillSwitch {
19 pub fn new() -> Self {
21 Self::default()
22 }
23
24 pub fn engage(&self) {
26 self.0.store(true, Ordering::Relaxed);
27 }
28
29 pub fn release(&self) {
31 self.0.store(false, Ordering::Relaxed);
32 }
33
34 pub fn is_engaged(&self) -> bool {
36 self.0.load(Ordering::Relaxed)
37 }
38}
39
40#[derive(Clone, Copy, Debug)]
42pub struct BreakerConfig {
43 pub threshold: u32,
45 pub cooldown: Duration,
47}
48
49impl Default for BreakerConfig {
50 fn default() -> Self {
51 Self {
52 threshold: 5,
53 cooldown: Duration::from_secs(30),
54 }
55 }
56}
57
58#[derive(Debug, Default)]
59struct BreakerState {
60 failures: u32,
61 opened_at: Option<Instant>,
62 trial_in_flight: bool,
63}
64
65#[derive(Debug)]
66pub(super) struct CircuitBreaker {
67 config: BreakerConfig,
68 state: Mutex<BreakerState>,
69}
70
71impl CircuitBreaker {
72 pub(super) fn new(config: BreakerConfig) -> Self {
73 Self {
74 config,
75 state: Mutex::new(BreakerState::default()),
76 }
77 }
78
79 pub(super) fn is_open(&self) -> bool {
80 let mut state = self.state.lock();
81 let Some(opened_at) = state.opened_at else {
82 return false;
83 };
84 if state.trial_in_flight || opened_at.elapsed() < self.config.cooldown {
85 return true;
86 }
87 state.trial_in_flight = true;
88 false
89 }
90
91 pub(super) fn success(&self) {
92 *self.state.lock() = BreakerState::default();
93 }
94
95 pub(super) fn failure(&self) {
96 let mut state = self.state.lock();
97 state.failures = state.failures.saturating_add(1);
98 state.trial_in_flight = false;
99 if state.failures >= self.config.threshold {
100 state.opened_at = Some(Instant::now());
101 }
102 }
103}
104
105pub(super) fn fallback_eligible(error: &LibsyError) -> bool {
107 matches!(
108 error,
109 LibsyError::ClientCall { source, .. } if match source {
110 LlmClientError::ContextWindowExceeded { .. }
111 | LlmClientError::Transport { .. }
112 | LlmClientError::Timeout { .. } => true,
113 LlmClientError::UpstreamHttp { status, .. } =>
114 matches!(
115 *status,
116 StatusCode::FORBIDDEN
117 | StatusCode::REQUEST_TIMEOUT
118 | StatusCode::TOO_MANY_REQUESTS
119 ) || status.is_server_error(),
120 _ => false,
121 }
122 )
123}
124
125pub(super) fn endpoint_failure(error: &LibsyError) -> bool {
127 matches!(
128 error,
129 LibsyError::ClientCall { source, .. } if match source {
130 LlmClientError::Transport { .. } | LlmClientError::Timeout { .. } => true,
131 LlmClientError::UpstreamHttp { status, .. } => matches!(
132 *status,
133 StatusCode::BAD_GATEWAY
134 | StatusCode::SERVICE_UNAVAILABLE
135 | StatusCode::GATEWAY_TIMEOUT
136 ),
137 _ => false,
138 }
139 )
140}