1use dashmap::DashMap;
2use once_cell::sync::Lazy;
3use std::fmt::{Display, Formatter};
4use std::{
5 fmt::Debug,
6 sync::{
7 atomic::{AtomicBool, Ordering::Relaxed},
8 Arc,
9 },
10 thread,
11 time::{Duration, Instant},
12};
13
14static START: Lazy<Instant> = Lazy::new(Instant::now);
15
16#[inline(always)]
17fn now_ms() -> u64 {
18 START.elapsed().as_millis() as u64
19}
20
21#[derive(Clone, Copy, Debug)]
23pub enum OsThreadHandle {
24 #[cfg(target_os = "linux")]
25 LinuxTid(i32),
26 #[cfg(target_os = "windows")]
27 WindowsTid(u32),
28 None,
29}
30
31#[cfg(feature = "demotion")]
32impl OsThreadHandle {
33 #[inline(always)]
34 pub fn current() -> Self {
35 #[cfg(target_os = "linux")]
36 unsafe {
37 OsThreadHandle::LinuxTid(libc::syscall(libc::SYS_gettid) as i32)
38 }
39 #[cfg(target_os = "windows")]
40 {
41 use windows_sys::Win32::System::Threading::GetCurrentThreadId;
42 OsThreadHandle::WindowsTid(unsafe { GetCurrentThreadId() })
43 }
44 }
45}
46
47#[derive(Clone, Debug)]
49pub struct Config {
50 pub max_unhealthy_executors: usize,
53 pub stall_threshold: Duration,
55 pub check_interval: Duration,
57 #[cfg(feature = "demotion")]
59 pub demote_stalled_threads: bool,
60}
61
62impl Default for Config {
63 fn default() -> Self {
64 Self {
65 max_unhealthy_executors: 0,
66 stall_threshold: Duration::from_secs(299),
67 check_interval: Duration::from_secs(30),
68 #[cfg(feature = "demotion")]
69 demote_stalled_threads: true,
70 }
71 }
72}
73
74pub trait Hooks<ExecutorIdType, EventType>: Send + Sync + 'static {
77 fn on_started(&self, _key: &ExecutorIdType, _token: &EventType) {}
79 fn on_stalled(&self, _key: &ExecutorIdType, _token: &EventType) {}
81 fn on_recovered(&self, _key: &ExecutorIdType, _token: &EventType) {}
84 fn on_meltdown(&self, _stalled: &[(ExecutorIdType, EventType)]) {}
87}
88
89pub struct NoopHooks;
91impl<ExecutorIdType, EventType> Hooks<ExecutorIdType, EventType> for NoopHooks {}
92
93#[cfg(feature = "demotion")]
94#[derive(Clone, Copy, Debug)]
95struct DemotionInfo {
96 handle: OsThreadHandle,
97 #[cfg(target_os = "linux")]
98 orig_nice: i32,
99 #[cfg(target_os = "windows")]
100 orig_prio: i32,
101}
102
103struct Entry<EventType: Clone> {
104 started_ms: u64,
105 is_stalled: AtomicBool,
106 event_payload: EventType,
107 #[cfg(feature = "demotion")]
108 demotion: Option<DemotionInfo>,
109}
110
111pub struct Watchdog<ExecutorIdType, EventType, HooksType = NoopHooks>
112where
113 ExecutorIdType: Eq + std::hash::Hash + Clone + Debug + Send + Sync + 'static,
114 EventType: Clone + Send + Sync + 'static,
115 HooksType: Hooks<ExecutorIdType, EventType>,
116{
117 config: Config,
118 accepting: AtomicBool,
119 entries: DashMap<ExecutorIdType, Entry<EventType>>,
120 hooks: HooksType,
121 #[cfg(feature = "systemd")]
122 systemd_ping: bool,
123}
124
125impl<ExecutorIdType, EventType> Watchdog<ExecutorIdType, EventType, NoopHooks>
126where
127 ExecutorIdType: Eq + std::hash::Hash + Clone + Debug + Send + Sync + 'static,
128 EventType: Clone + Send + Sync + 'static,
129{
130 pub fn new(cfg: Config) -> Self {
131 Self {
132 config: cfg,
133 accepting: AtomicBool::new(true),
134 entries: DashMap::new(),
135 hooks: NoopHooks,
136 #[cfg(feature = "systemd")]
137 systemd_ping: false,
138 }
139 }
140}
141
142impl<ExecutorIdType, EventType, HooksType> Watchdog<ExecutorIdType, EventType, HooksType>
143where
144 ExecutorIdType: Eq + std::hash::Hash + Clone + Debug + Send + Sync + 'static,
145 EventType: Clone + Send + Sync + 'static,
146 HooksType: Hooks<ExecutorIdType, EventType>,
147{
148 pub fn with_hooks<NewHooksType: Hooks<ExecutorIdType, EventType>>(
150 self,
151 hooks: NewHooksType,
152 ) -> Watchdog<ExecutorIdType, EventType, NewHooksType> {
153 Watchdog {
154 config: self.config,
155 accepting: self.accepting,
156 entries: self.entries,
157 hooks,
158 #[cfg(feature = "systemd")]
159 systemd_ping: self.systemd_ping,
160 }
161 }
162 #[cfg(feature = "systemd")]
164 pub fn with_systemd_ping(mut self, enabled: bool) -> Self {
165 self.systemd_ping = enabled;
166 self
167 }
168
169 pub fn set_accepting(&self, v: bool) {
171 self.accepting.store(v, Relaxed);
172 }
173
174 pub fn on_work_start(
176 &self,
177 key: &ExecutorIdType,
178 event_payload: &EventType,
179 ) -> Result<(), MeltdownInProgress> {
180 if !self.accepting.load(Relaxed) {
181 return Err(MeltdownInProgress);
182 }
183
184 #[cfg(feature = "demotion")]
185 let demotion = if self.config.demote_stalled_threads {
186 let handle = OsThreadHandle::current();
188 #[cfg(target_os = "linux")]
189 unsafe {
190 let orig = libc::getpriority(libc::PRIO_PROCESS, 0) as i32;
191 Some(DemotionInfo {
192 handle,
193 orig_nice: orig,
194 })
195 }
196 #[cfg(target_os = "windows")]
197 {
198 use windows_sys::Win32::System::Threading::{GetCurrentThread, GetThreadPriority};
199 unsafe {
200 let h = GetCurrentThread();
201 let p = GetThreadPriority(h);
202 Some(DemotionInfo {
203 handle,
204 orig_prio: p,
205 })
206 }
207 }
208 } else {
209 None
210 };
211
212 self.entries.insert(
213 key.clone(),
214 Entry {
215 started_ms: now_ms(),
216 is_stalled: AtomicBool::new(false),
217 event_payload: event_payload.clone(),
218 #[cfg(feature = "demotion")]
219 demotion,
220 },
221 );
222 self.hooks.on_started(key, event_payload);
223 Ok(())
224 }
225
226 pub fn on_work_finish(&self, key: &ExecutorIdType) {
228 if let Some((executor_id, entry)) = self.entries.remove(key) {
229 if entry.is_stalled.load(Relaxed) {
230 self.hooks.on_recovered(&executor_id, &entry.event_payload);
232 #[cfg(feature = "demotion")]
233 if let Some(info) = entry.demotion {
234 let _ = promote_thread(info);
235 }
236 }
237 }
238 }
239
240 pub fn start(self) -> Arc<Self> {
242 let this = Arc::new(self);
243 let this_clone = this.clone();
244 thread::spawn(move || {
245 let stall_ms = this.config.stall_threshold.as_millis() as u64;
246
247 loop {
248 #[cfg(feature = "systemd")]
249 if this.systemd_ping {
250 let _ = sd_notify::notify(false, &[sd_notify::NotifyState::Watchdog]);
251 }
252
253 thread::sleep(this.config.check_interval);
254
255 let now = now_ms();
256
257 let mut stalled_count = 0;
258
259 for kv in this.entries.iter() {
261 let started = kv.value().started_ms;
262 if now.saturating_sub(started) > stall_ms {
263 if !kv.value().is_stalled.swap(true, Relaxed) {
264 #[cfg(feature = "demotion")]
265 if this.config.demote_stalled_threads {
266 if let Some(info) = kv.value().demotion {
267 let _ = demote_thread(info);
268 }
269 }
270 this.hooks.on_stalled(kv.key(), &kv.value().event_payload);
271 }
272 stalled_count += 1;
273 }
274 }
275
276 if stalled_count > this.config.max_unhealthy_executors {
278 break;
279 }
280 }
281
282 this.accepting.store(false, Relaxed);
285 thread::sleep(this.config.stall_threshold);
286
287 if !this.entries.is_empty() {
288 let pairs: Vec<(ExecutorIdType, EventType)> = this
289 .entries
290 .iter()
291 .map(|kv| (kv.key().clone(), kv.value().event_payload.clone()))
292 .collect();
293 this.hooks.on_meltdown(&pairs);
294 }
295 });
296 this_clone
297 }
298}
299
300#[derive(Debug)]
302pub struct Draining;
303
304#[cfg(feature = "demotion")]
305fn demote_thread(info: DemotionInfo) -> std::io::Result<()> {
306 match info.handle {
307 #[cfg(target_os = "linux")]
308 OsThreadHandle::LinuxTid(tid) => unsafe {
309 let r = libc::setpriority(libc::PRIO_PROCESS, tid as u32, 19);
310 if r == 0 {
311 Ok(())
312 } else {
313 Err(std::io::Error::last_os_error())
314 }
315 },
316 #[cfg(target_os = "windows")]
317 OsThreadHandle::WindowsTid(tid) => {
318 use windows_sys::Win32::Foundation::CloseHandle;
319 use windows_sys::Win32::System::Threading::{
320 OpenThread, SetThreadPriority, THREAD_PRIORITY_IDLE, THREAD_QUERY_INFORMATION,
321 THREAD_SET_INFORMATION,
322 };
323 unsafe {
324 let h = OpenThread(THREAD_SET_INFORMATION | THREAD_QUERY_INFORMATION, 0, tid);
325 if h == std::ptr::null_mut() {
326 return Err(std::io::Error::last_os_error());
327 }
328 let ok = SetThreadPriority(h, THREAD_PRIORITY_IDLE);
329 let last = std::io::Error::last_os_error();
330 CloseHandle(h);
331 if ok != 0 {
332 Ok(())
333 } else {
334 Err(last)
335 }
336 }
337 }
338 _ => Ok(()),
339 }
340}
341
342#[cfg(feature = "demotion")]
343fn promote_thread(info: DemotionInfo) -> std::io::Result<()> {
344 match info.handle {
345 #[cfg(target_os = "linux")]
346 OsThreadHandle::LinuxTid(tid) => unsafe {
347 let r = libc::setpriority(libc::PRIO_PROCESS, tid as u32, info.orig_nice);
348 if r == 0 {
349 Ok(())
350 } else {
351 Err(std::io::Error::last_os_error())
352 }
353 },
354 #[cfg(target_os = "windows")]
355 OsThreadHandle::WindowsTid(tid) => {
356 use windows_sys::Win32::Foundation::CloseHandle;
357 use windows_sys::Win32::System::Threading::{
358 OpenThread, SetThreadPriority, THREAD_QUERY_INFORMATION, THREAD_SET_INFORMATION,
359 };
360 unsafe {
361 let h = OpenThread(THREAD_SET_INFORMATION | THREAD_QUERY_INFORMATION, 0, tid);
362 if h == std::ptr::null_mut() {
363 return Err(std::io::Error::last_os_error());
364 }
365 let ok = SetThreadPriority(h, info.orig_prio);
366 let last = std::io::Error::last_os_error();
367 CloseHandle(h);
368 if ok != 0 {
369 Ok(())
370 } else {
371 Err(last)
372 }
373 }
374 }
375 _ => Ok(()),
376 }
377}
378
379#[derive(Debug)]
380pub struct MeltdownInProgress;
381impl Display for MeltdownInProgress {
382 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
383 <Self as Debug>::fmt(self, f)
384 }
385}
386impl std::error::Error for MeltdownInProgress {}