refactor(#2390): split provision out of the create node in the spawn dag
This commit is contained in:
parent
96c748475e
commit
436adf6fd0
6 changed files with 79 additions and 35 deletions
|
|
@ -145,6 +145,7 @@ const NODE_KIND_LABEL = {
|
|||
prebuild: 'prebuild',
|
||||
stop_for_update: 'stop',
|
||||
swap: 'swap',
|
||||
provision: 'provision',
|
||||
create: 'create',
|
||||
meta_lock: 'meta lock',
|
||||
reconcile: 'reconcile',
|
||||
|
|
|
|||
|
|
@ -81,7 +81,8 @@ pub(super) async fn run_node(coord: &Arc<Coordinator>, claim: &Claim) -> Result<
|
|||
match &claim.kind {
|
||||
NodeKind::Prebuild { relock } => run_prebuild(coord, claim, &ctx, *relock).await,
|
||||
NodeKind::Swap => run_swap(coord, claim, &ctx).await,
|
||||
NodeKind::Create => run_create(coord, claim, &ctx).await,
|
||||
NodeKind::Provision => run_provision(coord, claim, &ctx).await,
|
||||
NodeKind::Create => run_create(claim, &ctx).await,
|
||||
NodeKind::MetaLock { sweep, fanout } => {
|
||||
run_meta_lock(coord, claim, &ctx, *sweep, fanout.clone()).await
|
||||
}
|
||||
|
|
@ -191,23 +192,35 @@ async fn run_swap(coord: &Arc<Coordinator>, claim: &Claim, ctx: &Ctx<'_>) -> Res
|
|||
result.map(|()| NodeOutput::default())
|
||||
}
|
||||
|
||||
/// First-spawn provisioning + `nixos-container create` (atomic
|
||||
/// build+create — no prebuild needed).
|
||||
async fn run_create(coord: &Arc<Coordinator>, claim: &Claim, ctx: &Ctx<'_>) -> Result<NodeOutput> {
|
||||
/// First-spawn pre-create provisioning: proposed/applied repos, state
|
||||
/// subvolume, and the meta `sync_agents` registration. Holds the
|
||||
/// deploy-window gate for the commit so it can't land inside another
|
||||
/// node's staged deploy window — same discipline as `Prebuild`, which
|
||||
/// commits under the gate then drops it before the (store-only) build.
|
||||
async fn run_provision(
|
||||
coord: &Arc<Coordinator>,
|
||||
claim: &Claim,
|
||||
ctx: &Ctx<'_>,
|
||||
) -> Result<NodeOutput> {
|
||||
let name = &claim.agent;
|
||||
let agent_dir = crate::paths::agent_runtime_dir(name);
|
||||
let hive = coord.hive_env();
|
||||
let paths = Coordinator::agent_paths(name, agent_dir);
|
||||
ctx.step("nixos-container create");
|
||||
// create_container registers the new agent in the meta flake
|
||||
// (sync_agents commit) before `nixos-container create` — hold the
|
||||
// deploy-window gate so that commit can't land inside another
|
||||
// node's staged deploy window.
|
||||
// Runtime dir creation and MCP listener registration are deferred to
|
||||
// the tail Reconcile (converge_start_preamble + register_agent) so this
|
||||
// node stays purely "provision + create", not "create + start".
|
||||
ctx.step("provisioning");
|
||||
let _window = crate::meta::exclusive().await;
|
||||
crate::lifecycle::create_container(name, &hive, &paths).await?;
|
||||
crate::lifecycle::provision_container(name, &hive, &paths).await?;
|
||||
Ok(NodeOutput::default())
|
||||
}
|
||||
|
||||
/// `nixos-container create` proper — the upstream `Provision` node
|
||||
/// already registered the agent in meta, so this only reads the store
|
||||
/// (no deploy-window gate needed, mirroring `Prebuild`'s build). Runtime
|
||||
/// dir creation and MCP listener registration are deferred to the tail
|
||||
/// `Reconcile` (`converge_start_preamble` + `register_agent`) so this
|
||||
/// node stays purely "create", not "create + start".
|
||||
async fn run_create(claim: &Claim, ctx: &Ctx<'_>) -> Result<NodeOutput> {
|
||||
ctx.step("nixos-container create");
|
||||
crate::lifecycle::create_only(&claim.agent).await?;
|
||||
Ok(NodeOutput::default())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -75,9 +75,14 @@ pub enum NodeKind {
|
|||
/// rebuild is the reconcile verb — and carries the post-rebuild
|
||||
/// bookkeeping tail (rev marker, forge/matrix sync, kick, rescan).
|
||||
Swap,
|
||||
/// First-spawn `nixos-container create` plus the pre-create
|
||||
/// provisioning (proposed/applied repos, state subvolume, meta
|
||||
/// registration).
|
||||
/// First-spawn pre-create provisioning: proposed/applied repos,
|
||||
/// state subvolume, and meta registration (`sync_agents`). Runs
|
||||
/// ahead of `Create` so the `nixos-container create --flake
|
||||
/// meta#<name>` ref resolves. Store/meta-only — no container yet —
|
||||
/// so it's lease- and build-slot-exempt like `Prebuild`.
|
||||
Provision,
|
||||
/// First-spawn `nixos-container create` proper. Assumes the
|
||||
/// upstream `Provision` node already registered the agent in meta.
|
||||
Create,
|
||||
/// Meta flake lock bump. `sweep = false`: `meta::lock_update`
|
||||
/// (commit fused, under `META_LOCK`) with the DAG's `inputs`;
|
||||
|
|
@ -139,6 +144,7 @@ impl NodeKind {
|
|||
match self {
|
||||
NodeKind::Prebuild { .. } => "prebuild",
|
||||
NodeKind::Swap => "swap",
|
||||
NodeKind::Provision => "provision",
|
||||
NodeKind::Create => "create",
|
||||
NodeKind::MetaLock { .. } => "meta_lock",
|
||||
NodeKind::Reconcile => "reconcile",
|
||||
|
|
@ -169,10 +175,12 @@ impl NodeKind {
|
|||
|
||||
/// Container-affecting kinds require the DAG to hold the agent's
|
||||
/// lifecycle lease (acquired at the first such node, held until the
|
||||
/// DAG is terminal). Lease-exempt kinds (`Prebuild`, `MetaLock`,
|
||||
/// `WritePermFile`) touch the store / meta repo, not the running
|
||||
/// container — which is exactly why a `Prebuild` can overlap
|
||||
/// another DAG's work on the same agent.
|
||||
/// DAG is terminal). Lease-exempt kinds (`Prebuild`, `Provision`,
|
||||
/// `MetaLock`, `WritePermFile`) touch the store / meta repo, not the
|
||||
/// running container — which is exactly why a `Prebuild` can overlap
|
||||
/// another DAG's work on the same agent. `Provision` precedes the
|
||||
/// container's existence entirely, so the lease is first taken at the
|
||||
/// `Create` node it feeds.
|
||||
pub fn needs_lease(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
//! restart(a): [wanted=Up] StopForUpdate(a) → Reconcile(a)
|
||||
//! start(a): [wanted=Up] Reconcile(a)
|
||||
//! stop(a): [wanted=Offline] Reconcile(a)
|
||||
//! spawn(a): [wanted=Up] Create(a) → WriteDropin(a) → Reconcile(a)
|
||||
//! spawn(a): [wanted=Up] Provision(a) → Create(a) → WriteDropin(a) → Reconcile(a)
|
||||
//! perm-change(a): WritePermFile(a) → «rebuild subgraph»
|
||||
//! meta-update(inp): MetaLock(inp) → «fan-out rebuild(a) per affected a»
|
||||
//! startup sweep: MetaLock(hyperhive, non-fatal) → «fan-out rebuild(stale a)»
|
||||
|
|
@ -194,9 +194,10 @@ pub fn reconcile_only(
|
|||
}
|
||||
}
|
||||
|
||||
/// First-deploy spawn (approval-driven): pre-start provisioning +
|
||||
/// `nixos-container create`, drop-in write, then `Reconcile` starts the
|
||||
/// container (`wanted = Up` written at approve time).
|
||||
/// First-deploy spawn (approval-driven): `Provision` (proposed/applied
|
||||
/// repos, state subvolume, meta registration) then `Create`
|
||||
/// (`nixos-container create`), drop-in write, then `Reconcile` starts
|
||||
/// the container (`wanted = Up` written at approve time).
|
||||
pub fn spawn(agent: &str, approval_id: i64, reason: String) -> DagSpec {
|
||||
DagSpec {
|
||||
template: Template::Spawn,
|
||||
|
|
@ -210,17 +211,21 @@ pub fn spawn(agent: &str, approval_id: i64, reason: String) -> DagSpec {
|
|||
transient: Some(TransientKind::Spawning),
|
||||
nodes: vec![
|
||||
NodeSpec {
|
||||
kind: NodeKind::Create,
|
||||
kind: NodeKind::Provision,
|
||||
deps: Vec::new(),
|
||||
},
|
||||
NodeSpec {
|
||||
kind: NodeKind::WriteDropin,
|
||||
kind: NodeKind::Create,
|
||||
deps: after_ok(0),
|
||||
},
|
||||
NodeSpec {
|
||||
kind: NodeKind::Reconcile,
|
||||
kind: NodeKind::WriteDropin,
|
||||
deps: after_ok(1),
|
||||
},
|
||||
NodeSpec {
|
||||
kind: NodeKind::Reconcile,
|
||||
deps: after_ok(2),
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -874,13 +874,13 @@ fn graceful_signal_and_drain_hold_no_build_slot() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn spawn_shape_create_dropin_reconcile() {
|
||||
fn spawn_shape_provision_create_dropin_reconcile() {
|
||||
let q = JobQueue::new(1);
|
||||
let id = submit(
|
||||
&q,
|
||||
templates::spawn("newbie", 7, "approval #7 spawn".to_owned()),
|
||||
);
|
||||
for expected in ["create", "write_dropin", "reconcile"] {
|
||||
for expected in ["provision", "create", "write_dropin", "reconcile"] {
|
||||
let c = claim_one(&q);
|
||||
assert_eq!(c.kind.as_str(), expected);
|
||||
assert_eq!(c.approval_id, Some(7));
|
||||
|
|
|
|||
|
|
@ -226,11 +226,13 @@ pub async fn spawn(name: &str, hive: &HiveEnv, paths: &AgentPaths) -> Result<()>
|
|||
priv_run("start", name).await
|
||||
}
|
||||
|
||||
/// First-spawn provisioning + `nixos-container create`, without the
|
||||
/// drop-in write or the start — the job queue's `Create` node.
|
||||
/// `spawn` composes this with `write_dropins` + start for direct
|
||||
/// callers (root-agent bootstrap).
|
||||
pub async fn create_container(name: &str, hive: &HiveEnv, paths: &AgentPaths) -> Result<()> {
|
||||
/// First-spawn pre-create provisioning — the job queue's `Provision`
|
||||
/// node. Fail fast on a port collision, set up the proposed/applied
|
||||
/// repos + state subvolume + claude/notes dirs, then register the new
|
||||
/// agent in the meta flake (`sync_agents`) so the later
|
||||
/// `nixos-container create --flake meta#<name>` ref resolves. Does NOT
|
||||
/// create the container — that's `create_only` / the `Create` node.
|
||||
pub async fn provision_container(name: &str, hive: &HiveEnv, paths: &AgentPaths) -> Result<()> {
|
||||
validate(name)?;
|
||||
if let Some(other) = port_collision(name).await {
|
||||
bail!(
|
||||
|
|
@ -247,10 +249,25 @@ pub async fn create_container(name: &str, hive: &HiveEnv, paths: &AgentPaths) ->
|
|||
// before `nixos-container create` so the `--flake meta#<name>`
|
||||
// ref resolves.
|
||||
let agents = agents_after_spawn(name).await?;
|
||||
crate::meta::sync_agents(hive, &agents).await?;
|
||||
crate::meta::sync_agents(hive, &agents).await
|
||||
}
|
||||
|
||||
/// The `nixos-container create` proper — the job queue's `Create` node.
|
||||
/// Assumes `provision_container` already registered the agent in meta.
|
||||
pub async fn create_only(name: &str) -> Result<()> {
|
||||
priv_run("create", name).await
|
||||
}
|
||||
|
||||
/// First-spawn provisioning + `nixos-container create`, without the
|
||||
/// drop-in write or the start. `spawn` composes this with
|
||||
/// `write_dropins` + start for direct callers (root-agent bootstrap);
|
||||
/// the job queue instead runs `provision_container` (`Provision` node)
|
||||
/// and `create_only` (`Create` node) as separate DAG steps.
|
||||
pub async fn create_container(name: &str, hive: &HiveEnv, paths: &AgentPaths) -> Result<()> {
|
||||
provision_container(name, hive, paths).await?;
|
||||
create_only(name).await
|
||||
}
|
||||
|
||||
/// Rebuild-path preamble shared by the job queue's `Prebuild` node and
|
||||
/// `rebuild_no_meta`: fail fast on a port collision, then make sure
|
||||
/// the applied repo + state dirs exist. Container untouched.
|
||||
|
|
|
|||
Loading…
Reference in a new issue