refactor(#2916): destroy submits a DAG instead of an imperative teardown
Destroy was a straight-line async fn with no queue node behind it, so
nothing in the graph could answer "is this container going down on
purpose?". That gap is why an imperative crash-watch suppression guard
existed: an RAII handle held for the operation's duration, a second way
to say what every other lifecycle op already says through its node.
Reuse the existing Stop node rather than teaching a new node to stop
things:
Stop -> DestroyContainer -> (PurgeState) -> DestroyBookkeeping
Stop already declares takes_container_down honestly, so the suppression
is now derived from the graph like every other op's. It also turns the
precondition into an edge: DestroyContainer runs only under a completed
Stop, so it operates on an already-stopped container and carries
takes_container_down = false permanently. A container still alive at
that point is a real bug and stays loud instead of being absorbed by a
flag -- which matters because a wrong true silently swallows a crash
while a wrong false only costs a spurious event.
Removes suppress_crash_watch, CrashWatchSuppression, crash_suppressed,
crash_watch_suppressed and NO_NODE_LABEL. The migration call sites went
with the obsolete startup migrations, so destroy was the last caller and
intent now has exactly one home.
destroy() becomes a submit-and-return, matching every sibling endpoint
(rebuild, kill, restart, start, pause, resume) -- it was the only
lifecycle op that awaited its work. The container rescan moves into the
bookkeeping tail, so ContainerRemoved now arrives after the 200 rather
than before it.
Also drops an orphaned doc-comment in coordinator.rs: two stacked blocks
where only the second described crash_suppressed, the first documenting
a field that no longer exists. Removing the field would have re-pointed
it at recent_transient.
This commit is contained in:
parent
2c7872841a
commit
6338939657
9 changed files with 326 additions and 194 deletions
|
|
@ -899,99 +899,20 @@ pub async fn run_finalize_deploy(coord: &Arc<Coordinator>, approval_id: i64) ->
|
|||
/// imperative infra that `auto_update::ensure_root_agent` recreates on the
|
||||
/// next hive-c0re startup if absent, so destroying it is transient rather
|
||||
/// than something to refuse at the API.
|
||||
pub async fn destroy(coord: &Arc<Coordinator>, name: &str, purge: bool) -> Result<()> {
|
||||
///
|
||||
/// Submits the teardown DAG and returns — it does not wait for the container to
|
||||
/// go away. Same contract as every other lifecycle op (`rebuild`, `kill`,
|
||||
/// `restart`, `start`): the queue owns the work, the caller gets an
|
||||
/// acknowledgement. Progress is visible as real nodes on the dashboard.
|
||||
pub fn destroy(coord: &Arc<Coordinator>, name: &str, purge: bool) {
|
||||
tracing::info!(%name, purge, "destroy");
|
||||
// Guard auto-clears on the success path's final scope exit and on
|
||||
// every early-return / cancellation along the way.
|
||||
// Destroy has no queue node behind it, so nothing in the graph says this
|
||||
// container is going away on purpose — without this the crash watcher
|
||||
// reports every destroy as a crash and the manager tries to recover it.
|
||||
let guard = coord.suppress_crash_watch(name);
|
||||
lifecycle::destroy(name).await?;
|
||||
coord.unregister_agent(name);
|
||||
let runtime = crate::paths::agent_runtime_dir(name);
|
||||
if runtime.exists() {
|
||||
let _ = std::fs::remove_dir_all(&runtime);
|
||||
if let Err(e) = coord.job_queue.insert_job(|b| {
|
||||
crate::job_queue::templates::destroy(b, name, purge);
|
||||
Vec::new()
|
||||
}) {
|
||||
tracing::error!(agent = %name, error = ?e, "destroy: insert failed");
|
||||
}
|
||||
if purge {
|
||||
// The state root may be a btrfs subvolume: a subvolume root
|
||||
// can't be removed with rmdir/`remove_dir_all`, so delete it via
|
||||
// hive-priv (root) first. No-op for plain-dir agents — the loop below
|
||||
// then handles the plain-dir state root plus the applied dir.
|
||||
if let Err(e) = crate::priv_client::delete_agent_subvolume(name).await {
|
||||
tracing::warn!(error = ?e, %name, "purge: delete state subvolume failed");
|
||||
}
|
||||
// A malformed name can't have a persistent state tree (the state dir
|
||||
// is only ever created under a validated Ident), so its removal is a
|
||||
// no-op — skip the state-dir sweep and just clear the applied dir.
|
||||
let state_dir = hive_types::Ident::parse(name)
|
||||
.ok()
|
||||
.map(|id| crate::paths::agent_state_dir(&id));
|
||||
for dir in state_dir
|
||||
.into_iter()
|
||||
.chain([crate::paths::applied_dir(name)])
|
||||
{
|
||||
if dir.exists()
|
||||
&& let Err(e) = std::fs::remove_dir_all(&dir)
|
||||
{
|
||||
tracing::warn!(error = ?e, dir = %dir.display(), "purge: remove failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
// Meta flake: drop the agent's input + nixosConfiguration so a
|
||||
// future spawn under the same name re-seeds cleanly, and so the
|
||||
// meta lock doesn't reference a vanished applied repo. Log + keep
|
||||
// going on failure — destroy already succeeded at the
|
||||
// nixos-container level, the meta repo is just bookkeeping.
|
||||
if let Err(e) = sync_meta_after_lifecycle(coord).await {
|
||||
tracing::warn!(error = ?e, %name, "meta sync after destroy failed");
|
||||
}
|
||||
let _ = coord.approvals.fail_pending_for_agent(
|
||||
name,
|
||||
if purge {
|
||||
"agent purged"
|
||||
} else {
|
||||
"agent destroyed"
|
||||
},
|
||||
);
|
||||
// Drop the durable power intent — a future agent of the same name
|
||||
// seeds fresh from its observed state.
|
||||
if let Err(e) = coord.power.remove(name) {
|
||||
tracing::warn!(%name, error = ?e, "agent_power: remove on destroy failed");
|
||||
}
|
||||
drop(guard);
|
||||
let _ = coord
|
||||
.push_todo(
|
||||
hive_sh4re::manager::MANAGER_AGENT,
|
||||
"core",
|
||||
Some(format!("destroyed:{name}")),
|
||||
format!("agent '{name}' destroyed"),
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
// Container row disappeared — rescan so the dashboard fires
|
||||
// `ContainerRemoved` for the gone row, then emit the
|
||||
// tombstones snapshot (gained one on destroy, lost one on
|
||||
// purge — recompute either way).
|
||||
coord.rescan_containers_and_emit().await;
|
||||
crate::dashboard::emit_tombstones_snapshot(coord).await;
|
||||
// Re-emit the schedules snapshot: the rescan above refreshed the live
|
||||
// roster, so any schedule that still targets the just-destroyed agent
|
||||
// now drops that ghost column live (no page reload needed).
|
||||
coord.emit_schedules_snapshot();
|
||||
// Update tmpfiles.d to remove the destroyed agent's dirs from the
|
||||
// boot-time pre-creation list. Best-effort: failure is logged only.
|
||||
tokio::spawn(lifecycle::sync_tmpfiles());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Rerender the meta flake from whatever containers still exist on
|
||||
/// disk. Called after lifecycle ops that change the agent set (today:
|
||||
/// destroy). Idempotent — a no-op when nothing changed.
|
||||
async fn sync_meta_after_lifecycle(coord: &Coordinator) -> Result<()> {
|
||||
let agents = lifecycle::agents_for_meta_listing().await?;
|
||||
crate::meta::sync_agents(&coord.hive_env(), &agents).await
|
||||
coord.emit_rebuild_queue_snapshot();
|
||||
}
|
||||
|
||||
pub fn deny(coord: &Coordinator, id: i64, note: Option<&str>) -> Result<()> {
|
||||
|
|
|
|||
Loading…
Reference in a new issue