feat(#343): route container restart through rebuild queue

This commit is contained in:
damocles 2026-06-02 12:59:49 +02:00
commit dce2bd0686
4 changed files with 35 additions and 21 deletions

View file

@ -38,9 +38,9 @@ somewhere."
| `MetaUpdate` | `nix flake update` on the meta flake. The worker runs the lock bump itself, then enqueues a cascade of `Rebuild` entries with `parent_id` set to the meta-update's id. |
| `Spawn` | First-deploy of a new agent (approval-driven). Same serialisation as `Rebuild` from the operator's POV. |
| `Destroy` | For future use (`destroy --purge` does real I/O). Variant exists so the wire shape doesn't change later; not currently routed through the queue. |
| `Restart` | Stop + start a container without touching config (~5-10s). Routed through the queue so it serialises against in-flight rebuilds for the same agent — prevents a restart racing a rebuild mid-flight. Sources: dashboard ↺ button, manager `restart` MCP tool. |
**Intentionally not queued** (sub-second ops; adding them adds dashboard noise
without serving the "one at a time" goal): `start`, `stop`, `restart`, `kill`.
**Intentionally not queued** (sub-second ops): `start`, `stop`, `kill`.
### Dedup

View file

@ -2720,15 +2720,15 @@ async fn post_restart(State(state): State<AppState>, AxumPath(name): AxumPath<St
if let Some(reject) = guard_agent_name(&state, &logical).await {
return reject;
}
lifecycle_action(
&state,
&name,
crate::coordinator::TransientKind::Restarting,
"restart",
|n| async move { lifecycle::restart(&n).await },
|s, n| s.coord.kick_agent(n, "container restarted"),
)
.await
state.coord.rebuild_queue.enqueue(
crate::rebuild_queue::QueueKind::Restart,
logical,
crate::rebuild_queue::QueueSource::Manual,
"manual via dashboard ↺ R3START button".to_owned(),
None,
);
state.coord.emit_rebuild_queue_snapshot();
(StatusCode::OK, "ok").into_response()
}
async fn post_start(State(state): State<AppState>, AxumPath(name): AxumPath<String>) -> Response {

View file

@ -159,21 +159,21 @@ async fn dispatch(req: &ManagerRequest, coord: &Arc<Coordinator>) -> ManagerResp
}
}
ManagerRequest::Restart { name } => {
tracing::info!(%name, "manager: restart");
tracing::info!(%name, "manager: enqueue restart");
if name == crate::lifecycle::MANAGER_NAME {
return ManagerResponse::Err {
message: "refusing to restart the manager from itself".into(),
};
}
match lifecycle::restart(name).await {
Ok(()) => {
coord.kick_agent(name, "container restarted");
ManagerResponse::Ok
}
Err(e) => ManagerResponse::Err {
message: format!("{e:#}"),
},
}
coord.rebuild_queue.enqueue(
crate::rebuild_queue::QueueKind::Restart,
name.to_owned(),
crate::rebuild_queue::QueueSource::Manual,
"manager `restart` tool".to_owned(),
None,
);
coord.emit_rebuild_queue_snapshot();
ManagerResponse::Ok
}
ManagerRequest::Update { name } => {
tracing::info!(%name, "manager: enqueue update");

View file

@ -32,6 +32,10 @@ pub enum QueueKind {
/// after the lock bump; children run as independent queue entries
/// grouped under this parent's `id`. `agent` = `"hyperhive"`.
StartupSweep,
/// Stop + start a container without touching config. Fast op (~5-10s).
/// Queued so it serialises against in-flight rebuilds for the same
/// agent — prevents a restart racing a rebuild mid-flight.
Restart,
}
impl QueueKind {
@ -42,6 +46,7 @@ impl QueueKind {
QueueKind::Spawn => "spawn",
QueueKind::Destroy => "destroy",
QueueKind::StartupSweep => "startup_sweep",
QueueKind::Restart => "restart",
}
}
}
@ -591,6 +596,15 @@ async fn dispatch(
// `finish` clears the step label; no explicit clear needed here.
Ok(())
}
(QueueKind::Restart, _) => {
let name = &entry.agent;
let _guard = coord.transient_guard(name, crate::coordinator::TransientKind::Restarting);
coord.set_queue_step(Some(entry.id), "nixos-container restart");
crate::lifecycle::restart(name).await?;
coord.kick_agent(name, "container restarted");
coord.rescan_containers_and_emit().await;
Ok(())
}
}
}