job_queue: finish reparent call-site swap, delete dead sync path
This commit is contained in:
parent
05b373474a
commit
18745f1a98
8 changed files with 94 additions and 216 deletions
|
|
@ -2,8 +2,12 @@
|
|||
//!
|
||||
//! Operator-driven agent reparenting — single (`/api/topology/set-parent`,
|
||||
//! form-encoded) and bulk (`/api/topology/set-parent-bulk`, JSON array →
|
||||
//! one git commit). Both go through `Coordinator::reparent*_with_notify`,
|
||||
//! which wraps `topology::set_parent` with the move-notification messages
|
||||
//! one git commit). Both submit a `NodeKind::Reparent` DAG to the job
|
||||
//! queue (fire-and-forget, like every other queue-backed op — the
|
||||
//! dashboard tree repaints off the queue's own snapshot/rescan once the
|
||||
//! commit lands, same as a rebuild or restart). The executor delegates to
|
||||
//! `Coordinator::reparent_bulk_with_notify`, which wraps
|
||||
//! `crate::meta::bulk_commit_topology` with the move-notification messages
|
||||
//! and the `ContainerView` rescan.
|
||||
|
||||
use axum::{
|
||||
|
|
@ -15,7 +19,8 @@ use serde::Deserialize;
|
|||
|
||||
use problem_details::ProblemDetails;
|
||||
|
||||
use super::{AppState, error_problem, error_response};
|
||||
use super::{AppState, error_problem};
|
||||
use crate::job_queue::{Source, submit};
|
||||
|
||||
/// `POST /api/topology/set-parent` body. `child` is required.
|
||||
/// `new_parent` may be:
|
||||
|
|
@ -44,11 +49,13 @@ pub(super) struct SetParentBulkEntry {
|
|||
/// `POST /api/topology/set-parent` — operator-driven parent move.
|
||||
/// Form fields: `child` (required, agent name), `new_parent`
|
||||
/// (optional — empty / absent string ⇒ promote to root). Refuses
|
||||
/// cycles and unknown agents. The manager is reparentable like any
|
||||
/// other agent — its privileges come from the privileged MCP socket,
|
||||
/// not its tree position. On success
|
||||
/// re-emits container snapshots so the dashboard tree repaints
|
||||
/// without a refresh.
|
||||
/// cycles and unknown agents (surfaced async on the job view — this
|
||||
/// handler only validates the identifiers, not the move itself). The
|
||||
/// manager is reparentable like any other agent — its privileges come
|
||||
/// from the privileged MCP socket, not its tree position. Submitting
|
||||
/// re-emits the queue snapshot immediately so the dashboard shows the
|
||||
/// queued move without a refresh; the tree itself repaints once the
|
||||
/// commit lands.
|
||||
pub(super) async fn post_set_parent(
|
||||
State(state): State<AppState>,
|
||||
Form(form): Form<SetParentForm>,
|
||||
|
|
@ -58,6 +65,8 @@ pub(super) async fn post_set_parent(
|
|||
return Err(ProblemDetails::from_status_code(StatusCode::BAD_REQUEST)
|
||||
.with_detail("set-parent: `child` required"));
|
||||
}
|
||||
let child = hive_types::Ident::parse(&child)
|
||||
.map_err(|e| error_problem(&format!("set-parent: `child` {e}")))?;
|
||||
// Empty / whitespace-only `new_parent` ⇒ promote to root. Web
|
||||
// forms submit the empty string for a "no value" radio button,
|
||||
// so this is the ergonomic encoding.
|
||||
|
|
@ -66,54 +75,65 @@ pub(super) async fn post_set_parent(
|
|||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(str::to_owned);
|
||||
// `reparent_with_notify` wraps `topology::set_parent` with the
|
||||
// three notification messages + the ContainerView rescan.
|
||||
// Idempotent same-parent calls skip both the messages and the
|
||||
// disk write per the topology fast-path.
|
||||
match state
|
||||
.coord
|
||||
.reparent_with_notify(&child, new_parent.as_deref())
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
tracing::info!(
|
||||
child = %child,
|
||||
new_parent = ?new_parent,
|
||||
"operator: set-parent via dashboard"
|
||||
);
|
||||
Ok((StatusCode::OK, "ok").into_response())
|
||||
}
|
||||
Err(e) => Err(error_problem(&format!("set-parent {child} failed: {e}"))),
|
||||
}
|
||||
.map(hive_types::Ident::parse)
|
||||
.transpose()
|
||||
.map_err(|e| error_problem(&format!("set-parent: `new_parent` {e}")))?;
|
||||
tracing::info!(
|
||||
child = %child,
|
||||
new_parent = ?new_parent,
|
||||
"operator: set-parent via dashboard"
|
||||
);
|
||||
submit::reparent(
|
||||
&state.coord,
|
||||
vec![(child, new_parent)],
|
||||
Source::Manual,
|
||||
"manual set-parent via dashboard".to_owned(),
|
||||
);
|
||||
Ok((StatusCode::OK, "ok").into_response())
|
||||
}
|
||||
|
||||
/// `POST /api/topology/set-parent-bulk` — move multiple agents in a single
|
||||
/// request, producing **one** git commit. JSON body: `[{"child":"name",
|
||||
/// "new_parent":"target-or-null"}, ...]`. Empty array is a no-op (200 OK).
|
||||
/// First validation error aborts the whole batch.
|
||||
/// First identifier that fails to parse aborts the whole batch before
|
||||
/// anything is submitted — a partially-invalid bulk move never reaches
|
||||
/// the queue.
|
||||
pub(super) async fn post_set_parent_bulk(
|
||||
State(state): State<AppState>,
|
||||
axum::Json(body): axum::Json<Vec<SetParentBulkEntry>>,
|
||||
) -> Response {
|
||||
) -> Result<Response, ProblemDetails> {
|
||||
if body.is_empty() {
|
||||
return (StatusCode::OK, "ok").into_response();
|
||||
return Ok((StatusCode::OK, "ok").into_response());
|
||||
}
|
||||
// Collect borrows for the coordinator call.
|
||||
let moves: Vec<(&str, Option<&str>)> = body
|
||||
// Collect into `Result<_, String>` first, not `ProblemDetails` directly —
|
||||
// clippy::result_large_err flags a ~232-byte `Err` variant threaded
|
||||
// through this closure's `?`. `String` is small enough to satisfy the
|
||||
// lint; the single `map_err` below promotes it to a `ProblemDetails`
|
||||
// once, after the fallible collect.
|
||||
let moves = body
|
||||
.iter()
|
||||
.map(|e| {
|
||||
let child: &str = &e.child;
|
||||
let parent: Option<&str> = e.new_parent.as_deref().filter(|s| !s.is_empty());
|
||||
(child, parent)
|
||||
let child = hive_types::Ident::parse(e.child.trim())
|
||||
.map_err(|err| format!("set-parent-bulk: `{}` {err}", e.child))?;
|
||||
let new_parent = e
|
||||
.new_parent
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(hive_types::Ident::parse)
|
||||
.transpose()
|
||||
.map_err(|err| format!("set-parent-bulk: `{}` {err}", e.child))?;
|
||||
Ok((child, new_parent))
|
||||
})
|
||||
.collect();
|
||||
match state.coord.reparent_bulk_with_notify(&moves).await {
|
||||
Ok(()) => {
|
||||
let names: Vec<&str> = body.iter().map(|e| e.child.as_str()).collect();
|
||||
tracing::info!(agents = ?names, "operator: set-parent-bulk via dashboard");
|
||||
(StatusCode::OK, "ok").into_response()
|
||||
}
|
||||
Err(e) => error_response(&format!("set-parent-bulk failed: {e}")),
|
||||
}
|
||||
.collect::<Result<Vec<_>, String>>()
|
||||
.map_err(|e| error_problem(&e))?;
|
||||
let names: Vec<&str> = body.iter().map(|e| e.child.as_str()).collect();
|
||||
tracing::info!(agents = ?names, "operator: set-parent-bulk via dashboard");
|
||||
submit::reparent(
|
||||
&state.coord,
|
||||
moves,
|
||||
Source::Manual,
|
||||
"manual set-parent-bulk via dashboard".to_owned(),
|
||||
);
|
||||
Ok((StatusCode::OK, "ok").into_response())
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue