aion-server 0.30.0

Aion workflow server library: HTTP, gRPC, WebSocket, and worker endpoints. Run it with the `aion` binary from the aion-cli crate.
Documentation
//! What each decision SAYS, held apart from what decides it.
//!
//! Every arm of auto-provision ends in a sentence an operator acts on, and
//! those sentences carry the load-bearing facts: whether a worker is actually
//! running, which namespace it serves, what to type to undo a stop. Building
//! them beside the branching that chooses them is how a claim comes to drift
//! from the fact it is about — so the branching lives in `provision`, and the
//! wording lives here where every arm can be read against every other.

use std::path::Path;

use aion_store::DesiredState;

use super::outcome::{AutoWorkerDecision, AutoWorkerOutcome};

/// The subject of one sentence: which queue, which document, which record.
pub(super) struct Subject<'a> {
    pub(super) task_queue: &'a str,
    pub(super) workflow_type: &'a str,
    pub(super) name: &'a str,
    pub(super) namespace: &'a str,
    pub(super) document: &'a Path,
}

/// The sentence for a record this deploy WROTE.
///
/// `decision` has already been chosen from the put outcome joined with whether
/// the convergence succeeded, so the running claim below can only ever repeat
/// a fact — never assert one.
pub(super) fn written(
    subject: &Subject<'_>,
    decision: AutoWorkerDecision,
    dial: &str,
) -> AutoWorkerOutcome {
    if decision == AutoWorkerDecision::OperatorStopped {
        return AutoWorkerOutcome::new(
            subject.task_queue,
            subject.workflow_type,
            decision,
            Some(subject.name.to_owned()),
            format!(
                "worker deployment `{name}` was rewritten onto this document (`{document}`) but \
                 LEFT STOPPED: an operator stopped it, and a redeploy is not a decision to undo \
                 that. Task queue `{queue}` is unserved until `aion worker start {name}`",
                name = subject.name,
                document = subject.document.display(),
                queue = subject.task_queue
            ),
        );
    }
    AutoWorkerOutcome::new(
        subject.task_queue,
        subject.workflow_type,
        decision,
        Some(subject.name.to_owned()),
        format!(
            "task queue `{queue}` declares a `harness` section, so worker deployment `{name}` was \
             written to serve it with `aion worker agent {document} --liminal-address {dial} \
             --namespace {namespace}`{tail}",
            queue = subject.task_queue,
            name = subject.name,
            document = subject.document.display(),
            namespace = subject.namespace,
            tail = running_tail(decision, subject.namespace)
        ),
    )
}

/// The sentence for a record that ALREADY named this exact document, so
/// nothing was written.
pub(super) fn unchanged(
    subject: &Subject<'_>,
    desired: DesiredState,
    converged: bool,
) -> AutoWorkerOutcome {
    let document = subject.document.display();
    if desired == DesiredState::Stopped {
        return AutoWorkerOutcome::new(
            subject.task_queue,
            subject.workflow_type,
            AutoWorkerDecision::OperatorStopped,
            Some(subject.name.to_owned()),
            format!(
                "worker deployment `{name}` already names this exact document (`{document}`) and \
                 is STOPPED because an operator stopped it. Nothing was rewritten and it was not \
                 started; task queue `{queue}` is unserved until `aion worker start {name}`",
                name = subject.name,
                queue = subject.task_queue
            ),
        );
    }
    let decision = if converged {
        AutoWorkerDecision::Unchanged
    } else {
        AutoWorkerDecision::RecordedNotRunning
    };
    AutoWorkerOutcome::new(
        subject.task_queue,
        subject.workflow_type,
        decision,
        Some(subject.name.to_owned()),
        format!(
            "worker deployment `{name}` already serves task queue `{queue}` from this exact \
             document (`{document}`), so nothing was rewritten and its worker was not \
             restarted{tail}",
            name = subject.name,
            queue = subject.task_queue,
            tail = if converged {
                ""
            } else {
                " — and it is NOT running on this server; see the supervision failure logged \
                 beside this line"
            }
        ),
    )
}

/// The loud refusal for a queue this server cannot honestly serve.
pub(super) fn dark_outbox(
    task_queue: &str,
    workflow_type: &str,
    dark: &super::listener::DarkOutbox,
) -> AutoWorkerOutcome {
    AutoWorkerOutcome::new(
        task_queue,
        workflow_type,
        AutoWorkerDecision::DarkOutbox,
        None,
        format!(
            "task queue `{task_queue}` declares a `harness` section, so this server would stand a \
             built-in agent worker up for it — but {dark}. NOTHING was minted: a deployment \
             record replays its argv verbatim, and one naming an address no listener answers on \
             would fail on every restart until its crash-loop budget parked it"
        ),
    )
}

/// The harness cannot launch on THIS host: the refusal names the exact path
/// that is absent, because "worker did not start" without the path sends an
/// operator hunting through worker logs on the wrong machine.
pub(super) fn unrunnable_harness(
    task_queue: &str,
    workflow_type: &str,
    why: &str,
) -> AutoWorkerOutcome {
    AutoWorkerOutcome::new(
        task_queue,
        workflow_type,
        AutoWorkerDecision::UnrunnableHarness,
        None,
        format!(
            "task queue `{task_queue}` declares a `harness` section, so this server would stand a              built-in agent worker up for it — but {why}. NOTHING was minted: a worker whose              agent cannot spawn on this host fails every dispatch routed to it, burning an              attempt each time while the worker that CAN serve the queue never sees the task.              Deploy this document to the host its harness names, or fix the paths it declares"
        ),
    )
}

/// One arm that could not be carried out, named by the queue it is about.
pub(super) fn failure(task_queue: &str, workflow_type: &str, detail: String) -> AutoWorkerOutcome {
    AutoWorkerOutcome::new(
        task_queue,
        workflow_type,
        AutoWorkerDecision::Failed,
        None,
        detail,
    )
}

/// The half-sentence that either claims a running worker or refuses to.
///
/// The namespace rides on the claiming arm because that is the moment an
/// operator forms the belief "the queue is served" — and it is served in ONE
/// namespace, which is not visible anywhere else they would look.
fn running_tail(decision: AutoWorkerDecision, namespace: &str) -> String {
    if decision.claims_running() {
        format!(
            ", and this server started it. It serves namespace `{namespace}` ONLY: a run started \
             in another namespace is not dispatched to it"
        )
    } else {
        String::from(
            " — and this server could NOT start it. The record is durable and the next boot's \
             reconcile honours it; see the supervision failure logged beside this line",
        )
    }
}