Skip to main content

rightkit_daemon/
supervisor.rs

1//! A supervisor loop for a daemon-pipe child (issue #295).
2//!
3//! A [`PrivateChild`] is an engine the app started itself and talks to over the daemon pipe. The
4//! `rightkit-job-queue` engine supervisor needs a sidecar stdio `hello` frame, which a daemon
5//! service does not speak; this one watches such a child with the two `rightkit-child-process`
6//! trackers instead:
7//!
8//! * **poll**: every [`SupervisorConfig::poll_interval`] the loop asks whether the child has
9//!   exited (`try_wait`, no reaping surprises: the supervisor owns the child);
10//! * **probe**: a caller-supplied closure says whether the child answers (the usual one is
11//!   [`ping_probe`]: connect with the child's own secret, verify the host, call `ping`). Each
12//!   result is fed to a [`HealthTracker`];
13//! * **relaunch**: an exit, an unhealthy child or a startup that never became healthy is stopped
14//!   (stdin closed first, then the tree is killed after [`SupervisorConfig::stop_grace`]) and the
15//!   [`RestartTracker`] decides: relaunch after its backoff, wait for demand, or give up
16//!   ([`SupervisorEvent::Exhausted`]).
17//!
18//! Decisions that are easy to get wrong, and what was chosen:
19//!
20//! * While a generation has never answered a probe, failures are expected (it is starting), so
21//!   only [`HealthOutcome::StartupTimedOut`] acts; `Unhealthy` acts only after the first success.
22//! * The first successful probe calls [`RestartTracker::ready`], so a child that comes up and later
23//!   crashes starts again from the base backoff (per the restart policy's own rules).
24//! * A failed launch (the closure returned an error) is charged to the restart budget like a
25//!   crash, so a binary that cannot start is not retried at poll speed forever.
26//! * [`SupervisorHandle::restart_now`] on a running child counts as an exit of that generation.
27//!   After the policy gave up ([`SupervisorState::Exhausted`]) it forgets the budget and starts
28//!   again: an explicit user start.
29//! * The probe runs on the supervisor thread and may block as long as its own deadline; give it a
30//!   short one ([`ping_probe`] takes the bound as an argument).
31//! * The child is created, probed and stopped on the supervisor thread only; the launch closure is
32//!   the one thing that crosses to it.
33
34use crate::client::{Client, ClientConfig};
35use crate::host_identity::HostExpectation;
36use crate::private::PrivateChild;
37use rightkit_child_process::{
38    Generation, HealthOutcome, HealthPolicy, HealthProbe, HealthTracker, RestartOutcome,
39    RestartPermit, RestartPolicy, RestartTracker,
40};
41use serde_json::json;
42use std::io;
43use std::sync::mpsc::{self, RecvTimeoutError};
44use std::sync::{Arc, Mutex};
45use std::time::{Duration, Instant};
46
47/// Default [`SupervisorConfig::poll_interval`].
48pub const DEFAULT_POLL_INTERVAL: Duration = Duration::from_secs(1);
49/// Default [`SupervisorConfig::stop_grace`].
50pub const DEFAULT_STOP_GRACE: Duration = Duration::from_secs(5);
51
52const MIN_POLL: Duration = Duration::from_millis(10);
53
54/// How a child is watched and restarted.
55#[derive(Debug, Clone, Copy)]
56pub struct SupervisorConfig {
57    restart: RestartPolicy,
58    health: HealthPolicy,
59    poll_interval: Duration,
60    stop_grace: Duration,
61}
62
63impl SupervisorConfig {
64    /// `restart` decides when and whether a stopped child starts again; `health` decides when
65    /// probe results make a child unhealthy or a start a failure.
66    pub fn new(restart: RestartPolicy, health: HealthPolicy) -> Self {
67        Self {
68            restart,
69            health,
70            poll_interval: DEFAULT_POLL_INTERVAL,
71            stop_grace: DEFAULT_STOP_GRACE,
72        }
73    }
74
75    /// Time between checks (exit poll plus one probe). Default [`DEFAULT_POLL_INTERVAL`]; raised
76    /// to at least 10 ms. A restart's backoff is honoured to its own precision, not this one's.
77    #[must_use]
78    pub fn poll_interval(mut self, interval: Duration) -> Self {
79        self.poll_interval = interval.max(MIN_POLL);
80        self
81    }
82
83    /// How long a child gets to exit by itself after its stdin was closed before its process tree
84    /// is killed. Default [`DEFAULT_STOP_GRACE`].
85    #[must_use]
86    pub fn stop_grace(mut self, grace: Duration) -> Self {
87        self.stop_grace = grace;
88        self
89    }
90}
91
92/// What the supervisor is doing now.
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum SupervisorState {
95    /// A child is running and has not yet answered a probe.
96    Starting,
97    /// A child is running and has answered a probe.
98    Ready,
99    /// The child is gone and the next start is waiting out its backoff.
100    WaitingRestart,
101    /// The child is gone and the policy waits for [`SupervisorHandle::restart_now`].
102    AwaitingDemand,
103    /// The restart budget is spent; nothing runs until [`SupervisorHandle::restart_now`].
104    Exhausted,
105    /// The policy never restarts and the child is gone, or the supervisor has stopped.
106    Stopped,
107}
108
109/// A snapshot of the supervisor, from [`SupervisorHandle::status`].
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111pub struct SupervisorStatus {
112    pub state: SupervisorState,
113    /// Children started so far (the running one included).
114    pub launches: u64,
115    /// The running child's process id, when there is one.
116    pub pid: Option<u32>,
117}
118
119/// What happened, content-free. `launch` is the 1-based count of children started.
120#[derive(Debug, Clone, PartialEq, Eq)]
121pub enum SupervisorEvent {
122    Started {
123        launch: u64,
124        pid: u32,
125    },
126    /// The child answered its first probe.
127    Ready {
128        launch: u64,
129    },
130    /// The child exited by itself.
131    Exited {
132        launch: u64,
133        code: Option<i32>,
134        success: bool,
135    },
136    /// A child that had been healthy failed `consecutive_failures` probes in a row; it is stopped.
137    Unhealthy {
138        launch: u64,
139        consecutive_failures: u32,
140    },
141    /// The child never answered a probe within the policy's startup timeout; it is stopped.
142    StartupTimedOut {
143        launch: u64,
144    },
145    /// The launch closure failed (never started a child).
146    LaunchFailed {
147        error: String,
148    },
149    RestartScheduled {
150        delay: Duration,
151    },
152    AwaitingDemand,
153    /// The restart budget is spent.
154    Exhausted,
155    /// The supervisor will not start another child (a policy that never restarts, or `stop`).
156    Stopped,
157}
158
159/// Callback for [`SupervisorEvent`]s. Runs on the supervisor thread; keep it short.
160pub type SupervisorEventFn = Arc<dyn Fn(&SupervisorEvent) + Send + Sync>;
161
162enum Cmd {
163    Stop,
164    RestartNow,
165}
166
167/// Control of a running supervisor. Dropping it stops the supervisor and its child.
168pub struct SupervisorHandle {
169    tx: mpsc::Sender<Cmd>,
170    status: Arc<Mutex<SupervisorStatus>>,
171    thread: Option<std::thread::JoinHandle<()>>,
172}
173
174impl SupervisorHandle {
175    /// The state now.
176    pub fn status(&self) -> SupervisorStatus {
177        *self.status.lock().unwrap_or_else(|e| e.into_inner())
178    }
179
180    /// Ask for a start: a running child is stopped and counted as an exit (then the policy
181    /// decides, so a policy that waits for demand starts at once); a child waiting for demand
182    /// starts; an exhausted or stopped policy forgets its budget and starts. A pending backoff is
183    /// left to run. Returns before the work is done.
184    pub fn restart_now(&self) {
185        let _ = self.tx.send(Cmd::RestartNow);
186    }
187
188    /// Stop the child (stdin closed, then killed after the grace) and end the supervisor. Waits
189    /// for the thread to finish.
190    pub fn stop(mut self) {
191        self.stop_inner();
192    }
193
194    fn stop_inner(&mut self) {
195        let _ = self.tx.send(Cmd::Stop);
196        if let Some(thread) = self.thread.take() {
197            let _ = thread.join();
198        }
199    }
200}
201
202impl Drop for SupervisorHandle {
203    fn drop(&mut self) {
204        self.stop_inner();
205    }
206}
207
208/// Start supervising. `launch` makes a child (normally [`PrivateChild::spawn`] of an
209/// `OwnedCommand`, with the per-run suite made fresh each time if the child needs one); `probe`
210/// says whether the running child answers (see [`ping_probe`]); `on_event` hears what happens.
211/// The first child is started at once, on the supervisor thread.
212pub fn supervise<L, P>(
213    config: SupervisorConfig,
214    launch: L,
215    probe: P,
216    on_event: Option<SupervisorEventFn>,
217) -> io::Result<SupervisorHandle>
218where
219    L: FnMut() -> io::Result<PrivateChild> + Send + 'static,
220    P: FnMut(&PrivateChild) -> bool + Send + 'static,
221{
222    let (tx, rx) = mpsc::channel();
223    let status = Arc::new(Mutex::new(SupervisorStatus {
224        state: SupervisorState::Starting,
225        launches: 0,
226        pid: None,
227    }));
228    let core = Core {
229        cfg: config,
230        launch,
231        probe,
232        tracker: RestartTracker::new(config.restart),
233        origin: Instant::now(),
234        launches: 0,
235        status: status.clone(),
236        on_event,
237        phase: Phase::Idle(SupervisorState::Stopped),
238    };
239    let thread = std::thread::Builder::new()
240        .name("rk-supervisor".into())
241        .spawn(move || core.run(rx))?;
242    Ok(SupervisorHandle {
243        tx,
244        status,
245        thread: Some(thread),
246    })
247}
248
249/// A probe for [`supervise`]: connect to the child's service with the child's own secret, verify
250/// the host when `expect` is given, and call `ping`. True only for a `{"pong": true}` answer.
251/// `timeout` bounds one probe (connect, hello and the call). Use [`HostExpectation`] whenever the
252/// secret is sent: a secret goes to whatever answered at the endpoint.
253pub fn ping_probe(
254    config: ClientConfig,
255    expect: Option<HostExpectation>,
256    timeout: Duration,
257) -> impl FnMut(&PrivateChild) -> bool + Send + 'static {
258    move |child| {
259        let config = config
260            .clone()
261            .secret(child.secret().clone())
262            .deadline(Some(timeout));
263        let client = match &expect {
264            Some(expect) => Client::connect_expecting(&config, expect).ok(),
265            None => Client::connect_config(&config).ok(),
266        };
267        match client {
268            Some(client) => matches!(
269                client.call("ping", json!({})),
270                Ok(value) if value["pong"] == true
271            ),
272            None => false,
273        }
274    }
275}
276
277struct Running {
278    child: PrivateChild,
279    generation: Generation,
280    health: HealthTracker,
281    ready: bool,
282    launch: u64,
283}
284
285enum Phase {
286    Running(Box<Running>),
287    /// The next start is waiting for `due`.
288    Waiting(RestartPermit, Instant),
289    /// Nothing runs: `AwaitingDemand`, `Exhausted` or `Stopped`.
290    Idle(SupervisorState),
291}
292
293struct Core<L, P> {
294    cfg: SupervisorConfig,
295    launch: L,
296    probe: P,
297    tracker: RestartTracker,
298    origin: Instant,
299    launches: u64,
300    status: Arc<Mutex<SupervisorStatus>>,
301    on_event: Option<SupervisorEventFn>,
302    phase: Phase,
303}
304
305impl<L, P> Core<L, P>
306where
307    L: FnMut() -> io::Result<PrivateChild>,
308    P: FnMut(&PrivateChild) -> bool,
309{
310    fn at(&self) -> Duration {
311        self.origin.elapsed()
312    }
313
314    fn emit(&self, event: SupervisorEvent) {
315        if let Some(f) = &self.on_event {
316            f(&event);
317        }
318    }
319
320    fn set_status(&self, state: SupervisorState, pid: Option<u32>) {
321        let mut s = self.status.lock().unwrap_or_else(|e| e.into_inner());
322        s.state = state;
323        s.launches = self.launches;
324        s.pid = pid;
325    }
326
327    fn run(mut self, rx: mpsc::Receiver<Cmd>) {
328        self.start();
329        loop {
330            let wait = match &self.phase {
331                Phase::Waiting(_, due) => due
332                    .saturating_duration_since(Instant::now())
333                    .min(self.cfg.poll_interval),
334                _ => self.cfg.poll_interval,
335            };
336            match rx.recv_timeout(wait) {
337                Ok(Cmd::RestartNow) => self.restart_now(),
338                Ok(Cmd::Stop) | Err(RecvTimeoutError::Disconnected) => {
339                    self.shutdown();
340                    return;
341                }
342                Err(RecvTimeoutError::Timeout) => self.tick(),
343            }
344        }
345    }
346
347    /// Launch a child now. A launch failure is charged to the budget like a crash.
348    fn start(&mut self) {
349        match (self.launch)() {
350            Ok(child) => {
351                let at = self.at();
352                let generation = self.tracker.started(at);
353                self.launches += 1;
354                let pid = child.child().id();
355                self.set_status(SupervisorState::Starting, Some(pid));
356                self.emit(SupervisorEvent::Started {
357                    launch: self.launches,
358                    pid,
359                });
360                self.phase = Phase::Running(Box::new(Running {
361                    child,
362                    generation,
363                    health: HealthTracker::new(self.cfg.health, at),
364                    ready: false,
365                    launch: self.launches,
366                }));
367            }
368            Err(error) => {
369                self.emit(SupervisorEvent::LaunchFailed {
370                    error: error.to_string(),
371                });
372                let outcome = self.tracker.request_restart(self.at());
373                self.apply(outcome);
374            }
375        }
376    }
377
378    fn apply(&mut self, outcome: RestartOutcome) {
379        match outcome {
380            RestartOutcome::Restart(permit) => {
381                let delay = permit.delay();
382                self.emit(SupervisorEvent::RestartScheduled { delay });
383                self.set_status(SupervisorState::WaitingRestart, None);
384                self.phase = Phase::Waiting(permit, Instant::now() + delay);
385            }
386            RestartOutcome::AwaitingDemand => {
387                self.emit(SupervisorEvent::AwaitingDemand);
388                self.set_status(SupervisorState::AwaitingDemand, None);
389                self.phase = Phase::Idle(SupervisorState::AwaitingDemand);
390            }
391            RestartOutcome::Exhausted => {
392                self.emit(SupervisorEvent::Exhausted);
393                self.set_status(SupervisorState::Exhausted, None);
394                self.phase = Phase::Idle(SupervisorState::Exhausted);
395            }
396            // `IgnoredStale` cannot happen for a generation this loop owns; if it ever did,
397            // stopping loudly beats a silent loop.
398            RestartOutcome::Stopped | RestartOutcome::IgnoredStale => {
399                self.emit(SupervisorEvent::Stopped);
400                self.set_status(SupervisorState::Stopped, None);
401                self.phase = Phase::Idle(SupervisorState::Stopped);
402            }
403        }
404    }
405
406    fn tick(&mut self) {
407        let placeholder = Phase::Idle(SupervisorState::Stopped);
408        match std::mem::replace(&mut self.phase, placeholder) {
409            Phase::Running(running) => self.tick_running(*running),
410            Phase::Waiting(permit, due) => {
411                if Instant::now() >= due && self.tracker.permit_valid(&permit, self.at()) {
412                    self.start();
413                } else {
414                    self.phase = Phase::Waiting(permit, due);
415                }
416            }
417            idle @ Phase::Idle(_) => self.phase = idle,
418        }
419    }
420
421    fn tick_running(&mut self, mut running: Running) {
422        match running.child.child_mut().try_wait() {
423            Ok(Some(status)) => {
424                self.emit(SupervisorEvent::Exited {
425                    launch: running.launch,
426                    code: status.code(),
427                    success: status.success(),
428                });
429                // The child has been reaped by `try_wait`; dropping the wrapper closes its stdin.
430                drop(running.child);
431                let outcome = self.tracker.exited(running.generation, self.at());
432                self.apply(outcome);
433                return;
434            }
435            Ok(None) => {}
436            Err(_) => {
437                // The state of the child cannot be read: do not guess it is fine.
438                self.stop_child(running.child);
439                let outcome = self.tracker.exited(running.generation, self.at());
440                self.apply(outcome);
441                return;
442            }
443        }
444        let answered = (self.probe)(&running.child);
445        let at = self.at();
446        let outcome = running.health.observe(
447            at,
448            if answered {
449                HealthProbe::Success
450            } else {
451                HealthProbe::Failure
452            },
453        );
454        if answered && !running.ready {
455            running.ready = true;
456            self.tracker.ready(running.generation);
457            self.set_status(SupervisorState::Ready, Some(running.child.child().id()));
458            self.emit(SupervisorEvent::Ready {
459                launch: running.launch,
460            });
461        }
462        let failure = match outcome {
463            HealthOutcome::StartupTimedOut => Some(SupervisorEvent::StartupTimedOut {
464                launch: running.launch,
465            }),
466            // Failures before the first success are a child still starting, not an unhealthy one.
467            HealthOutcome::Unhealthy {
468                consecutive_failures,
469            } if running.ready => Some(SupervisorEvent::Unhealthy {
470                launch: running.launch,
471                consecutive_failures,
472            }),
473            _ => None,
474        };
475        match failure {
476            None => self.phase = Phase::Running(Box::new(running)),
477            Some(event) => {
478                self.emit(event);
479                self.stop_child(running.child);
480                let outcome = self.tracker.exited(running.generation, self.at());
481                self.apply(outcome);
482            }
483        }
484    }
485
486    /// Stdin closed (the polite cue), then the whole tree is killed after the grace.
487    fn stop_child(&self, mut child: PrivateChild) {
488        child.close_stdin();
489        let _ = child.child_mut().wait_then_kill(self.cfg.stop_grace);
490    }
491
492    fn restart_now(&mut self) {
493        let placeholder = Phase::Idle(SupervisorState::Stopped);
494        match std::mem::replace(&mut self.phase, placeholder) {
495            Phase::Running(running) => {
496                self.stop_child(running.child);
497                let at = self.at();
498                let mut outcome = self.tracker.exited(running.generation, at);
499                if outcome == RestartOutcome::AwaitingDemand {
500                    // This is the demand.
501                    outcome = self.tracker.request_restart(at);
502                }
503                self.apply(outcome);
504            }
505            // A start is already on its way.
506            waiting @ Phase::Waiting(..) => self.phase = waiting,
507            Phase::Idle(SupervisorState::AwaitingDemand) => {
508                match self.tracker.request_restart(self.at()) {
509                    // Nothing ever ran, so the tracker was not waiting: start directly.
510                    RestartOutcome::AwaitingDemand => self.start(),
511                    other => self.apply(other),
512                }
513            }
514            Phase::Idle(_) => {
515                self.tracker.reset_budget();
516                self.start();
517            }
518        }
519    }
520
521    fn shutdown(&mut self) {
522        self.tracker.shutdown();
523        if let Phase::Running(running) =
524            std::mem::replace(&mut self.phase, Phase::Idle(SupervisorState::Stopped))
525        {
526            self.stop_child(running.child);
527        }
528        self.set_status(SupervisorState::Stopped, None);
529        self.emit(SupervisorEvent::Stopped);
530    }
531}