appcore_supervisor/
watchdog.rs1use crate::{SupervisorError, SupervisorResult};
14use std::sync::atomic::{AtomicU64, AtomicU8, Ordering};
15
16pub const DEFAULT_WATCHDOG_CHECK_INTERVAL_MS: u64 = 1_000;
18pub const DEFAULT_WATCHDOG_STALL_TIMEOUT_MS: u64 = 15_000;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub struct WatchdogConfig {
24 pub enabled: bool,
26 pub check_interval_ms: u64,
28 pub stall_timeout_ms: u64,
30}
31
32impl WatchdogConfig {
33 pub fn validate(self) -> SupervisorResult<Self> {
35 if self.check_interval_ms == 0 || self.stall_timeout_ms == 0 {
36 return Err(SupervisorError::InvalidConfiguration(
37 "watchdog intervals must be greater than zero".to_string(),
38 ));
39 }
40 if self.enabled && self.stall_timeout_ms <= self.check_interval_ms {
41 return Err(SupervisorError::InvalidConfiguration(
42 "watchdog stall timeout must exceed its check interval".to_string(),
43 ));
44 }
45 Ok(self)
46 }
47}
48
49impl Default for WatchdogConfig {
50 fn default() -> Self {
51 Self {
52 enabled: true,
53 check_interval_ms: DEFAULT_WATCHDOG_CHECK_INTERVAL_MS,
54 stall_timeout_ms: DEFAULT_WATCHDOG_STALL_TIMEOUT_MS,
55 }
56 }
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum WatchdogState {
62 Starting,
64 Healthy,
66 Stalled,
68 Failed,
70 Stopping,
72}
73
74impl WatchdogState {
75 fn as_u8(self) -> u8 {
76 match self {
77 Self::Starting => 0,
78 Self::Healthy => 1,
79 Self::Stalled => 2,
80 Self::Failed => 3,
81 Self::Stopping => 4,
82 }
83 }
84
85 fn from_u8(value: u8) -> Self {
86 match value {
87 1 => Self::Healthy,
88 2 => Self::Stalled,
89 3 => Self::Failed,
90 4 => Self::Stopping,
91 _ => Self::Starting,
92 }
93 }
94}
95
96pub struct SupervisorWatchdog {
98 config: WatchdogConfig,
99 created_at_ms: u64,
100 last_reconcile_at_ms: AtomicU64,
101 reconcile_sequence: AtomicU64,
102 last_progress_at_ms: AtomicU64,
103 state: AtomicU8,
104}
105
106impl SupervisorWatchdog {
107 pub fn new(config: WatchdogConfig, created_at_ms: u64) -> SupervisorResult<Self> {
109 Ok(Self::from_validated(config.validate()?, created_at_ms))
110 }
111
112 pub(crate) fn with_default(created_at_ms: u64) -> Self {
113 Self::from_validated(WatchdogConfig::default(), created_at_ms)
114 }
115
116 fn from_validated(config: WatchdogConfig, created_at_ms: u64) -> Self {
117 Self {
118 config,
119 created_at_ms,
120 last_reconcile_at_ms: AtomicU64::new(0),
121 reconcile_sequence: AtomicU64::new(0),
122 last_progress_at_ms: AtomicU64::new(0),
123 state: AtomicU8::new(WatchdogState::Starting.as_u8()),
124 }
125 }
126
127 pub fn record_reconcile_started(&self, timestamp_ms: u64) {
129 self.last_reconcile_at_ms
130 .store(timestamp_ms, Ordering::Release);
131 }
132
133 pub fn record_reconcile_completed(&self, timestamp_ms: u64) -> u64 {
135 self.last_reconcile_at_ms
136 .store(timestamp_ms, Ordering::Release);
137 self.last_progress_at_ms
138 .store(timestamp_ms, Ordering::Release);
139 let sequence = self
140 .reconcile_sequence
141 .fetch_add(1, Ordering::AcqRel)
142 .saturating_add(1);
143 if !matches!(
144 self.state(),
145 WatchdogState::Stopping | WatchdogState::Failed
146 ) {
147 self.set_state(WatchdogState::Healthy);
148 }
149 sequence
150 }
151
152 pub fn evaluate(&self, timestamp_ms: u64) -> Option<(WatchdogState, WatchdogState)> {
154 let previous = self.state();
155 if matches!(previous, WatchdogState::Stopping | WatchdogState::Failed) {
156 return None;
157 }
158 let next = if !self.config.enabled {
159 WatchdogState::Healthy
160 } else if self.stalled_for_ms(timestamp_ms) > self.config.stall_timeout_ms {
161 WatchdogState::Stalled
162 } else if self.reconcile_sequence.load(Ordering::Acquire) == 0 {
163 WatchdogState::Starting
164 } else {
165 WatchdogState::Healthy
166 };
167 if previous == next {
168 return None;
169 }
170 self.set_state(next);
171 Some((previous, next))
172 }
173
174 pub fn mark_stopping(&self) {
176 self.set_state(WatchdogState::Stopping);
177 }
178
179 pub fn mark_failed(&self) {
181 self.set_state(WatchdogState::Failed);
182 }
183
184 pub fn config(&self) -> WatchdogConfig {
186 self.config
187 }
188
189 pub fn state(&self) -> WatchdogState {
191 WatchdogState::from_u8(self.state.load(Ordering::Acquire))
192 }
193
194 pub fn last_reconcile_at_ms(&self) -> u64 {
196 self.last_reconcile_at_ms.load(Ordering::Acquire)
197 }
198
199 pub fn reconcile_sequence(&self) -> u64 {
201 self.reconcile_sequence.load(Ordering::Acquire)
202 }
203
204 pub fn last_progress_at_ms(&self) -> u64 {
206 self.last_progress_at_ms.load(Ordering::Acquire)
207 }
208
209 pub fn stalled_for_ms(&self, timestamp_ms: u64) -> u64 {
211 let progress = self.last_progress_at_ms();
212 timestamp_ms.saturating_sub(if progress == 0 {
213 self.created_at_ms
214 } else {
215 progress
216 })
217 }
218
219 fn set_state(&self, state: WatchdogState) {
220 self.state.store(state.as_u8(), Ordering::Release);
221 }
222}