feat(hive-c0re): replace rebuild queue with generic job-DAG queue

jobs are now DAGs of primitive nodes (prebuild, stop-for-update, swap,
reconcile, signal, drain, ...) driven by one scheduler with N build
slots + per-agent lifecycle leases. per-agent power intent (wanted
up/offline) is durable in agent_power.sqlite; Reconcile nodes converge
observed state to it. kills the graceful-stop watcher thread, the
deferred-start follow-up, and the cascade pre-enqueue (fan-out on
MetaLock completion instead). tracker: #2166
This commit is contained in:
müde 2026-07-06 20:13:14 +02:00
commit 7946e03fde
25 changed files with 3673 additions and 2731 deletions

View file

@ -131,7 +131,7 @@ async fn dispatch(req: &HostRequest, coord: Arc<Coordinator>) -> HostResponse {
agents.retain(|a| prev.contains(a));
}
let infra = scoped_infra(scope);
handle_start(&agents, &infra).await?
handle_start(&coord, &agents, &infra).await?
}
HostRequest::Destroy { name, purge } => {
actions::destroy(&coord, name, *purge).await?;
@ -201,6 +201,9 @@ async fn handle_spawn(coord: &Arc<Coordinator>, name: &str) -> Result<HostRespon
let paths = Coordinator::agent_paths(name, agent_dir);
match lifecycle::spawn(name, &hive, &paths).await {
Ok(()) => {
if let Err(e) = coord.power.set(name, crate::power::Wanted::Up) {
tracing::warn!(%name, error = ?e, "agent_power: set wanted=up failed");
}
coord.notify_manager(&hive_sh4re::HelperEvent::Spawned {
agent: name.to_owned(),
ok: true,
@ -224,8 +227,12 @@ async fn handle_spawn(coord: &Arc<Coordinator>, name: &str) -> Result<HostRespon
}
/// Kill `name`'s container, unregister its socket, notify the manager.
/// Persists `wanted = Offline` first so reconciles don't undo the kill.
async fn handle_kill(coord: &Arc<Coordinator>, name: &str) -> Result<HostResponse> {
tracing::info!(%name, "kill");
if let Err(e) = coord.power.set(name, crate::power::Wanted::Offline) {
tracing::warn!(%name, error = ?e, "agent_power: set wanted=offline failed");
}
lifecycle::kill(name).await?;
coord.unregister_agent(name);
coord.notify_manager(&hive_sh4re::HelperEvent::Killed {
@ -284,25 +291,29 @@ async fn handle_stop(
tracing::info!(?agents, ?infra, graceful, "stop");
let mut ok_items: Vec<String> = Vec::new();
let mut errors: Vec<String> = Vec::new();
let mut enqueued_graceful = false;
for agent in agents {
if graceful {
// Graceful stop: enqueue the quiesce orchestration rather than a
// hard kill. Serialised through the rebuild queue so it can't race
// an in-flight rebuild for the same agent, and its per-step
// progress surfaces on the queue snapshot + build log.
coord.rebuild_queue.enqueue(
crate::rebuild_queue::QueueKind::GracefulStop,
agent.clone(),
crate::rebuild_queue::QueueSource::Manual,
// Graceful stop: submit the quiesce DAG rather than a hard
// kill. The per-agent lease keeps it from racing an
// in-flight rebuild for the same agent; the cheap Signal
// nodes all fire immediately so every agent's drain
// overlaps. `submit::graceful_stop` also persists
// `wanted = Offline` and emits the queue snapshot.
crate::job_queue::submit::graceful_stop(
coord,
agent,
crate::job_queue::Source::Manual,
"manual via hivectl graceful stop".to_owned(),
None,
);
ok_items.push(agent.clone());
enqueued_graceful = true;
continue;
}
// Persist the intent even if the kill itself fails — otherwise
// the next boot reconcile would restart the agent.
if let Err(e) = coord.power.set(agent, crate::power::Wanted::Offline) {
tracing::warn!(%agent, error = ?e, "agent_power: set wanted=offline failed");
}
match lifecycle::kill(agent).await {
Ok(()) => ok_items.push(agent.clone()),
Err(e) => {
@ -311,9 +322,6 @@ async fn handle_stop(
}
}
}
if enqueued_graceful {
coord.emit_rebuild_queue_snapshot();
}
for &container in infra {
let name = container.unit_name();
@ -333,7 +341,11 @@ async fn handle_stop(
/// inverse of [`handle_stop`]. Infra comes up before agents so the agents
/// find forge/matrix/gateway ready. Per-target failures aggregated. Callers
/// resolve the [`LifecycleScope`] to these explicit name lists up front.
async fn handle_start(agents: &[String], infra: &[InfraContainer]) -> Result<HostResponse> {
async fn handle_start(
coord: &Arc<Coordinator>,
agents: &[String],
infra: &[InfraContainer],
) -> Result<HostResponse> {
tracing::info!(?agents, ?infra, "start");
let mut ok_items: Vec<String> = Vec::new();
let mut errors: Vec<String> = Vec::new();
@ -350,6 +362,11 @@ async fn handle_start(agents: &[String], infra: &[InfraContainer]) -> Result<Hos
}
for agent in agents {
// Persist the intent even if the start itself fails — the next
// reconcile (boot or queued) retries toward `Up`.
if let Err(e) = coord.power.set(agent, crate::power::Wanted::Up) {
tracing::warn!(%agent, error = ?e, "agent_power: set wanted=up failed");
}
match lifecycle::start(agent).await {
Ok(()) => ok_items.push(agent.clone()),
Err(e) => {