use std::num::NonZeroU32;
use std::time::Duration;
use engenho_substrate::maquina::StateMachine;
use serde::{Deserialize, Serialize};
use crate::boot::{BootPhase, FailureClass, Timestamp, backoff};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ExitIntent {
Halt,
Relaunch,
}
impl ExitIntent {
#[must_use]
pub const fn code(self) -> i32 {
match self {
Self::Halt => 0,
Self::Relaunch => 75,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum StopReason {
OperatorRequest,
HeldAtStartup,
ReinitComplete,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(tag = "then", rename_all = "snake_case")]
pub enum AfterDrain {
Stop,
Restart,
Exit {
intent: ExitIntent,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum PendingApply {
InSync,
RestartNeeded {
leaves: Vec<String>,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(tag = "class", rename_all = "snake_case")]
pub enum RetryClass {
Backoff {
next_retry_at: Timestamp,
delay_ms: u64,
},
Hold,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct FailureReport {
pub phase: BootPhase,
pub error: String,
pub at: Timestamp,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(tag = "state", rename_all = "snake_case")]
pub enum LifecycleState {
Resolving {
since: Timestamp,
},
Booting {
attempt: NonZeroU32,
phase: BootPhase,
since: Timestamp,
},
Running {
attempt: NonZeroU32,
since: Timestamp,
apiserver_addr: String,
pending: PendingApply,
},
Draining {
attempt: NonZeroU32,
#[serde(flatten)]
then: AfterDrain,
},
Stopped {
reason: StopReason,
epoch: u64,
since: Timestamp,
},
Failed {
attempt: NonZeroU32,
report: FailureReport,
retry: RetryClass,
},
Wedged {
cause: String,
since: Timestamp,
},
Exiting {
intent: ExitIntent,
},
}
impl LifecycleState {
#[must_use]
pub const fn name(&self) -> &'static str {
match self {
Self::Resolving { .. } => "resolving",
Self::Booting { .. } => "booting",
Self::Running { .. } => "running",
Self::Draining { .. } => "draining",
Self::Stopped { .. } => "stopped",
Self::Failed { .. } => "failed",
Self::Wedged { .. } => "wedged",
Self::Exiting { .. } => "exiting",
}
}
#[must_use]
pub const fn may_hold_store(&self) -> bool {
match self {
Self::Booting { .. }
| Self::Running { .. }
| Self::Draining { .. }
| Self::Wedged { .. } => true,
Self::Resolving { .. }
| Self::Stopped { .. }
| Self::Failed { .. }
| Self::Exiting { .. } => false,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(tag = "outcome", rename_all = "snake_case")]
pub enum StoreOutcome {
Released,
Held {
cause: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "event", rename_all = "snake_case")]
pub enum LifecycleEvent {
HeldAtStartup {
at: Timestamp,
},
Start {
at: Timestamp,
},
Retry {
at: Timestamp,
},
RetryDue {
at: Timestamp,
},
ConfigChanged {
at: Timestamp,
},
ConfigApplied {
pending: PendingApply,
},
Phase {
phase: BootPhase,
at: Timestamp,
},
Booted {
at: Timestamp,
apiserver_addr: String,
},
BootFailed {
report: FailureReport,
class: FailureClass,
store: StoreOutcome,
},
Stop {
at: Timestamp,
},
Restart {
at: Timestamp,
},
Exit {
intent: ExitIntent,
},
Drained {
at: Timestamp,
store: StoreOutcome,
},
}
impl LifecycleEvent {
#[must_use]
pub const fn name(&self) -> &'static str {
match self {
Self::HeldAtStartup { .. } => "held_at_startup",
Self::Start { .. } => "start",
Self::Retry { .. } => "retry",
Self::RetryDue { .. } => "retry_due",
Self::ConfigChanged { .. } => "config_changed",
Self::ConfigApplied { .. } => "config_applied",
Self::Phase { .. } => "phase",
Self::Booted { .. } => "booted",
Self::BootFailed { .. } => "boot_failed",
Self::Stop { .. } => "stop",
Self::Restart { .. } => "restart",
Self::Exit { .. } => "exit",
Self::Drained { .. } => "drained",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LifecycleEffect {
None,
StartBoot {
attempt: NonZeroU32,
},
CancelBoot,
ShutdownRuntime,
Exit(ExitIntent),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RefusedBecause {
RuntimeRunning,
RuntimeNotRunning,
RuntimeNotFailed,
RuntimeNotStopped,
LifecycleBusy,
Wedged,
Unexpected,
}
impl RefusedBecause {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::RuntimeRunning => "runtime_running",
Self::RuntimeNotRunning => "runtime_not_running",
Self::RuntimeNotFailed => "runtime_not_failed",
Self::RuntimeNotStopped => "runtime_not_stopped",
Self::LifecycleBusy => "lifecycle_busy",
Self::Wedged => "wedged",
Self::Unexpected => "unexpected",
}
}
}
impl std::fmt::Display for RefusedBecause {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("{event} refused while {state}: {reason}")]
pub struct Refused {
pub state: &'static str,
pub event: &'static str,
pub reason: RefusedBecause,
}
impl engenho_substrate::ErrorKind for Refused {
fn kind(&self) -> &'static str {
self.reason.as_str()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Lifecycle {
pub state: LifecycleState,
attempts: u32,
epoch: u64,
streak: u32,
}
impl Lifecycle {
#[must_use]
pub const fn at(since: Timestamp) -> Self {
Self {
state: LifecycleState::Resolving { since },
attempts: 0,
epoch: 0,
streak: 0,
}
}
#[must_use]
pub const fn attempts(&self) -> u32 {
self.attempts
}
#[must_use]
pub const fn epoch(&self) -> u64 {
self.epoch
}
#[must_use]
pub const fn streak(&self) -> u32 {
self.streak
}
fn begin_boot(&mut self, at: Timestamp) -> LifecycleEffect {
self.attempts = self.attempts.saturating_add(1);
let attempt = NonZeroU32::new(self.attempts).unwrap_or(NonZeroU32::MAX);
self.state = LifecycleState::Booting {
attempt,
phase: BootPhase::FIRST,
since: at,
};
LifecycleEffect::StartBoot { attempt }
}
fn fail(&mut self, attempt: NonZeroU32, report: &FailureReport, class: FailureClass) {
self.epoch = self.epoch.saturating_add(1);
self.streak = self.streak.saturating_add(1);
let retry = match class {
FailureClass::Backoff => {
let delay = backoff(self.streak);
RetryClass::Backoff {
next_retry_at: report.at.after(delay),
delay_ms: duration_ms(delay),
}
}
FailureClass::Hold => RetryClass::Hold,
};
self.state = LifecycleState::Failed {
attempt,
report: report.clone(),
retry,
};
}
fn rest(&mut self, reason: StopReason, since: Timestamp) {
self.state = LifecycleState::Stopped {
reason,
epoch: self.epoch,
since,
};
}
fn drain(
&mut self,
attempt: NonZeroU32,
then: AfterDrain,
effect: LifecycleEffect,
) -> LifecycleEffect {
self.state = LifecycleState::Draining { attempt, then };
effect
}
fn exit(&mut self, intent: ExitIntent) -> LifecycleEffect {
self.state = LifecycleState::Exiting { intent };
LifecycleEffect::Exit(intent)
}
fn settle(&mut self, then: AfterDrain, store: &StoreOutcome, at: Timestamp) -> LifecycleEffect {
match (then, store) {
(AfterDrain::Exit { intent }, _) => self.exit(intent),
(AfterDrain::Stop, StoreOutcome::Released) => {
self.epoch = self.epoch.saturating_add(1);
self.rest(StopReason::OperatorRequest, at);
LifecycleEffect::None
}
(AfterDrain::Restart, StoreOutcome::Released) => {
self.epoch = self.epoch.saturating_add(1);
self.begin_boot(at)
}
(AfterDrain::Stop | AfterDrain::Restart, StoreOutcome::Held { cause }) => {
self.state = LifecycleState::Wedged {
cause: cause.clone(),
since: at,
};
LifecycleEffect::None
}
}
}
}
fn duration_ms(d: Duration) -> u64 {
u64::try_from(d.as_millis()).unwrap_or(u64::MAX)
}
#[derive(Debug, Default)]
pub struct DaemonLifecycle;
engenho_substrate::define_named!(DaemonLifecycle, "engenho-daemon-lifecycle");
impl StateMachine for DaemonLifecycle {
type State = Lifecycle;
type Event = LifecycleEvent;
type Effect = LifecycleEffect;
type Err = Refused;
fn initial() -> Lifecycle {
Lifecycle::at(Timestamp::now())
}
fn step(
m: &Lifecycle,
event: &LifecycleEvent,
) -> Result<(Lifecycle, LifecycleEffect), Refused> {
Self::starting(m, event)
.or_else(|| Self::booting(m, event))
.or_else(|| Self::stopping(m, event))
.ok_or_else(|| Refused {
state: m.state.name(),
event: event.name(),
reason: Self::refusal(&m.state, event),
})
}
fn is_terminal(m: &Lifecycle) -> bool {
matches!(m.state, LifecycleState::Exiting { .. })
}
}
type Stepped = Option<(Lifecycle, LifecycleEffect)>;
impl DaemonLifecycle {
fn starting(m: &Lifecycle, event: &LifecycleEvent) -> Stepped {
use LifecycleEvent as E;
use LifecycleState as S;
let mut next = m.clone();
let effect = match (&m.state, event) {
(S::Resolving { .. } | S::Stopped { .. }, E::Start { at })
| (S::Failed { .. }, E::Retry { at } | E::ConfigChanged { at })
| (
S::Failed {
retry: RetryClass::Backoff { .. },
..
},
E::RetryDue { at },
) => next.begin_boot(*at),
(S::Resolving { .. }, E::HeldAtStartup { at }) => {
next.rest(StopReason::HeldAtStartup, *at);
LifecycleEffect::None
}
_ => return None,
};
Some((next, effect))
}
fn booting(m: &Lifecycle, event: &LifecycleEvent) -> Stepped {
use LifecycleEvent as E;
use LifecycleState as S;
let mut next = m.clone();
let effect = match (&m.state, event) {
(S::Booting { attempt, .. }, E::Phase { phase, at }) => {
next.state = S::Booting {
attempt: *attempt,
phase: *phase,
since: *at,
};
LifecycleEffect::None
}
(
S::Running {
attempt,
since,
apiserver_addr,
..
},
E::ConfigApplied { pending },
) => {
next.state = S::Running {
attempt: *attempt,
since: *since,
apiserver_addr: apiserver_addr.clone(),
pending: pending.clone(),
};
LifecycleEffect::None
}
(S::Booting { attempt, .. }, E::Booted { at, apiserver_addr }) => {
next.streak = 0;
next.state = S::Running {
attempt: *attempt,
since: *at,
apiserver_addr: apiserver_addr.clone(),
pending: PendingApply::InSync,
};
LifecycleEffect::None
}
(
S::Booting { attempt, .. },
E::BootFailed {
report,
class,
store,
},
) => {
match store {
StoreOutcome::Released => next.fail(*attempt, report, *class),
StoreOutcome::Held { cause } => {
next.state = S::Wedged {
cause: cause.clone(),
since: report.at,
};
}
}
LifecycleEffect::None
}
_ => return None,
};
Some((next, effect))
}
fn stopping(m: &Lifecycle, event: &LifecycleEvent) -> Stepped {
use LifecycleEvent as E;
use LifecycleState as S;
let mut next = m.clone();
let effect = match (&m.state, event) {
(S::Booting { attempt, .. }, E::Stop { .. }) => {
next.drain(*attempt, AfterDrain::Stop, LifecycleEffect::CancelBoot)
}
(S::Booting { attempt, .. }, E::Restart { .. }) => {
next.drain(*attempt, AfterDrain::Restart, LifecycleEffect::CancelBoot)
}
(S::Booting { attempt, .. }, E::Exit { intent }) => next.drain(
*attempt,
AfterDrain::Exit { intent: *intent },
LifecycleEffect::CancelBoot,
),
(S::Running { attempt, .. }, E::Stop { .. }) => {
next.drain(*attempt, AfterDrain::Stop, LifecycleEffect::ShutdownRuntime)
}
(S::Running { attempt, .. }, E::Restart { .. }) => next.drain(
*attempt,
AfterDrain::Restart,
LifecycleEffect::ShutdownRuntime,
),
(S::Running { attempt, .. }, E::Exit { intent }) => next.drain(
*attempt,
AfterDrain::Exit { intent: *intent },
LifecycleEffect::ShutdownRuntime,
),
(S::Failed { .. }, E::Stop { at }) => {
next.rest(StopReason::OperatorRequest, *at);
LifecycleEffect::None
}
(S::Draining { .. }, E::Booted { .. }) => LifecycleEffect::ShutdownRuntime,
(S::Draining { .. }, E::Phase { .. }) => LifecycleEffect::None,
(S::Draining { then, .. }, E::BootFailed { report, store, .. }) => {
next.settle(*then, store, report.at)
}
(S::Draining { then, .. }, E::Drained { at, store }) => next.settle(*then, store, *at),
(S::Draining { attempt, .. }, E::Exit { intent }) => {
next.state = S::Draining {
attempt: *attempt,
then: AfterDrain::Exit { intent: *intent },
};
LifecycleEffect::None
}
(
S::Resolving { .. } | S::Stopped { .. } | S::Failed { .. } | S::Wedged { .. },
E::Exit { intent },
) => next.exit(*intent),
_ => return None,
};
Some((next, effect))
}
fn refusal(state: &LifecycleState, event: &LifecycleEvent) -> RefusedBecause {
use LifecycleEvent as E;
use LifecycleState as S;
match (state, event) {
(S::Wedged { .. }, _) => RefusedBecause::Wedged,
(S::Running { .. }, E::Start { .. }) => RefusedBecause::RuntimeRunning,
(S::Booting { .. } | S::Draining { .. }, E::Start { .. })
| (S::Draining { .. }, E::Stop { .. } | E::Restart { .. }) => {
RefusedBecause::LifecycleBusy
}
(S::Failed { .. }, E::Start { .. }) => RefusedBecause::RuntimeNotStopped,
(_, E::Retry { .. }) => RefusedBecause::RuntimeNotFailed,
(S::Resolving { .. } | S::Stopped { .. }, E::Stop { .. } | E::Restart { .. })
| (S::Failed { .. }, E::Restart { .. })
| (_, E::ConfigApplied { .. }) => RefusedBecause::RuntimeNotRunning,
_ => RefusedBecause::Unexpected,
}
}
}
#[cfg(test)]
mod tests;