hive-c0re: route dashboard start/stop through the rebuild queue
This commit is contained in:
parent
681e993626
commit
2966f682ce
2 changed files with 72 additions and 65 deletions
|
|
@ -1,9 +1,10 @@
|
|||
//! Container lifecycle endpoints for the dashboard.
|
||||
//!
|
||||
//! Rebuild / restart / update-all enqueue onto the rebuild queue; kill /
|
||||
//! start run the lifecycle op directly through `lifecycle_action` (which
|
||||
//! marks the container transient for the duration so the dashboard can
|
||||
//! spinner); destroy delegates to `actions::destroy` (optionally purging).
|
||||
//! Rebuild / restart / start / stop (hard + graceful) / update-all all
|
||||
//! enqueue onto the rebuild queue, so each shows a visible queued→running
|
||||
//! transient on the dashboard — a direct sub-second start/stop only flashed
|
||||
//! the badge; destroy delegates to `actions::destroy` (optionally
|
||||
//! purging).
|
||||
|
||||
use axum::{
|
||||
extract::{Form, Path as AxumPath, Query, State},
|
||||
|
|
@ -43,44 +44,6 @@ pub(super) async fn post_rebuild(
|
|||
(StatusCode::OK, "ok").into_response()
|
||||
}
|
||||
|
||||
/// Common shape for the simple lifecycle action handlers (start /
|
||||
/// stop / restart / rebuild): strip the container prefix, mark
|
||||
/// transient for the duration so the dashboard can spinner, run the
|
||||
/// lifecycle op, clear transient, redirect on success or surface the
|
||||
/// error. `verb` only appears in the error message; `extra` runs on
|
||||
/// success after `clear_transient` for handlers that need follow-up
|
||||
/// (e.g. `kill` also unregisters the agent + fires `HelperEvent`).
|
||||
async fn lifecycle_action<F, Fut>(
|
||||
state: &AppState,
|
||||
name: &str,
|
||||
kind: crate::coordinator::TransientKind,
|
||||
verb: &str,
|
||||
body: F,
|
||||
extra: impl FnOnce(&AppState, &str),
|
||||
) -> Response
|
||||
where
|
||||
F: FnOnce(String) -> Fut,
|
||||
Fut: std::future::Future<Output = anyhow::Result<()>>,
|
||||
{
|
||||
let logical = strip_container_prefix(name);
|
||||
let guard = state.coord.transient_guard(&logical, kind);
|
||||
let result = body(logical.clone()).await;
|
||||
drop(guard);
|
||||
match result {
|
||||
Ok(()) => {
|
||||
extra(state, &logical);
|
||||
// Rescan so the running/needs_login/needs_update flip on
|
||||
// the affected row lands on every dashboard's SSE channel
|
||||
// without waiting for a snapshot poll. 200 + matching
|
||||
// `data-no-refresh` on the form skip the post-submit
|
||||
// /api/state refetch.
|
||||
state.coord.rescan_containers_and_emit().await;
|
||||
(StatusCode::OK, "ok").into_response()
|
||||
}
|
||||
Err(e) => error_response(&format!("{verb} {logical} failed: {e:#}")),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn post_kill(
|
||||
State(state): State<AppState>,
|
||||
AxumPath(name): AxumPath<String>,
|
||||
|
|
@ -116,20 +79,15 @@ pub(super) async fn post_kill(
|
|||
// `manager_server.rs::ManagerRequest::Kill` stays in place: a
|
||||
// manager calling Kill on its own container is self-suicide
|
||||
// mid-call, not a legitimate operator action.
|
||||
lifecycle_action(
|
||||
&state,
|
||||
&name,
|
||||
crate::coordinator::TransientKind::Stopping,
|
||||
"kill",
|
||||
|n| async move { lifecycle::kill(&n).await },
|
||||
|s, n| {
|
||||
s.coord.unregister_agent(n);
|
||||
s.coord.notify_manager(&hive_sh4re::HelperEvent::Killed {
|
||||
agent: n.to_owned(),
|
||||
});
|
||||
},
|
||||
)
|
||||
.await
|
||||
state.coord.rebuild_queue.enqueue(
|
||||
crate::rebuild_queue::QueueKind::Stop,
|
||||
logical,
|
||||
crate::rebuild_queue::QueueSource::Manual,
|
||||
"manual via dashboard stop".to_owned(),
|
||||
None,
|
||||
);
|
||||
state.coord.emit_rebuild_queue_snapshot();
|
||||
(StatusCode::OK, "ok").into_response()
|
||||
}
|
||||
|
||||
pub(super) async fn post_restart(
|
||||
|
|
@ -159,15 +117,15 @@ pub(super) async fn post_start(
|
|||
if let Some(reject) = guard_agent_name(&state, &logical).await {
|
||||
return reject;
|
||||
}
|
||||
lifecycle_action(
|
||||
&state,
|
||||
&name,
|
||||
crate::coordinator::TransientKind::Starting,
|
||||
"start",
|
||||
|n| async move { lifecycle::start(&n).await },
|
||||
|s, n| s.coord.kick_agent(n, "container started"),
|
||||
)
|
||||
.await
|
||||
state.coord.rebuild_queue.enqueue(
|
||||
crate::rebuild_queue::QueueKind::Start,
|
||||
logical,
|
||||
crate::rebuild_queue::QueueSource::Manual,
|
||||
"manual via dashboard start".to_owned(),
|
||||
None,
|
||||
);
|
||||
state.coord.emit_rebuild_queue_snapshot();
|
||||
(StatusCode::OK, "ok").into_response()
|
||||
}
|
||||
|
||||
pub(super) async fn post_update_all(State(state): State<AppState>) -> Response {
|
||||
|
|
|
|||
|
|
@ -46,6 +46,15 @@ pub enum QueueKind {
|
|||
/// stop-checkpoint turn (flush durable `/state`), wait for it to drain,
|
||||
/// then `nixos-container stop`. Falls back to a hard stop on timeout.
|
||||
GracefulStop,
|
||||
/// Start a stopped container (`lifecycle::start`). Routed through the
|
||||
/// queue so the dashboard shows a visible queued→running transient — a
|
||||
/// direct sub-second start only flashes the badge — and bulk starts
|
||||
/// serialise legibly on the queue. Fast op.
|
||||
Start,
|
||||
/// Hard-stop a container (`lifecycle::kill`), no quiesce. Routed through
|
||||
/// the queue for the same visible-progress reason as `Start`; the
|
||||
/// quiescing variant is `GracefulStop`. Fast op.
|
||||
Stop,
|
||||
}
|
||||
|
||||
impl QueueKind {
|
||||
|
|
@ -59,6 +68,8 @@ impl QueueKind {
|
|||
QueueKind::Restart => "restart",
|
||||
QueueKind::PermChange => "perm_change",
|
||||
QueueKind::GracefulStop => "graceful_stop",
|
||||
QueueKind::Start => "start",
|
||||
QueueKind::Stop => "stop",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -845,9 +856,47 @@ async fn dispatch(
|
|||
crate::auto_update::rebuild_agent(coord, name, ¤t_rev, Some(entry.id)).await
|
||||
}
|
||||
(QueueKind::GracefulStop, _) => run_graceful_stop(coord, entry).await,
|
||||
(QueueKind::Start, _) => run_start(coord, entry).await,
|
||||
(QueueKind::Stop, _) => run_stop(coord, entry).await,
|
||||
}
|
||||
}
|
||||
|
||||
/// Start a stopped container off the queue (`QueueKind::Start`), with a
|
||||
/// `Starting` transient so the dashboard shows a visible queued→running
|
||||
/// progression rather than the sub-second flash of a direct start.
|
||||
async fn run_start(
|
||||
coord: &std::sync::Arc<crate::coordinator::Coordinator>,
|
||||
entry: &QueueEntry,
|
||||
) -> anyhow::Result<()> {
|
||||
let name = &entry.agent;
|
||||
let _guard = coord.transient_guard(name, crate::coordinator::TransientKind::Starting);
|
||||
coord.set_queue_step(Some(entry.id), "nixos-container start");
|
||||
crate::lifecycle::start(name).await?;
|
||||
coord.kick_agent(name, "container started");
|
||||
coord.rescan_containers_and_emit().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Hard-stop a container off the queue (`QueueKind::Stop`) — same teardown as
|
||||
/// a direct kill (unregister + `Killed` event), but with a `Stopping`
|
||||
/// transient for visible queue progress. The quiescing variant is
|
||||
/// `run_graceful_stop`.
|
||||
async fn run_stop(
|
||||
coord: &std::sync::Arc<crate::coordinator::Coordinator>,
|
||||
entry: &QueueEntry,
|
||||
) -> anyhow::Result<()> {
|
||||
let name = &entry.agent;
|
||||
let _guard = coord.transient_guard(name, crate::coordinator::TransientKind::Stopping);
|
||||
coord.set_queue_step(Some(entry.id), "nixos-container stop");
|
||||
crate::lifecycle::kill(name).await?;
|
||||
coord.unregister_agent(name);
|
||||
coord.notify_manager(&hive_sh4re::HelperEvent::Killed {
|
||||
agent: name.clone(),
|
||||
});
|
||||
coord.rescan_containers_and_emit().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run one `GracefulStop` entry: signal the harness to quiesce (it returns
|
||||
/// `GracefulStop` on its next `Recv`, runs one stop-checkpoint turn to flush
|
||||
/// durable `/state`, then exits), wait for it to drain — bounded by
|
||||
|
|
|
|||
Loading…
Reference in a new issue