aion-server 0.29.0

Aion workflow server library: HTTP, gRPC, WebSocket, and worker endpoints. Run it with the `aion` binary from the aion-cli crate.
Documentation
//! The shape of a record auto-provision mints: its name, and its argv.
//!
//! # The name is DERIVED, never chosen
//!
//! `auto/<task_queue>` — one queue, one auto record, forever. A derived name is
//! what makes a redeploy a RE-MINT of the same row instead of an accumulating
//! pile of near-identical deployments, and it is what lets the skip rule below
//! be stated as a simple test on a name.
//!
//! The `auto/` prefix is reserved by that rule, not by validation: a record
//! whose name does not begin with it is an operator's, and an operator's record
//! is never touched, replaced, or duplicated.
//!
//! # The argv is the whole launch, and it is replayed verbatim
//!
//! A managed worker replays its record's argv on every restart, so this is the
//! one place the launch is decided. It carries only what a CONNECTION needs —
//! the document (which holds the harness section and therefore the whole
//! launch), the queue, the address to dial, the identity, and the namespace.
//! Nothing here invents a concurrency, a reconnect budget or an environment:
//! those are the document's, and putting them on the command line is exactly
//! the drift pair #204 deleted.

use std::collections::BTreeSet;
use std::path::Path;

use aion_store::{DesiredState, NewWorkerDeployment, WorkerArtifactRef};

/// The prefix every auto-provisioned record's name carries.
pub const AUTO_PREFIX: &str = "auto/";

/// The record name that serves `task_queue` on this server's behalf.
#[must_use]
pub fn auto_name(task_queue: &str) -> String {
    format!("{AUTO_PREFIX}{task_queue}")
}

/// Whether `name` belongs to auto-provision rather than to an operator.
#[must_use]
pub fn is_auto_name(name: &str) -> bool {
    name.starts_with(AUTO_PREFIX)
}

/// The argv tail a `builtin` record replays to serve one queue's agent actions.
///
/// `document` must be the absolute path of a staged document; `dial` the
/// address this box's worker reaches the liminal listener on.
#[must_use]
pub fn verb(task_queue: &str, document: &Path, dial: &str, namespace: &str) -> Vec<String> {
    vec![
        "worker".to_owned(),
        "agent".to_owned(),
        document.display().to_string(),
        // Explicit even for a single-block document: the record outlives the
        // shape of the file it names, and a document that later grows a second
        // `worker` block must not silently change which queue this serves.
        "--task-queue".to_owned(),
        task_queue.to_owned(),
        "--liminal-address".to_owned(),
        dial.to_owned(),
        "--identity".to_owned(),
        auto_name(task_queue),
        "--namespace".to_owned(),
        namespace.to_owned(),
    ]
}

/// The document path a `builtin` auto record's argv names, if it has one.
///
/// The argv is the record's own data and is the ONLY place this is written
/// down: the durable record has no document field, and inventing one would be
/// a second truth to keep in step with the argv the supervisor actually
/// replays. Read positionally against [`verb`]'s own shape — `worker agent
/// <document>` — which is why both live in this module.
#[must_use]
pub fn document_path(artifact: &WorkerArtifactRef) -> Option<&Path> {
    let WorkerArtifactRef::Builtin { verb } = artifact;
    match (verb.first(), verb.get(1), verb.get(2)) {
        (Some(worker), Some(agent), Some(document)) if worker == "worker" && agent == "agent" => {
            Some(Path::new(document))
        }
        _ => None,
    }
}

/// The workflow type whose deploy staged the document this record names.
///
/// Derived from the staged directory's own `<type>@<digest>` name, which
/// [`super::documents`] writes and this reads — one convention, spelled in two
/// places that a change to either breaks visibly. `None` for a record whose
/// argv names no staged document, which is every record an operator wrote.
#[must_use]
pub fn staged_workflow_type(artifact: &WorkerArtifactRef) -> Option<String> {
    let directory = document_path(artifact)?.parent()?.file_name()?.to_str()?;
    let (workflow_type, _digest) = directory.rsplit_once('@')?;
    Some(workflow_type.to_owned())
}

/// The operator-authored half of the record auto-provision would write for
/// `task_queue`, minus the deploy-time binary identity the caller captures.
///
/// Desired state is `Running` and there is no third option: the queue's own
/// document asked for a worker by declaring a `harness` section, and a record
/// minted `Stopped` would be a worker nobody started — the exact state this
/// whole lane exists to end.
#[must_use]
pub fn new_deployment(
    task_queue: &str,
    document: &Path,
    dial: &str,
    namespace: &str,
    binary: aion_store::DeployedBinaryIdentity,
) -> NewWorkerDeployment {
    NewWorkerDeployment {
        name: auto_name(task_queue),
        artifact: WorkerArtifactRef::Builtin {
            verb: verb(task_queue, document, dial, namespace),
        },
        binary,
        namespaces: BTreeSet::from([namespace.to_owned()]),
        task_queue: task_queue.to_owned(),
        // No locality affinity: this record is about the box that minted it,
        // and claiming a node it did not choose would route work by an
        // assumption nobody made.
        node: None,
        desired: DesiredState::Running,
    }
}

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