feat(fleet): GET /api/fleet/capacity returns the scheduler's own survey
Pulled forward from the observability phase because the capacity harness scenario needs it. A test that recomputed the slot arithmetic in bash would drift from `vm_placement` and then agree with itself while the scheduler did something else — the same shape as every silent-success bug in this codebase. Returns `survey()` + `rank()` unmodified, and keeps `unfit` as its own list: "the fleet is full" and "we could not read the fleet" send an operator to different places, so they must not be summed into one number. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
d48bdbc9a7
commit
dc0443de34
@@ -402,3 +402,56 @@ async fn bridge_terminal(hub: Arc<NodeHub>, node_id: NodeId, socket: WebSocket)
|
||||
}
|
||||
hub.terminal_close(node_id, sid).await;
|
||||
}
|
||||
|
||||
/// `GET /api/fleet/capacity` — what the SCHEDULER sees, verbatim.
|
||||
///
|
||||
/// Pulled forward from the observability phase because the capacity harness
|
||||
/// scenario needs it: a test that recomputed the slot arithmetic in bash would
|
||||
/// drift from `vm_placement` and then agree with itself while the scheduler did
|
||||
/// something else. This returns `vm_placement::survey` unmodified, so the fleet
|
||||
/// page, the harness and the placer cannot disagree.
|
||||
///
|
||||
/// `backend` narrows to the nodes that can boot one image (`?backend=claude`),
|
||||
/// matching what `choose` does for a phase.
|
||||
pub async fn capacity(
|
||||
State(state): State<AppState>,
|
||||
Authed(user): Authed,
|
||||
Query(q): Query<CapacityQuery>,
|
||||
) -> Result<Json<Value>, ApiError> {
|
||||
let ws = user.workspace_id.as_uuid().to_owned();
|
||||
let (fit, unfit) =
|
||||
crate::vm_placement::survey(&state.pool, &state.node_hub, ws, q.backend.as_deref())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
eprintln!("fleet capacity survey failed: {e}");
|
||||
ApiError::Internal
|
||||
})?;
|
||||
let ranked = crate::vm_placement::rank(fit);
|
||||
Ok(Json(json!({
|
||||
// Total free slots across the fleet. A burst larger than this MUST
|
||||
// queue rather than overcommit — that is the whole feature.
|
||||
"slots": ranked.iter().map(|n| n.slots).sum::<i64>(),
|
||||
"nodes": ranked.iter().map(|n| json!({
|
||||
"id": n.node_id,
|
||||
"name": n.name,
|
||||
"slots": n.slots,
|
||||
"committedVms": n.committed_vms,
|
||||
"headroom": n.headroom,
|
||||
"memTotalMib": n.mem_total_mib,
|
||||
"usedEffMib": n.used_eff_mib,
|
||||
"diskFreeGib": n.disk_free_gib,
|
||||
})).collect::<Vec<_>>(),
|
||||
// Never folded into the above. "Full" and "unreadable" send an
|
||||
// operator to different places, so they stay separate here too.
|
||||
"unfit": unfit.iter().map(|(id, name, why)| json!({
|
||||
"id": id,
|
||||
"name": name,
|
||||
"reason": why.reason(),
|
||||
})).collect::<Vec<_>>(),
|
||||
})))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CapacityQuery {
|
||||
pub backend: Option<String>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user