feat(#2290): StartableAgent token — start_with_fallback requires preamble proof
- lifecycle::StartableAgent: opaque token produced only by converge_start_preamble. #[must_use] with a hint to call start_with_fallback(token). - lifecycle::converge_start_preamble(name, hive, paths): runs ensure_agent_runtime_dir + write_dropins, returns StartableAgent. The only way to obtain a token. - lifecycle::start_with_fallback(token: StartableAgent): public API now requires the token. Callers that skip the preamble get a compile error, not a runtime outage. - lifecycle::start_with_fallback_inner(name): private; used internally by rebuild_no_meta where the preamble is already enforced structurally (write_dropins was called on the line above). - exec.rs ReconcileAction::Start: migrated to converge_start_preamble + start_with_fallback(token). The write_dropins + start_with_fallback two-step is now a single typed pipeline.
This commit is contained in:
parent
15fc33d2e1
commit
950a13bc69
2 changed files with 56 additions and 19 deletions
|
|
@ -253,27 +253,20 @@ async fn run_reconcile(
|
||||||
.transient
|
.transient
|
||||||
.is_none()
|
.is_none()
|
||||||
.then(|| coord.transient_guard(name, crate::coordinator::TransientKind::Starting));
|
.then(|| coord.transient_guard(name, crate::coordinator::TransientKind::Starting));
|
||||||
// Converge the ephemeral host-side state before the start:
|
// Run the typed start preamble: ensures the runtime dir
|
||||||
// `/run/hyperhive/agents/<name>` + `/run/hive-agent/<name>`
|
// exists and writes the nspawn/resource-limits drop-ins.
|
||||||
// (both nspawn bind sources — tmpfs, empty after a host
|
// The returned StartableAgent token is the only way to call
|
||||||
// reboot; nspawn refuses to start with a missing source)
|
// start_with_fallback — omitting this becomes a compile error.
|
||||||
// and the resource-limits drop-in under
|
// MCP listener registration is handled by mcp_sockets::spawn_poll
|
||||||
// `/run/systemd/system/`. Rebuild DAGs get this from their
|
// (first tick immediate); the container boot takes longer than
|
||||||
// Prebuild/Swap nodes; the bare-Reconcile templates (boot
|
// the 10 s interval so the listener is ready in time.
|
||||||
// reconcile, plain start/restart) otherwise start with
|
|
||||||
// nothing under /run and fail.
|
|
||||||
//
|
|
||||||
// Dir creation is the pure-filesystem part (no Coordinator
|
|
||||||
// dep). The MCP listener is reconciled by mcp_sockets::spawn_poll
|
|
||||||
// whose first tick fires immediately on daemon start — the
|
|
||||||
// container boot takes longer than the 10 s interval anyway.
|
|
||||||
crate::lifecycle::ensure_agent_runtime_dir(name)?;
|
|
||||||
let agent_dir = Coordinator::agent_dir(name);
|
let agent_dir = Coordinator::agent_dir(name);
|
||||||
let hive = coord.hive_env();
|
let hive = coord.hive_env();
|
||||||
let paths = Coordinator::agent_paths(name, agent_dir);
|
let paths = Coordinator::agent_paths(name, agent_dir);
|
||||||
crate::lifecycle::write_dropins(name, &hive, &paths).await?;
|
let token =
|
||||||
|
crate::lifecycle::converge_start_preamble(name, &hive, &paths).await?;
|
||||||
ctx.step("nixos-container start");
|
ctx.step("nixos-container start");
|
||||||
crate::lifecycle::start_with_fallback(name).await?;
|
crate::lifecycle::start_with_fallback(token).await?;
|
||||||
coord.kick_agent(name, "container started");
|
coord.kick_agent(name, "container started");
|
||||||
coord.rescan_containers_and_emit().await;
|
coord.rescan_containers_and_emit().await;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -420,17 +420,59 @@ pub async fn start(name: &str) -> Result<()> {
|
||||||
priv_run("start", name).await
|
priv_run("start", name).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Opaque token produced by [`converge_start_preamble`].
|
||||||
|
/// [`start_with_fallback`] requires this as proof that the pre-start
|
||||||
|
/// preamble (runtime dir + drop-ins) ran. Dropping the token without
|
||||||
|
/// calling `start_with_fallback` is a no-op.
|
||||||
|
#[must_use = "call lifecycle::start_with_fallback(token) to start the container"]
|
||||||
|
pub struct StartableAgent {
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run the per-agent start preamble: ensure the runtime dir exists and write
|
||||||
|
/// the nspawn / resource-limits drop-ins. Returns a [`StartableAgent`] token
|
||||||
|
/// as typed proof that the preamble ran; pass it to [`start_with_fallback`].
|
||||||
|
/// Callers that omit this step cannot call `start_with_fallback` — the type
|
||||||
|
/// system makes forgetting the preamble a compile error.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if `ensure_agent_runtime_dir` or `write_dropins` fails.
|
||||||
|
pub async fn converge_start_preamble(
|
||||||
|
name: &str,
|
||||||
|
hive: &HiveEnv,
|
||||||
|
paths: &AgentPaths,
|
||||||
|
) -> Result<StartableAgent> {
|
||||||
|
ensure_agent_runtime_dir(name)?;
|
||||||
|
write_dropins(name, hive, paths).await?;
|
||||||
|
Ok(StartableAgent {
|
||||||
|
name: name.to_owned(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Start with the cold-start fallback: when a plain start fails (the
|
/// Start with the cold-start fallback: when a plain start fails (the
|
||||||
/// activation-error shape), retry once via stop + kill + start before
|
/// activation-error shape), retry once via stop + kill + start before
|
||||||
/// giving up. Used by the queue's fast-lane `Start` handler and the
|
/// giving up. Used by the queue's fast-lane `Start` handler and the
|
||||||
/// inline start-after-rebuild path.
|
/// inline start-after-rebuild path.
|
||||||
/// See `docs/coordinator.md::Cold-start fallback`.
|
/// See `docs/coordinator.md::Cold-start fallback`.
|
||||||
///
|
///
|
||||||
|
/// Requires a [`StartableAgent`] token from [`converge_start_preamble`]
|
||||||
|
/// to prove the preamble ran. For internal use within this module (where
|
||||||
|
/// the preamble is already enforced structurally) call
|
||||||
|
/// `start_with_fallback_inner` directly.
|
||||||
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Propagates the retry's start error (annotated with the original
|
/// Propagates the retry's start error (annotated with the original
|
||||||
/// failure) when the fallback also fails.
|
/// failure) when the fallback also fails.
|
||||||
pub async fn start_with_fallback(name: &str) -> Result<()> {
|
pub async fn start_with_fallback(token: StartableAgent) -> Result<()> {
|
||||||
|
start_with_fallback_inner(&token.name).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Internal implementation of the cold-start fallback. Used by
|
||||||
|
/// [`start_with_fallback`] (public, token-gated) and by
|
||||||
|
/// [`rebuild_no_meta`] where the preamble is already enforced structurally.
|
||||||
|
async fn start_with_fallback_inner(name: &str) -> Result<()> {
|
||||||
validate(name)?;
|
validate(name)?;
|
||||||
if let Err(start_err) = priv_run("start", name).await {
|
if let Err(start_err) = priv_run("start", name).await {
|
||||||
let container = container_name(name);
|
let container = container_name(name);
|
||||||
|
|
@ -577,7 +619,9 @@ pub async fn rebuild_no_meta(
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
on_step("nixos-container start");
|
on_step("nixos-container start");
|
||||||
start_with_fallback(name).await?;
|
// write_dropins was called above; use the inner fn directly
|
||||||
|
// since the preamble is enforced structurally in this path.
|
||||||
|
start_with_fallback_inner(name).await?;
|
||||||
}
|
}
|
||||||
Ok(false)
|
Ok(false)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue