aion-server 0.27.1

Aion workflow server library: HTTP, gRPC, WebSocket, and worker endpoints. Run it with the `aion` binary from the aion-cli crate.
Documentation
//! Boot-stage reporting: the running narration of a boot that takes minutes.
//!
//! A big store spends minutes in WAL recovery, and until the record carried a
//! stage there was nothing to read but silence: `status` could say a server
//! existed but not what it was doing, and an operator watching a home could
//! not tell a boot that was PROGRESSING from one that was STUCK.
//!
//! A [`StageReporter`] is a cheap, cloneable handle on the incarnation's own
//! pid record. It is safe to hand into a blocking closure or another thread —
//! the store build runs on the blocking pool and reports from there — and
//! every report does two things at once: it writes the stage into the record
//! (where `aion server status` and the launcher read it) and emits a
//! `tracing::info!` line (where the log reader sees it). One story, told in
//! both places, from one call.
//!
//! Every report bumps `stage_seq`. That counter is the difference between
//! "this boot is slow" and "this boot is wedged": a reader that sees the
//! sequence advance knows work is happening even when the stage token has not
//! changed for minutes.

use std::path::PathBuf;
use std::sync::{Arc, Mutex};

use tracing::{info, warn};

use super::guard::{RecordUpdate, update_record};
use super::pid_file::{PidRecord, now_unix_secs};

/// The machine token for the stage that resolves configuration — the first
/// thing a boot finishes, and the one that names the home every later stage
/// happens under.
pub const STAGE_CONFIG: &str = "config";

/// The store is being opened: directories checked, the database handed to
/// haematite.
pub const STAGE_STORE_OPEN: &str = "store-open";

/// The open is blocked on the data directory's writer lock, which a live
/// process holds — a draining predecessor mid-handover, or another server on
/// the same data directory.
pub const STAGE_WRITER_LOCK_WAIT: &str = "writer-lock-wait";

/// The store's shards are being materialized, which is where WAL recovery
/// replays. This is the stage that takes minutes on a big store.
pub const STAGE_WAL_RECOVERY: &str = "wal-recovery";

/// The engine is recovering resident workflows from durable state.
pub const STAGE_ENGINE_RECOVERY: &str = "engine-recovery";

/// The transport listeners are being bound. The last stage before serving.
pub const STAGE_BINDING: &str = "binding";

/// A cloneable handle for writing this incarnation's boot stages.
///
/// Cloning is cheap (a path, an `Arc`, and a flag) and every clone writes the
/// SAME record, so a stage reported from the store's blocking thread is
/// immediately visible through the run loop's guard.
#[derive(Clone, Debug)]
pub struct StageReporter {
    path: PathBuf,
    record: Arc<Mutex<PidRecord>>,
    holds_claim: bool,
}

impl StageReporter {
    /// Build a reporter over a guard's shared record. Created only by
    /// [`PidFileGuard::stage_reporter`](super::guard::PidFileGuard::stage_reporter).
    pub(super) fn new(path: PathBuf, record: Arc<Mutex<PidRecord>>, holds_claim: bool) -> Self {
        Self {
            path,
            record,
            holds_claim,
        }
    }

    /// A reporter that writes nothing, for a caller with no claim to report
    /// against: an embedder building [`ServerState`](crate::ServerState)
    /// directly, or a test.
    ///
    /// It is deliberately a real reporter rather than an `Option`: the boot
    /// path reports at half a dozen seams, and threading an `Option` through
    /// all of them would make "no reporter" a case each seam had to handle.
    /// Its reports are logged (at `debug`) and dropped.
    #[must_use]
    pub fn detached() -> Self {
        Self {
            path: PathBuf::new(),
            record: Arc::new(Mutex::new(detached_record())),
            holds_claim: false,
        }
    }

    /// Report that this incarnation has reached `stage`, with one human
    /// sentence of `detail`.
    ///
    /// Infallible by design: a stage report is narration, and a boot must
    /// never fail because it could not describe itself. A write that cannot
    /// happen is reported through `tracing` at `warn` — the same channel the
    /// stage line itself goes to, so the loss is never silent.
    pub fn report(&self, stage: &str, detail: String) {
        info!(
            boot_stage = stage,
            boot_stage_detail = %detail,
            "boot stage"
        );
        let stage_owned = stage.to_owned();
        let update = update_record(&self.path, &self.record, self.holds_claim, move |record| {
            record.stage = Some(stage_owned);
            record.stage_detail = Some(detail);
            record.stage_seq = record.stage_seq.saturating_add(1);
            record.stage_updated_at_unix_secs = now_unix_secs();
        });
        match update {
            Ok(RecordUpdate::Written | RecordUpdate::Unclaimed | RecordUpdate::NotOurs) => {}
            Err(error) => warn!(
                %error,
                boot_stage = stage,
                "could not write the boot stage into this home's pid record; the \
                 stage is in the log above, but `aion server status` and the \
                 launcher will not see it"
            ),
        }
    }
}

/// The placeholder record a [`StageReporter::detached`] handle carries. It is
/// never written anywhere — `holds_claim` is false, so every update short-
/// circuits before touching it — and exists only so the handle has the same
/// shape as a real one.
fn detached_record() -> PidRecord {
    PidRecord {
        pid: 0,
        started_at_unix_secs: 0,
        binary_sha256: String::new(),
        version: String::new(),
        commit: String::new(),
        state: super::pid_file::IncarnationState::Booting,
        http_address: None,
        grpc_address: None,
        intended_http_address: None,
        intended_grpc_address: None,
        stage: None,
        stage_detail: None,
        stage_seq: 0,
        stage_updated_at_unix_secs: 0,
        drain_timeout_seconds: 0,
    }
}

#[cfg(test)]
#[path = "stage_tests.rs"]
mod tests;