Skip to main content

aion_server/worker/auto_provision/
record.rs

1//! The shape of a record auto-provision mints: its name, and its argv.
2//!
3//! # The name is DERIVED, never chosen
4//!
5//! `auto/<task_queue>` — one queue, one auto record, forever. A derived name is
6//! what makes a redeploy a RE-MINT of the same row instead of an accumulating
7//! pile of near-identical deployments, and it is what lets the skip rule below
8//! be stated as a simple test on a name.
9//!
10//! The `auto/` prefix is reserved by that rule, not by validation: a record
11//! whose name does not begin with it is an operator's, and an operator's record
12//! is never touched, replaced, or duplicated.
13//!
14//! # The argv is the whole launch, and it is replayed verbatim
15//!
16//! A managed worker replays its record's argv on every restart, so this is the
17//! one place the launch is decided. It carries only what a CONNECTION needs —
18//! the document (which holds the harness section and therefore the whole
19//! launch), the queue, the address to dial, the identity, and the namespace.
20//! Nothing here invents a concurrency, a reconnect budget or an environment:
21//! those are the document's, and putting them on the command line is exactly
22//! the drift pair #204 deleted.
23
24use std::collections::BTreeSet;
25use std::path::Path;
26
27use aion_store::{DesiredState, NewWorkerDeployment, WorkerArtifactRef};
28
29/// The prefix every auto-provisioned record's name carries.
30pub const AUTO_PREFIX: &str = "auto/";
31
32/// The record name that serves `task_queue` on this server's behalf.
33#[must_use]
34pub fn auto_name(task_queue: &str) -> String {
35    format!("{AUTO_PREFIX}{task_queue}")
36}
37
38/// Whether `name` belongs to auto-provision rather than to an operator.
39#[must_use]
40pub fn is_auto_name(name: &str) -> bool {
41    name.starts_with(AUTO_PREFIX)
42}
43
44/// The argv tail a `builtin` record replays to serve one queue's agent actions.
45///
46/// `document` must be the absolute path of a staged document; `dial` the
47/// address this box's worker reaches the liminal listener on.
48#[must_use]
49pub fn verb(task_queue: &str, document: &Path, dial: &str, namespace: &str) -> Vec<String> {
50    vec![
51        "worker".to_owned(),
52        "agent".to_owned(),
53        document.display().to_string(),
54        // Explicit even for a single-block document: the record outlives the
55        // shape of the file it names, and a document that later grows a second
56        // `worker` block must not silently change which queue this serves.
57        "--task-queue".to_owned(),
58        task_queue.to_owned(),
59        "--liminal-address".to_owned(),
60        dial.to_owned(),
61        "--identity".to_owned(),
62        auto_name(task_queue),
63        "--namespace".to_owned(),
64        namespace.to_owned(),
65    ]
66}
67
68/// The document path a `builtin` auto record's argv names, if it has one.
69///
70/// The argv is the record's own data and is the ONLY place this is written
71/// down: the durable record has no document field, and inventing one would be
72/// a second truth to keep in step with the argv the supervisor actually
73/// replays. Read positionally against [`verb`]'s own shape — `worker agent
74/// <document>` — which is why both live in this module.
75#[must_use]
76pub fn document_path(artifact: &WorkerArtifactRef) -> Option<&Path> {
77    let WorkerArtifactRef::Builtin { verb } = artifact;
78    match (verb.first(), verb.get(1), verb.get(2)) {
79        (Some(worker), Some(agent), Some(document)) if worker == "worker" && agent == "agent" => {
80            Some(Path::new(document))
81        }
82        _ => None,
83    }
84}
85
86/// The workflow type whose deploy staged the document this record names.
87///
88/// Derived from the staged directory's own `<type>@<digest>` name, which
89/// [`super::documents`] writes and this reads — one convention, spelled in two
90/// places that a change to either breaks visibly. `None` for a record whose
91/// argv names no staged document, which is every record an operator wrote.
92#[must_use]
93pub fn staged_workflow_type(artifact: &WorkerArtifactRef) -> Option<String> {
94    let directory = document_path(artifact)?.parent()?.file_name()?.to_str()?;
95    let (workflow_type, _digest) = directory.rsplit_once('@')?;
96    Some(workflow_type.to_owned())
97}
98
99/// The operator-authored half of the record auto-provision would write for
100/// `task_queue`, minus the deploy-time binary identity the caller captures.
101///
102/// Desired state is `Running` and there is no third option: the queue's own
103/// document asked for a worker by declaring a `harness` section, and a record
104/// minted `Stopped` would be a worker nobody started — the exact state this
105/// whole lane exists to end.
106#[must_use]
107pub fn new_deployment(
108    task_queue: &str,
109    document: &Path,
110    dial: &str,
111    namespace: &str,
112    binary: aion_store::DeployedBinaryIdentity,
113) -> NewWorkerDeployment {
114    NewWorkerDeployment {
115        name: auto_name(task_queue),
116        artifact: WorkerArtifactRef::Builtin {
117            verb: verb(task_queue, document, dial, namespace),
118        },
119        binary,
120        namespaces: BTreeSet::from([namespace.to_owned()]),
121        task_queue: task_queue.to_owned(),
122        // No locality affinity: this record is about the box that minted it,
123        // and claiming a node it did not choose would route work by an
124        // assumption nobody made.
125        node: None,
126        desired: DesiredState::Running,
127    }
128}
129
130#[cfg(test)]
131#[path = "record_tests.rs"]
132mod tests;