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. | | `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. | | `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. | | `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 **Intentionally not queued** (sub-second ops): `start`, `stop`, `kill`.
without serving the "one at a time" goal): `start`, `stop`, `restart`, `kill`.
### Dedup ### 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 { if let Some(reject) = guard_agent_name(&state, &logical).await {
return reject; return reject;
} }
lifecycle_action( state.coord.rebuild_queue.enqueue(
&state, crate::rebuild_queue::QueueKind::Restart,
&name, logical,
crate::coordinator::TransientKind::Restarting, crate::rebuild_queue::QueueSource::Manual,
"restart", "manual via dashboard ↺ R3START button".to_owned(),
|n| async move { lifecycle::restart(&n).await }, None,
|s, n| s.coord.kick_agent(n, "container restarted"), );
) state.coord.emit_rebuild_queue_snapshot();
.await (StatusCode::OK, "ok").into_response()
} }
async fn post_start(State(state): State<AppState>, AxumPath(name): AxumPath<String>) -> 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 } => { ManagerRequest::Restart { name } => {
tracing::info!(%name, "manager: restart"); tracing::info!(%name, "manager: enqueue restart");
if name == crate::lifecycle::MANAGER_NAME { if name == crate::lifecycle::MANAGER_NAME {
return ManagerResponse::Err { return ManagerResponse::Err {
message: "refusing to restart the manager from itself".into(), message: "refusing to restart the manager from itself".into(),
}; };
} }
match lifecycle::restart(name).await { coord.rebuild_queue.enqueue(
Ok(()) => { crate::rebuild_queue::QueueKind::Restart,
coord.kick_agent(name, "container restarted"); name.to_owned(),
ManagerResponse::Ok crate::rebuild_queue::QueueSource::Manual,
} "manager `restart` tool".to_owned(),
Err(e) => ManagerResponse::Err { None,
message: format!("{e:#}"), );
}, coord.emit_rebuild_queue_snapshot();
} ManagerResponse::Ok
} }
ManagerRequest::Update { name } => { ManagerRequest::Update { name } => {
tracing::info!(%name, "manager: enqueue update"); 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 /// after the lock bump; children run as independent queue entries
/// grouped under this parent's `id`. `agent` = `"hyperhive"`. /// grouped under this parent's `id`. `agent` = `"hyperhive"`.
StartupSweep, 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 { impl QueueKind {
@ -42,6 +46,7 @@ impl QueueKind {
QueueKind::Spawn => "spawn", QueueKind::Spawn => "spawn",
QueueKind::Destroy => "destroy", QueueKind::Destroy => "destroy",
QueueKind::StartupSweep => "startup_sweep", QueueKind::StartupSweep => "startup_sweep",
QueueKind::Restart => "restart",
} }
} }
} }
@ -591,6 +596,15 @@ async fn dispatch(
// `finish` clears the step label; no explicit clear needed here. // `finish` clears the step label; no explicit clear needed here.
Ok(()) 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(())
}
} }
} }