use std::num::{NonZeroU32, NonZeroUsize};
use std::path::PathBuf;
use std::pin::Pin;
use std::sync::Arc;
use std::time::Duration;
use engenho_config::{ConfigError, EngenhoConfig, OverrideLayer, ProvenanceMap};
use engenho_kubelet::ContainerRuntime;
use engenho_serve::{StopHandle, stop_channel};
use engenho_store::data_dir_lock::{DataDirLock, LockError};
use engenho_substrate::maquina::{MachineError, MachineRunner};
use engenho_substrate::relogio::WallClock;
use serde::{Deserialize, Serialize};
use tokio::sync::{mpsc, oneshot, watch};
use tokio::task::{JoinError, JoinHandle};
use tracing::{debug, error, info, warn};
use super::control_dir::ControlDir;
use super::journal::{
BootAttempt, BootJournal, IdentityRecord, LastSeen, PhaseRecord, PreviousRun, RunMarker,
};
use super::machine::{
DaemonLifecycle, ExitIntent, FailureReport, Lifecycle, LifecycleEffect, LifecycleEvent,
LifecycleState, PendingApply, Refused, RefusedBecause, RetryClass, StoreOutcome,
};
use crate::boot::{BootKind, BootPhase, BootProgress, BootRecorder, FailureClass, Timestamp};
use crate::child::{Child, ChildState, DeadChild, Death, DeathCause, RespawnError, Respawned};
use crate::control::apply::ApplyEffect;
use crate::control::overrides::OverrideStore;
use crate::control::reinit::{self, MovedAside, Reinit, ReinitOp};
use crate::control::ring::Ring;
use crate::error::RuntimeError;
use crate::layout::Area;
use crate::publish::PublishRecord;
use crate::release::{BootFailed, BootUnwind, StoreReleased};
use crate::runtime::Runtime;
pub type Fold =
Arc<dyn Fn(Option<&OverrideLayer>) -> Result<ResolvedConfig, ConfigError> + Send + Sync>;
#[derive(Clone)]
pub struct ConfigSource {
fold: Fold,
overrides: Option<Arc<OverrideStore>>,
}
impl ConfigSource {
pub fn new(
resolve: impl Fn() -> Result<ResolvedConfig, ConfigError> + Send + Sync + 'static,
) -> Self {
Self {
fold: Arc::new(move |_| resolve()),
overrides: None,
}
}
pub fn layered(
fold: impl Fn(Option<&OverrideLayer>) -> Result<ResolvedConfig, ConfigError>
+ Send
+ Sync
+ 'static,
overrides: Arc<OverrideStore>,
) -> Self {
Self {
fold: Arc::new(fold),
overrides: Some(overrides),
}
}
pub fn resolve(&self) -> Result<ResolvedConfig, ConfigError> {
let layer = self.overrides.as_ref().map(|o| o.layer()).transpose()?;
(self.fold)(layer.as_ref())
}
pub fn resolve_with(
&self,
overrides: Option<&OverrideLayer>,
) -> Result<ResolvedConfig, ConfigError> {
(self.fold)(overrides)
}
#[must_use]
pub fn overrides(&self) -> Option<&Arc<OverrideStore>> {
self.overrides.as_ref()
}
}
#[derive(Debug, Clone)]
pub struct ResolvedConfig {
pub config: EngenhoConfig,
pub provenance: Option<ProvenanceMap>,
}
impl ResolvedConfig {
#[must_use]
pub const fn untracked(config: EngenhoConfig) -> Self {
Self {
config,
provenance: None,
}
}
}
const HISTORY_CAP: NonZeroUsize = match NonZeroUsize::new(64) {
Some(cap) => cap,
None => NonZeroUsize::MIN,
};
pub struct SupervisorConfig {
pub data_dir: PathBuf,
pub source: ConfigSource,
pub backend: Option<Arc<dyn ContainerRuntime>>,
pub declared: Option<PathBuf>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Hold {
None,
AcrossRelaunch,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct DaemonInfo {
pub version: String,
pub pid: u32,
pub started_at: Timestamp,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Snapshot {
pub lifecycle: LifecycleState,
pub epoch: u64,
pub attempts: Vec<BootAttempt>,
pub previous_run: PreviousRun,
pub identity: Option<IdentityRecord>,
pub daemon: DaemonInfo,
pub data_dir: PathBuf,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Accepted {
pub accepted_at: Timestamp,
pub lifecycle: LifecycleState,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct StopDone {
pub epoch: u64,
pub lifecycle: LifecycleState,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum CommandError {
#[error(transparent)]
Refused(#[from] Refused),
#[error("the supervisor has ended")]
Gone,
}
#[derive(Debug, thiserror::Error)]
pub enum SupervisorError {
#[error("cannot create the control directory {}: {source}", path.display())]
ControlDir {
path: PathBuf,
source: std::io::Error,
},
#[error(transparent)]
Locked(#[from] LockError),
}
type Reply<T> = oneshot::Sender<Result<T, Refused>>;
enum Request {
Start(Reply<Accepted>),
Stop(Hold, Reply<StopDone>),
Restart(Reply<Accepted>),
Retry(Reply<Accepted>),
Exit(ExitIntent, Reply<Accepted>),
Inspect(oneshot::Sender<Inspection>),
Reconfigure {
restart_now: bool,
reply: oneshot::Sender<Result<Reconfigured, ReconfigureError>>,
},
Republish(oneshot::Sender<Result<Vec<PublishRecord>, ReconfigureError>>),
RestartChild {
child: Child,
reply: oneshot::Sender<Result<Respawned, RestartChildError>>,
},
Reinit {
reinit: Reinit,
epoch: Option<u64>,
reply: oneshot::Sender<Result<MovedAside, ReinitRefused>>,
},
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ReinitRefused {
#[error("the runtime must be stopped first")]
NotStopped,
#[error("the runtime ran since the challenge was prepared (stop epoch {bound}, now {now})")]
EpochMoved {
bound: u64,
now: u64,
},
#[error("the store is held: {0}")]
StoreHeld(String),
#[error("{0}")]
Failed(String),
#[error("the supervisor has ended")]
Gone,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Reconfigured {
Running {
republished: bool,
respawned: Vec<Child>,
pending: Vec<engenho_config::LeafPath>,
restarted: bool,
},
Retrying,
NotRunning,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ReconfigureError {
#[error("the configuration does not resolve: {0}")]
Config(String),
#[error("the running runtime could not apply it: {0}")]
Runtime(String),
#[error("no runtime is running")]
NotRunning,
#[error("the supervisor has ended")]
Gone,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum RestartChildError {
#[error(transparent)]
Refused(#[from] RespawnError),
#[error("no runtime is running")]
NotRunning,
#[error("the supervisor has ended")]
Gone,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum StoreLock {
HeldByThisDaemon,
HeldByOtherProcess,
Free,
Unknown,
}
#[derive(Debug, Clone)]
pub struct RuntimeFacts {
pub config: EngenhoConfig,
pub provenance: Option<ProvenanceMap>,
pub declared_digest: Option<String>,
pub apiserver_addr: std::net::SocketAddr,
pub boot_kind: BootKind,
pub revision: u64,
pub leader: bool,
pub children: Vec<ChildFact>,
pub publish: Vec<PublishRecord>,
pub server_sans: Option<Vec<String>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ChildFact {
pub child: Child,
pub state: ChildState,
pub spawned_at: Timestamp,
pub ended_at: Option<Timestamp>,
pub generation: u64,
pub last_death: Option<Death>,
}
#[derive(Debug, Clone)]
pub struct Inspection {
pub runtime: Option<RuntimeFacts>,
pub store_lock: StoreLock,
pub store_present: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DaemonEvent {
Lifecycle(LifecycleState),
BootPhase {
attempt: NonZeroU32,
record: PhaseRecord,
},
ChildDied {
child: Child,
cause: DeathCause,
},
ChildRespawned {
child: Child,
generation: u64,
},
ReinitExecuted {
operation: ReinitOp,
},
ConfigApplied {
generation: u64,
leaves: Vec<engenho_config::LeafPath>,
effect: ApplyEffect,
},
KubeconfigPublished {
record: PublishRecord,
},
}
pub const EVENTS: usize = 4096;
#[derive(Clone)]
pub struct SupervisorHandle {
requests: mpsc::Sender<Request>,
snapshot: watch::Receiver<Snapshot>,
events: Arc<Ring<DaemonEvent>>,
declared_changes: watch::Receiver<u64>,
}
impl SupervisorHandle {
#[must_use]
pub fn declared_changes(&self) -> watch::Receiver<u64> {
self.declared_changes.clone()
}
#[must_use]
pub fn events(&self) -> &Arc<Ring<DaemonEvent>> {
&self.events
}
pub async fn inspect(&self) -> Result<Inspection, CommandError> {
let (reply, answer) = oneshot::channel();
self.requests
.send(Request::Inspect(reply))
.await
.map_err(|_| CommandError::Gone)?;
answer.await.map_err(|_| CommandError::Gone)
}
#[must_use]
pub fn snapshot(&self) -> Snapshot {
self.snapshot.borrow().clone()
}
#[must_use]
pub fn watch(&self) -> watch::Receiver<Snapshot> {
self.snapshot.clone()
}
pub async fn start(&self) -> Result<Accepted, CommandError> {
self.ask(Request::Start).await
}
pub async fn stop(&self, hold: Hold) -> Result<StopDone, CommandError> {
self.ask(|reply| Request::Stop(hold, reply)).await
}
pub async fn restart(&self) -> Result<Accepted, CommandError> {
self.ask(Request::Restart).await
}
pub async fn retry(&self) -> Result<Accepted, CommandError> {
self.ask(Request::Retry).await
}
pub async fn exit(&self, intent: ExitIntent) -> Result<Accepted, CommandError> {
self.ask(|reply| Request::Exit(intent, reply)).await
}
pub async fn reconfigure(&self, restart_now: bool) -> Result<Reconfigured, ReconfigureError> {
let (reply, answer) = oneshot::channel();
self.requests
.send(Request::Reconfigure { restart_now, reply })
.await
.map_err(|_| ReconfigureError::Gone)?;
answer.await.map_err(|_| ReconfigureError::Gone)?
}
pub async fn republish(&self) -> Result<Vec<PublishRecord>, ReconfigureError> {
let (reply, answer) = oneshot::channel();
self.requests
.send(Request::Republish(reply))
.await
.map_err(|_| ReconfigureError::Gone)?;
answer.await.map_err(|_| ReconfigureError::Gone)?
}
pub async fn restart_child(&self, child: Child) -> Result<Respawned, RestartChildError> {
let (reply, answer) = oneshot::channel();
self.requests
.send(Request::RestartChild { child, reply })
.await
.map_err(|_| RestartChildError::Gone)?;
answer.await.map_err(|_| RestartChildError::Gone)?
}
pub async fn reinit(
&self,
reinit: Reinit,
epoch: Option<u64>,
) -> Result<MovedAside, ReinitRefused> {
let (reply, answer) = oneshot::channel();
self.requests
.send(Request::Reinit {
reinit,
epoch,
reply,
})
.await
.map_err(|_| ReinitRefused::Gone)?;
answer.await.map_err(|_| ReinitRefused::Gone)?
}
async fn ask<T>(&self, request: impl FnOnce(Reply<T>) -> Request) -> Result<T, CommandError> {
let (reply, answer) = oneshot::channel();
self.requests
.send(request(reply))
.await
.map_err(|_| CommandError::Gone)?;
answer
.await
.map_err(|_| CommandError::Gone)?
.map_err(CommandError::Refused)
}
}
enum Slot {
Idle(Option<StoreReleased>),
Booting {
task: JoinHandle<BootOutcome>,
cancel: StopHandle,
progress: mpsc::UnboundedReceiver<BootProgress>,
},
Up(Box<Running>),
Draining(JoinHandle<Result<StoreReleased, RuntimeError>>),
Wedged,
}
struct Running {
runtime: Runtime,
provenance: Option<ProvenanceMap>,
declared_digest: Option<String>,
}
struct BootOutcome {
result: Result<Runtime, BootFailed>,
provenance: Option<ProvenanceMap>,
declared_digest: Option<String>,
}
enum SlotEvent {
Progress(BootProgress),
BootDone(Box<Result<BootOutcome, JoinError>>),
Drained(Result<Result<StoreReleased, RuntimeError>, JoinError>),
ChildDied(DeadChild),
}
async fn slot_event(slot: &mut Slot) -> SlotEvent {
match slot {
Slot::Booting { task, progress, .. } => tokio::select! {
biased;
Some(report) = progress.recv() => SlotEvent::Progress(report),
done = task => SlotEvent::BootDone(Box::new(done)),
},
Slot::Up(running) => SlotEvent::ChildDied(running.runtime.next_dead_child().await),
Slot::Draining(task) => SlotEvent::Drained(task.await),
Slot::Idle(_) | Slot::Wedged => std::future::pending().await,
}
}
async fn retry_due(timer: &mut Option<(NonZeroU32, Pin<Box<tokio::time::Sleep>>)>) {
match timer {
Some((_, sleep)) => sleep.as_mut().await,
None => std::future::pending().await,
}
}
pub struct Supervisor {
data_dir: PathBuf,
control: ControlDir,
_lock: DataDirLock,
source: ConfigSource,
backend: Option<Arc<dyn ContainerRuntime>>,
machine: MachineRunner<DaemonLifecycle>,
slot: Slot,
journal: BootJournal,
marker: RunMarker,
previous_run: PreviousRun,
identity: Option<IdentityRecord>,
daemon: DaemonInfo,
retry: Option<(NonZeroU32, Pin<Box<tokio::time::Sleep>>)>,
pending_stops: Vec<Reply<StopDone>>,
requests: mpsc::Receiver<Request>,
snapshot: watch::Sender<Snapshot>,
events: Arc<Ring<DaemonEvent>>,
declared: mpsc::UnboundedReceiver<()>,
declared_path: Option<PathBuf>,
_watcher: Option<shikumi::ConfigWatcher>,
}
impl Supervisor {
pub fn new(config: SupervisorConfig) -> Result<(Self, SupervisorHandle), SupervisorError> {
let SupervisorConfig {
data_dir,
source,
backend,
declared,
} = config;
let control = ControlDir::under(&data_dir);
control
.create()
.map_err(|source| SupervisorError::ControlDir {
path: control.root().to_path_buf(),
source,
})?;
let lock = control.lock()?;
let started_at = Timestamp::now();
let previous_run = PreviousRun::from_marker(control.read_run().as_ref());
let marker = RunMarker::Released { at: started_at };
if let Err(err) = control.write_run(&marker) {
warn!(error = %err, "cannot record the run marker");
}
let machine = MachineRunner::<DaemonLifecycle>::from_state(
Lifecycle::at(started_at),
Arc::new(WallClock),
)
.with_history_cap(HISTORY_CAP);
let daemon = DaemonInfo {
version: env!("CARGO_PKG_VERSION").to_owned(),
pid: std::process::id(),
started_at,
};
let (declared_tx, declared_rx) = mpsc::unbounded_channel();
let (declared_count, declared_changes) = watch::channel(0u64);
let watcher = declared
.as_deref()
.and_then(|path| watch_declared(path, declared_tx, declared_count));
let journal = control.read_journal();
let identity = control.read_identity();
let (requests_tx, requests) = mpsc::channel(32);
let first = Snapshot {
lifecycle: machine.state().state.clone(),
epoch: 0,
attempts: journal.attempts(),
previous_run: previous_run.clone(),
identity: identity.clone(),
daemon: daemon.clone(),
data_dir: data_dir.clone(),
};
let (snapshot, snapshot_rx) = watch::channel(first);
let events = Arc::new(Ring::new(EVENTS));
info!(
data_dir = %data_dir.display(),
previous_run = ?previous_run,
"supervisor up"
);
let supervisor = Self {
data_dir,
control,
_lock: lock,
source,
backend,
machine,
slot: Slot::Idle(None),
journal,
marker,
previous_run,
identity,
daemon,
retry: None,
pending_stops: Vec::new(),
requests,
snapshot,
events: Arc::clone(&events),
declared: declared_rx,
declared_path: declared,
_watcher: watcher,
};
let handle = SupervisorHandle {
requests: requests_tx,
snapshot: snapshot_rx,
events,
declared_changes,
};
Ok((supervisor, handle))
}
pub async fn run(mut self) -> ExitIntent {
let at = Timestamp::now();
let first = if self.control.is_held() {
info!("hold marker present: staying stopped until started");
LifecycleEvent::HeldAtStartup { at }
} else {
LifecycleEvent::Start { at }
};
self.apply_logged(first);
let mut requests_open = true;
loop {
if let LifecycleState::Exiting { intent } = self.machine.state().state {
self.finish();
return intent;
}
tokio::select! {
request = self.requests.recv(), if requests_open => match request {
Some(Request::Inspect(reply)) => {
let _ = reply.send(self.inspect().await);
}
Some(Request::Reconfigure { restart_now, reply }) => {
let _ = reply.send(self.reconfigure(restart_now, Timestamp::now()).await);
}
Some(Request::RestartChild { child, reply }) => {
let _ = reply.send(self.restart_child(child).await);
}
Some(request) => self.on_request(request),
None => requests_open = false,
},
event = slot_event(&mut self.slot) => self.on_slot_event(event),
() = retry_due(&mut self.retry) => {
self.retry = None;
self.apply_logged(LifecycleEvent::RetryDue { at: Timestamp::now() });
}
Some(()) = self.declared.recv() => {
self.apply_logged(LifecycleEvent::ConfigChanged { at: Timestamp::now() });
}
}
}
}
fn on_request(&mut self, request: Request) {
let at = Timestamp::now();
match request {
Request::Start(reply) => {
let result = self.apply(LifecycleEvent::Start { at });
if result.is_ok() {
self.write_hold(Hold::None, at);
}
let _ = reply.send(result.map(|()| self.accepted(at)));
}
Request::Stop(hold, reply) => match self.apply(LifecycleEvent::Stop { at }) {
Ok(()) => {
self.write_hold(hold, at);
self.pending_stops.push(reply);
self.answer_stops();
}
Err(refused) => {
let _ = reply.send(Err(refused));
}
},
Request::Restart(reply) => {
let result = self.apply(LifecycleEvent::Restart { at });
let _ = reply.send(result.map(|()| self.accepted(at)));
}
Request::Retry(reply) => {
let result = self.apply(LifecycleEvent::Retry { at });
let _ = reply.send(result.map(|()| self.accepted(at)));
}
Request::Exit(intent, reply) => {
let result = self.apply(LifecycleEvent::Exit { intent });
let _ = reply.send(result.map(|()| self.accepted(at)));
}
Request::Republish(reply) => {
let _ = reply.send(self.republish());
}
Request::Reinit {
reinit,
epoch,
reply,
} => {
let _ = reply.send(self.reinit(reinit, epoch, at));
}
Request::Inspect(_) | Request::Reconfigure { .. } | Request::RestartChild { .. } => {}
}
}
async fn restart_child(&mut self, child: Child) -> Result<Respawned, RestartChildError> {
let Slot::Up(running) = &mut self.slot else {
return Err(RestartChildError::NotRunning);
};
let respawned = running.runtime.respawn(child).await?;
self.report_children(&respawned.rebuilt, &respawned.died);
Ok(respawned)
}
fn report_children(&self, changed: &[Child], died: &[DeadChild]) {
for dead in died {
self.events.push(DaemonEvent::ChildDied {
child: dead.child,
cause: dead.cause,
});
}
let Slot::Up(running) = &self.slot else {
return;
};
let children = running.runtime.children();
for &child in changed {
if let Some(handle) = children
.get(child)
.filter(|h| h.state() == ChildState::Running)
{
self.events.push(DaemonEvent::ChildRespawned {
child,
generation: handle.generation(),
});
}
}
}
async fn reconfigure(
&mut self,
restart_now: bool,
at: Timestamp,
) -> Result<Reconfigured, ReconfigureError> {
match &self.machine.state().state {
LifecycleState::Running { .. } => {}
LifecycleState::Failed {
retry: RetryClass::Hold,
..
} => {
self.apply_logged(LifecycleEvent::ConfigChanged { at });
return Ok(Reconfigured::Retrying);
}
_ => return Ok(Reconfigured::NotRunning),
}
let resolved = self
.source
.resolve()
.map_err(|e| ReconfigureError::Config(e.to_string()))?;
let Slot::Up(running) = &mut self.slot else {
return Ok(Reconfigured::NotRunning);
};
let adopted = running
.runtime
.adopt(&resolved.config)
.await
.map_err(|e| ReconfigureError::Runtime(e.to_string()))?;
let pending =
crate::control::apply::restart_pending(running.runtime.config(), &resolved.config);
running.provenance = resolved.provenance;
let republished = adopted.published.is_some();
for record in adopted.published.into_iter().flatten() {
self.events
.push(DaemonEvent::KubeconfigPublished { record });
}
let respawned = adopted.children.changed;
self.report_children(&respawned, &adopted.children.died);
self.apply_logged(LifecycleEvent::ConfigApplied {
pending: if pending.is_empty() {
PendingApply::InSync
} else {
PendingApply::RestartNeeded {
leaves: pending.iter().map(ToString::to_string).collect(),
}
},
});
let restarted = restart_now
&& !pending.is_empty()
&& self.apply(LifecycleEvent::Restart { at }).is_ok();
if restarted {
info!(leaves = ?pending, "restarting the runtime for a configuration change");
}
Ok(Reconfigured::Running {
republished,
respawned,
pending,
restarted,
})
}
fn reinit(
&mut self,
reinit: Reinit,
epoch: Option<u64>,
at: Timestamp,
) -> Result<MovedAside, ReinitRefused> {
let _store_lock = if reinit.needs_stopped() {
self.resting_in(epoch)?
} else {
None
};
let moved = reinit::move_aside(&self.data_dir, reinit, at)
.map_err(|e| ReinitRefused::Failed(e.to_string()))?;
if reinit == Reinit::RotateAdminToken {
crate::runtime::load_or_generate_admin_token(&self.data_dir).map_err(|e| {
ReinitRefused::Failed(
[
"the old token is in ",
moved.attic.display().to_string().as_str(),
", and a new one could not be written: ",
e.to_string().as_str(),
]
.concat(),
)
})?;
}
info!(
operation = reinit.op().name(),
attic = %moved.attic.display(),
moved = ?moved.moved,
"re-initialized"
);
self.events.push(DaemonEvent::ReinitExecuted {
operation: reinit.op(),
});
Ok(moved)
}
fn resting_in(&self, epoch: Option<u64>) -> Result<Option<DataDirLock>, ReinitRefused> {
let resting = matches!(
self.machine.state().state,
LifecycleState::Stopped { .. } | LifecycleState::Failed { .. }
) && matches!(self.slot, Slot::Idle(_));
if !resting {
return Err(ReinitRefused::NotStopped);
}
let now = self.machine.state().epoch();
if let Some(bound) = epoch.filter(|bound| *bound != now) {
return Err(ReinitRefused::EpochMoved { bound, now });
}
let store = Area::Store.path(&self.data_dir);
if !store.exists() {
return Ok(None);
}
DataDirLock::acquire(&store)
.map(Some)
.map_err(|e| ReinitRefused::StoreHeld(e.to_string()))
}
fn republish(&mut self) -> Result<Vec<PublishRecord>, ReconfigureError> {
let Slot::Up(running) = &mut self.slot else {
return Err(ReconfigureError::NotRunning);
};
let records = running
.runtime
.republish()
.map_err(|e| ReconfigureError::Runtime(e.to_string()))?;
for record in &records {
self.events.push(DaemonEvent::KubeconfigPublished {
record: record.clone(),
});
}
Ok(records)
}
async fn inspect(&self) -> Inspection {
let store_dir = self.data_dir.join(crate::runtime::STORE_DIR);
let store_present = store_dir.exists();
let (runtime, store_lock) = match &self.slot {
Slot::Up(running) => {
let rt = &running.runtime;
let (revision, leader) = rt.store_position().await;
let durable = rt.config().runtime.durable;
let facts = RuntimeFacts {
config: rt.config().clone(),
provenance: running.provenance.clone(),
declared_digest: running.declared_digest.clone(),
apiserver_addr: rt.local_addr(),
boot_kind: rt.boot_kind(),
revision,
leader,
children: rt
.children()
.iter()
.map(|(child, handle)| ChildFact {
child,
state: handle.state(),
spawned_at: handle.spawned_at(),
ended_at: handle.ended_at(),
generation: handle.generation(),
last_death: handle.last_death(),
})
.collect(),
publish: rt.publish_records().to_vec(),
server_sans: rt.server_sans().map(<[String]>::to_vec),
};
let lock = if durable {
StoreLock::HeldByThisDaemon
} else {
StoreLock::Free
};
(Some(facts), lock)
}
Slot::Booting { .. } | Slot::Draining(_) | Slot::Wedged => {
(None, StoreLock::HeldByThisDaemon)
}
Slot::Idle(_) => (
None,
match DataDirLock::acquire(&store_dir) {
Ok(_) => StoreLock::Free,
Err(LockError::Held { .. }) => StoreLock::HeldByOtherProcess,
Err(LockError::Unusable { .. }) => StoreLock::Unknown,
},
),
};
Inspection {
runtime,
store_lock,
store_present,
}
}
fn on_slot_event(&mut self, event: SlotEvent) {
match event {
SlotEvent::Progress(BootProgress::Entered { phase, at }) => {
self.journal.entered(phase, at);
self.persist_journal();
self.publish_phases(2);
self.apply_logged(LifecycleEvent::Phase { phase, at });
}
SlotEvent::Progress(BootProgress::Kind(kind)) => {
self.journal.observed(kind);
self.persist_journal();
}
SlotEvent::BootDone(done) => self.on_boot_done(*done),
SlotEvent::Drained(done) => self.on_drained(done),
SlotEvent::ChildDied(dead) => {
self.events.push(DaemonEvent::ChildDied {
child: dead.child,
cause: dead.cause,
});
}
}
}
fn publish_phases(&self, n: usize) {
let Some(latest) = self.journal.latest() else {
return;
};
let start = latest.phases.len().saturating_sub(n);
for record in &latest.phases[start..] {
self.events.push(DaemonEvent::BootPhase {
attempt: latest.attempt,
record: record.clone(),
});
}
}
fn on_boot_done(&mut self, done: Result<BootOutcome, JoinError>) {
let at = Timestamp::now();
let read_by_attempt = match &done {
Ok(outcome) => outcome.declared_digest.clone(),
Err(_) => None,
};
let event = match done {
Ok(BootOutcome {
result: Ok(runtime),
provenance,
declared_digest,
}) => {
let apiserver_addr = runtime.local_addr().to_string();
if runtime.boot_kind() == BootKind::FirstBoot {
self.record_identity(runtime.config(), at);
}
self.journal.succeeded();
self.publish_phases(1);
info!(addr = %apiserver_addr, "engenho up — apiserver bound");
self.slot = Slot::Up(Box::new(Running {
runtime,
provenance,
declared_digest,
}));
LifecycleEvent::Booted { at, apiserver_addr }
}
Ok(BootOutcome {
result:
Err(BootFailed {
error,
phase,
unwind,
}),
..
}) => {
let class = FailureClass::of(&error);
let rendered = error.to_string();
let store = match unwind {
BootUnwind::NeverOpened => {
self.slot = Slot::Idle(None);
StoreOutcome::Released
}
BootUnwind::Released(released) => {
self.slot = Slot::Idle(Some(released));
StoreOutcome::Released
}
BootUnwind::StillShared { strong_count } => {
self.slot = Slot::Wedged;
StoreOutcome::Held {
cause: format!(
"{strong_count} holders of the store remained after the failed boot unwound"
),
}
}
BootUnwind::TerminateFailed(err) => {
self.slot = Slot::Wedged;
StoreOutcome::Held {
cause: format!("terminating the failed boot's store failed: {err}"),
}
}
};
if matches!(error, RuntimeError::BootCancelled { .. }) {
self.journal.cancelled();
info!(%phase, "boot cancelled");
} else {
self.journal.failed(&rendered);
warn!(%phase, ?class, error = %rendered, "boot failed");
}
self.publish_phases(1);
LifecycleEvent::BootFailed {
report: FailureReport {
phase,
error: rendered,
at,
},
class,
store,
}
}
Err(join) => {
let phase = match self.machine.state().state {
LifecycleState::Booting { phase, .. } => phase,
_ => BootPhase::FIRST,
};
let rendered = format!("the boot task ended abnormally: {join}");
error!(%phase, error = %rendered, "boot failed");
self.journal.failed(&rendered);
self.slot = Slot::Wedged;
LifecycleEvent::BootFailed {
report: FailureReport {
phase,
error: rendered,
at,
},
class: FailureClass::Hold,
store: StoreOutcome::Held {
cause: "the boot task panicked".into(),
},
}
}
};
self.persist_journal();
self.apply_logged(event);
if matches!(self.machine.state().state, LifecycleState::Failed { .. }) {
self.resettle_declared(read_by_attempt.as_deref(), at);
}
}
fn resettle_declared(&mut self, read_by_attempt: Option<&str>, at: Timestamp) {
let Some(path) = self.declared_path.clone() else {
return;
};
if file_digest(&path).as_deref() != read_by_attempt {
debug!(path = %path.display(), "the declared file moved under a failed boot — retrying");
self.apply_logged(LifecycleEvent::ConfigChanged { at });
}
}
fn on_drained(&mut self, done: Result<Result<StoreReleased, RuntimeError>, JoinError>) {
let store = match done {
Ok(Ok(released)) => {
self.slot = Slot::Idle(Some(released));
info!("engenho stopped cleanly");
StoreOutcome::Released
}
Ok(Err(err)) => {
self.slot = Slot::Wedged;
error!(error = %err, "the runtime did not stop cleanly");
StoreOutcome::Held {
cause: err.to_string(),
}
}
Err(join) => {
self.slot = Slot::Wedged;
error!(error = %join, "the runtime's stop ended abnormally");
StoreOutcome::Held {
cause: format!("the runtime's stop ended abnormally: {join}"),
}
}
};
self.apply_logged(LifecycleEvent::Drained {
at: Timestamp::now(),
store,
});
}
fn apply(&mut self, event: LifecycleEvent) -> Result<(), Refused> {
let state = self.machine.state().state.name();
let name = event.name();
let effect = self.machine.step(event).map_err(|err| match err {
MachineError::Step(refused) => refused,
MachineError::Terminal => Refused {
state,
event: name,
reason: RefusedBecause::Unexpected,
},
})?;
self.perform(effect);
self.after_transition();
Ok(())
}
fn apply_logged(&mut self, event: LifecycleEvent) {
if let Err(refused) = self.apply(event) {
debug!(%refused, "lifecycle event refused");
}
}
fn perform(&mut self, effect: LifecycleEffect) {
match effect {
LifecycleEffect::None | LifecycleEffect::Exit(_) => {}
LifecycleEffect::StartBoot { attempt } => self.start_boot(attempt),
LifecycleEffect::CancelBoot => {
if let Slot::Booting { cancel, .. } = &self.slot {
cancel.stop();
}
}
LifecycleEffect::ShutdownRuntime => {
match std::mem::replace(&mut self.slot, Slot::Idle(None)) {
Slot::Up(running) => {
info!("stopping the runtime");
self.slot = Slot::Draining(tokio::spawn(running.runtime.shutdown()));
}
other => {
self.slot = other;
error!("the lifecycle asked to stop a runtime that is not up");
}
}
}
}
}
fn start_boot(&mut self, attempt: NonZeroU32) {
let Slot::Idle(_released) = std::mem::replace(&mut self.slot, Slot::Idle(None)) else {
error!(
attempt = attempt.get(),
"the lifecycle started a boot while the slot was not idle"
);
return;
};
let (cancel, signal) = stop_channel();
let (progress_tx, progress) = mpsc::unbounded_channel();
let rec = BootRecorder::new(progress_tx, signal);
let task = tokio::spawn(supervised_boot(
self.source.clone(),
self.backend.clone(),
self.data_dir.clone(),
self.declared_path.clone(),
rec,
));
self.slot = Slot::Booting {
task,
cancel,
progress,
};
let since = match &self.machine.state().state {
LifecycleState::Booting { since, .. } => *since,
_ => Timestamp::now(),
};
self.journal.begin(attempt, since);
self.persist_journal();
info!(attempt = attempt.get(), "booting");
}
fn after_transition(&mut self) {
self.sync_retry_timer();
self.sync_run_marker();
self.answer_stops();
self.publish();
}
fn sync_retry_timer(&mut self) {
match &self.machine.state().state {
LifecycleState::Failed {
attempt,
retry: RetryClass::Backoff { delay_ms, .. },
..
} => {
if self.retry.as_ref().map(|(a, _)| *a) != Some(*attempt) {
let sleep = tokio::time::sleep(Duration::from_millis(*delay_ms));
self.retry = Some((*attempt, Box::pin(sleep)));
}
}
_ => self.retry = None,
}
}
fn sync_run_marker(&mut self) {
let live = |last_seen| RunMarker::Live {
last_seen,
pid: std::process::id(),
};
let wanted = match &self.machine.state().state {
LifecycleState::Booting { phase, .. } => live(LastSeen::Booting { phase: *phase }),
LifecycleState::Running { .. } => live(LastSeen::Running),
LifecycleState::Draining { .. } => live(LastSeen::Draining),
LifecycleState::Wedged { .. } => return,
LifecycleState::Exiting { .. } if matches!(self.slot, Slot::Wedged) => return,
LifecycleState::Resolving { .. }
| LifecycleState::Stopped { .. }
| LifecycleState::Failed { .. }
| LifecycleState::Exiting { .. } => {
if matches!(self.marker, RunMarker::Released { .. }) {
return;
}
RunMarker::Released {
at: Timestamp::now(),
}
}
};
if wanted != self.marker {
if let Err(err) = self.control.write_run(&wanted) {
warn!(error = %err, "cannot record the run marker");
}
self.marker = wanted;
}
}
fn answer_stops(&mut self) {
let state = &self.machine.state().state;
if matches!(state, LifecycleState::Draining { .. }) {
return;
}
let done = StopDone {
epoch: self.machine.state().epoch(),
lifecycle: state.clone(),
};
for reply in self.pending_stops.drain(..) {
let _ = reply.send(Ok(done.clone()));
}
}
fn publish(&self) {
let lifecycle = self.machine.state();
if self.snapshot.borrow().lifecycle != lifecycle.state {
self.events
.push(DaemonEvent::Lifecycle(lifecycle.state.clone()));
}
self.snapshot.send_replace(Snapshot {
lifecycle: lifecycle.state.clone(),
epoch: lifecycle.epoch(),
attempts: self.journal.attempts(),
previous_run: self.previous_run.clone(),
identity: self.identity.clone(),
daemon: self.daemon.clone(),
data_dir: self.data_dir.clone(),
});
}
fn accepted(&self, at: Timestamp) -> Accepted {
Accepted {
accepted_at: at,
lifecycle: self.machine.state().state.clone(),
}
}
fn write_hold(&self, hold: Hold, at: Timestamp) {
let written = match hold {
Hold::None => self.control.clear_hold(),
Hold::AcrossRelaunch => self.control.set_hold(at),
};
if let Err(err) = written {
warn!(error = %err, "cannot update the hold marker");
}
}
fn record_identity(&mut self, config: &EngenhoConfig, at: Timestamp) {
let identity = IdentityRecord {
cluster_name: config.cluster.name.clone(),
node_name: config.runtime.node_name.clone(),
recorded_at: at,
};
if let Err(err) = self.control.write_identity(&identity) {
warn!(error = %err, "cannot record the first-boot identity");
}
self.identity = Some(identity);
}
fn persist_journal(&self) {
if let Err(err) = self.control.write_journal(&self.journal) {
warn!(error = %err, "cannot persist the boot journal");
}
}
fn finish(&mut self) {
self.sync_run_marker();
self.persist_journal();
self.publish();
}
}
fn watch_declared(
path: &std::path::Path,
changed: mpsc::UnboundedSender<()>,
count: watch::Sender<u64>,
) -> Option<shikumi::ConfigWatcher> {
let started = shikumi::ConfigWatcher::watch(path, move |event| {
if shikumi::WatchEventClass::classify(&event.kind) == shikumi::WatchEventClass::Reload {
let _ = changed.send(());
count.send_modify(|n| *n = n.wrapping_add(1));
}
});
match started {
Ok(watcher) => Some(watcher),
Err(err) => {
warn!(path = %path.display(), error = %err, "cannot watch the declared config; a held boot will wait for an operator's retry");
None
}
}
}
async fn supervised_boot(
source: ConfigSource,
backend: Option<Arc<dyn ContainerRuntime>>,
data_dir: PathBuf,
declared: Option<PathBuf>,
mut rec: BootRecorder,
) -> BootOutcome {
let failed = |result, declared_digest| BootOutcome {
result,
provenance: None,
declared_digest,
};
if let Err(e) = rec.enter(BootPhase::ResolveConfig) {
return failed(Err(rec.failed(e)), None);
}
let declared_digest = declared.as_deref().and_then(file_digest);
let resolved = match source.resolve() {
Ok(resolved) => resolved,
Err(e) => return failed(Err(rec.failed(e.into())), declared_digest),
};
if resolved.config.runtime.data_dir != data_dir {
let err = RuntimeError::DataDirMoved {
control: data_dir,
resolved: resolved.config.runtime.data_dir.clone(),
};
return failed(Err(rec.failed(err)), declared_digest);
}
let ResolvedConfig { config, provenance } = resolved;
BootOutcome {
result: Runtime::boot_resolved(config, backend, &mut rec).await,
provenance,
declared_digest,
}
}
#[must_use]
pub fn file_digest(path: &std::path::Path) -> Option<String> {
std::fs::read(path)
.ok()
.map(|bytes| blake3::hash(&bytes).to_hex().to_string())
}