1#![allow(dead_code)]
17#![allow(clippy::redundant_pub_crate)]
18
19use core::sync::atomic::{AtomicU64, Ordering};
20use core::time::Duration;
21use std::time::Instant;
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub enum FaultState {
27 Running,
29 Faulted {
31 reason: FaultReason,
33 since_ms: u32,
36 },
37}
38
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
41pub enum FaultReason {
42 BudgetExceeded {
45 took_ms: u32,
47 budget_ms: u32,
49 },
50 ExecutorFaulted,
54}
55
56#[derive(Clone, Copy, Debug, PartialEq, Eq)]
58pub enum ExecutorFaultState {
59 Running,
61 Faulted {
64 reason: ExecutorFaultReason,
66 since_ms: u32,
68 },
69}
70
71#[derive(Clone, Copy, Debug, PartialEq, Eq)]
73pub enum ExecutorFaultReason {
74 IterationBudgetExceeded {
78 task_idx: u32,
80 took_ms: u32,
82 budget_ms: u32,
84 },
85}
86
87#[derive(Debug, Default)]
102pub(crate) struct FaultAtomic(AtomicU64);
103
104impl FaultAtomic {
105 pub(crate) const fn new() -> Self {
107 Self(AtomicU64::new(0))
108 }
109
110 #[allow(clippy::cast_possible_truncation)]
112 pub(crate) fn pack(state: FaultState) -> u64 {
113 match state {
114 FaultState::Running => 0,
115 FaultState::Faulted { reason, since_ms } => {
116 let (disc, took_ms) = match reason {
117 FaultReason::BudgetExceeded { took_ms, .. } => (1_u64, took_ms),
118 FaultReason::ExecutorFaulted => (2_u64, 0_u32),
119 };
120 (disc << 62) | ((u64::from(took_ms) & 0x3FFF_FFFF) << 32) | u64::from(since_ms)
121 }
122 }
123 }
124
125 #[allow(clippy::cast_possible_truncation)]
127 pub(crate) const fn unpack(packed: u64, budget_ms: u32) -> FaultState {
128 let disc = (packed >> 62) & 0x3;
129 let took_ms = ((packed >> 32) & 0x3FFF_FFFF) as u32;
130 let since_ms = (packed & 0xFFFF_FFFF) as u32;
131 match disc {
132 1 => FaultState::Faulted {
133 reason: FaultReason::BudgetExceeded { took_ms, budget_ms },
134 since_ms,
135 },
136 2 => FaultState::Faulted {
137 reason: FaultReason::ExecutorFaulted,
138 since_ms,
139 },
140 _ => FaultState::Running,
142 }
143 }
144
145 pub(crate) fn load(&self, budget_ms: u32) -> FaultState {
147 Self::unpack(self.0.load(Ordering::Acquire), budget_ms)
148 }
149
150 pub(crate) fn swap(&self, state: FaultState, budget_ms: u32) -> FaultState {
154 Self::unpack(self.0.swap(Self::pack(state), Ordering::AcqRel), budget_ms)
155 }
156}
157
158#[derive(Debug, Default)]
170pub(crate) struct ExecutorFaultAtomic(AtomicU64);
171
172impl ExecutorFaultAtomic {
173 pub(crate) const fn new() -> Self {
175 Self(AtomicU64::new(0))
176 }
177
178 #[allow(clippy::cast_possible_truncation)]
180 pub(crate) fn pack(state: ExecutorFaultState) -> u64 {
181 match state {
182 ExecutorFaultState::Running => 0,
183 ExecutorFaultState::Faulted { reason, since_ms } => {
184 let took_ms = match reason {
185 ExecutorFaultReason::IterationBudgetExceeded { took_ms, .. } => took_ms,
186 };
187 (1_u64 << 62) | ((u64::from(took_ms) & 0x3FFF_FFFF) << 32) | u64::from(since_ms)
188 }
189 }
190 }
191
192 #[allow(clippy::cast_possible_truncation)]
196 pub(crate) const fn unpack(packed: u64, task_idx: u32, budget_ms: u32) -> ExecutorFaultState {
197 let disc = (packed >> 62) & 0x3;
198 let took_ms = ((packed >> 32) & 0x3FFF_FFFF) as u32;
199 let since_ms = (packed & 0xFFFF_FFFF) as u32;
200 match disc {
201 1 => ExecutorFaultState::Faulted {
202 reason: ExecutorFaultReason::IterationBudgetExceeded {
203 task_idx,
204 took_ms,
205 budget_ms,
206 },
207 since_ms,
208 },
209 _ => ExecutorFaultState::Running,
211 }
212 }
213
214 pub(crate) fn load(&self, task_idx: u32, budget_ms: u32) -> ExecutorFaultState {
216 Self::unpack(self.0.load(Ordering::Acquire), task_idx, budget_ms)
217 }
218
219 pub(crate) fn swap(
221 &self,
222 state: ExecutorFaultState,
223 task_idx: u32,
224 budget_ms: u32,
225 ) -> ExecutorFaultState {
226 Self::unpack(
227 self.0.swap(Self::pack(state), Ordering::AcqRel),
228 task_idx,
229 budget_ms,
230 )
231 }
232}
233
234#[allow(clippy::cast_possible_truncation)]
236pub(crate) fn duration_to_ms_sat(d: Duration) -> u32 {
237 let ms = d.as_millis();
238 if ms > u128::from(u32::MAX) {
239 u32::MAX
240 } else {
241 ms as u32
242 }
243}
244
245pub(crate) fn instant_to_since_ms(at: Instant, start: Instant) -> u32 {
248 duration_to_ms_sat(at.saturating_duration_since(start))
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254
255 #[test]
256 fn pack_unpack_running() {
257 let s = FaultState::Running;
258 let p = FaultAtomic::pack(s);
259 assert_eq!(p, 0);
260 assert_eq!(FaultAtomic::unpack(p, 100), FaultState::Running);
261 }
262
263 #[test]
264 fn pack_unpack_budget_exceeded_round_trip() {
265 let s = FaultState::Faulted {
266 reason: FaultReason::BudgetExceeded {
267 took_ms: 17,
268 budget_ms: 10,
269 },
270 since_ms: 1_234,
271 };
272 let p = FaultAtomic::pack(s);
273 let restored = FaultAtomic::unpack(p, 10);
275 assert_eq!(restored, s);
276 }
277
278 #[test]
279 fn pack_unpack_executor_faulted_round_trip() {
280 let s = FaultState::Faulted {
281 reason: FaultReason::ExecutorFaulted,
282 since_ms: u32::MAX,
283 };
284 let p = FaultAtomic::pack(s);
285 let restored = FaultAtomic::unpack(p, 999);
287 assert_eq!(restored, s);
288 }
289
290 #[test]
291 fn pack_unpack_saturates_took_ms() {
292 let s = FaultState::Faulted {
293 reason: FaultReason::BudgetExceeded {
294 took_ms: 0x3FFF_FFFF,
295 budget_ms: 5,
296 },
297 since_ms: 42,
298 };
299 let p = FaultAtomic::pack(s);
300 let restored = FaultAtomic::unpack(p, 5);
301 assert_eq!(restored, s);
302 }
303
304 #[test]
305 fn fault_atomic_swap_returns_previous() {
306 let fa = FaultAtomic::new();
307 assert_eq!(fa.load(0), FaultState::Running);
308 let prev = fa.swap(
309 FaultState::Faulted {
310 reason: FaultReason::BudgetExceeded {
311 took_ms: 5,
312 budget_ms: 3,
313 },
314 since_ms: 100,
315 },
316 3,
317 );
318 assert_eq!(prev, FaultState::Running);
319 let prev = fa.swap(FaultState::Running, 3);
320 assert!(matches!(
321 prev,
322 FaultState::Faulted {
323 reason: FaultReason::BudgetExceeded { .. },
324 ..
325 }
326 ));
327 }
328
329 #[test]
330 fn executor_fault_atomic_swap_returns_previous() {
331 let efa = ExecutorFaultAtomic::new();
332 assert_eq!(efa.load(0, 0), ExecutorFaultState::Running);
333 let prev = efa.swap(
334 ExecutorFaultState::Faulted {
335 reason: ExecutorFaultReason::IterationBudgetExceeded {
336 task_idx: 3,
337 took_ms: 20,
338 budget_ms: 10,
339 },
340 since_ms: 50,
341 },
342 3,
343 10,
344 );
345 assert_eq!(prev, ExecutorFaultState::Running);
346 }
347
348 #[test]
349 fn duration_helpers_saturate() {
350 assert_eq!(duration_to_ms_sat(Duration::from_millis(5)), 5);
351 let huge = Duration::from_secs(60 * 60 * 24 * 365 * 100); assert_eq!(duration_to_ms_sat(huge), u32::MAX);
353 }
354}