swarm-controller: drop the Intent split, one unconditional wanted-state write
mara: "i dont want any logic differene between the two cases" and "do not refuse to recreate an agent". wanted.rs goes back to a single write path (set), with no terminal-state refusal at all, used identically by the pause/resume endpoint and the agent-creation job node. The agent-creation node's own idempotency requirement (re-running create against a name that already has a declaration must not silently pause it) now lives entirely in declare_new_agent: it reads the current declaration first and only writes Paused when the agent has no entry, or its entry is Destroyed (recreating a previously- destroyed name is the fresh deploy that state's own doc comment names as the way back).
This commit is contained in:
parent
75af27abe0
commit
c908a71f3e
2 changed files with 87 additions and 270 deletions
|
|
@ -327,25 +327,51 @@ async fn run_swarm_node(
|
|||
(builder, outcome)
|
||||
}
|
||||
|
||||
/// Declare a brand-new agent at [`NEW_AGENT_WANTED_STATE`].
|
||||
/// Declare a brand-new agent at [`NEW_AGENT_WANTED_STATE`] — unless it turns
|
||||
/// out not to be new.
|
||||
///
|
||||
/// A named function beside [`publish_deploy`] rather than the two lines
|
||||
/// inline, for the same reason that one is: `run_swarm_node`'s match is a
|
||||
/// per-variant index, and every arm that grows a body past a call pushes the
|
||||
/// next reader further from the variant they came to read.
|
||||
///
|
||||
/// Reads the current declaration first and only calls
|
||||
/// [`WantedWriter::set`](wanted::WantedWriter::set) when this agent has no
|
||||
/// entry yet — `wanted.rs` itself no longer distinguishes creation from any
|
||||
/// other write (mara: "i dont want any logic difference between the two
|
||||
/// cases"), so the idempotency this node needs — calling create against a
|
||||
/// name that already has a declaration (an operator migrating a pre-existing
|
||||
/// agent into this bookkeeping, or a retried request) must not silently
|
||||
/// pause an agent already running under some other state — lives entirely
|
||||
/// here, the one caller it applies to. An existing `Destroyed` entry counts
|
||||
/// as "no entry" for this check and gets written over: recreating a
|
||||
/// previously-destroyed name is exactly the fresh deploy that state's own
|
||||
/// doc comment names as the way back, and mara separately ruled "do not
|
||||
/// refuse to recreate an agent".
|
||||
async fn declare_new_agent(
|
||||
writer: &wanted::WantedWriter,
|
||||
hive: &str,
|
||||
agent: &str,
|
||||
) -> hive_jobq::scheduler::Outcome {
|
||||
use hive_jobq::scheduler::Outcome;
|
||||
use swarm_queue_client::wanted::AgentState;
|
||||
|
||||
// `create`, not `set`: a name that was destroyed earlier still carries a
|
||||
// terminal entry, and recreating the agent is the fresh deploy that
|
||||
// entry's own doc comment names as the way back. `set` would refuse it,
|
||||
// cancelling the deploy of an agent whose identity, repo and config the
|
||||
// swarm has just built.
|
||||
match writer.create(hive, agent, NEW_AGENT_WANTED_STATE).await {
|
||||
let existing = match writer.view(hive).await {
|
||||
Ok(declaration) => declaration.and_then(|d| d.agents.get(agent).map(|a| a.state)),
|
||||
Err(e) => return Outcome::Failed(format!("{e:#}")),
|
||||
};
|
||||
if let Some(state) = existing
|
||||
&& state != AgentState::Destroyed
|
||||
{
|
||||
tracing::debug!(
|
||||
hive,
|
||||
agent,
|
||||
?state,
|
||||
"agent already declared, leaving its wanted state alone"
|
||||
);
|
||||
return Outcome::Done;
|
||||
}
|
||||
match writer.set(hive, agent, NEW_AGENT_WANTED_STATE).await {
|
||||
Ok(_) => Outcome::Done,
|
||||
Err(e) => Outcome::Failed(format!("{e:#}")),
|
||||
}
|
||||
|
|
@ -894,7 +920,6 @@ fn declaration_target(
|
|||
responses(
|
||||
(status = 200, description = "the declaration as now published", body = Vec<AgentDeclaration>),
|
||||
(status = 400, description = "a name is not an identifier, the hive is not in this swarm, or the state is unknown (problem+json)", body = String),
|
||||
(status = 409, description = "the agent is already declared destroyed, a terminal state the request tries to move it off of (problem+json)", body = String),
|
||||
(status = 503, description = "no swarm queue is wired up (problem+json)", body = String),
|
||||
(status = 500, description = "the declaration could not be published (problem+json)", body = String),
|
||||
),
|
||||
|
|
@ -912,14 +937,6 @@ async fn set_agent_state(
|
|||
.into_string();
|
||||
|
||||
let declaration = writer.set(&hive, &agent, req.state).await.map_err(|e| {
|
||||
// A `TerminalStateError` is the caller's mistake, but not a malformed
|
||||
// request — the request is well-formed, it just conflicts with the
|
||||
// target resource's current (terminal) state, which per RFC 9110 is
|
||||
// what 409 exists for, not 400. Everything else here is the
|
||||
// pre-existing catch-all.
|
||||
if let Some(terminal) = e.downcast_ref::<wanted::TerminalStateError>() {
|
||||
return error_problem(axum::http::StatusCode::CONFLICT, &terminal.to_string());
|
||||
}
|
||||
tracing::warn!(hive = %hive, agent = %agent, error = %format!("{e:#}"), "declaring agent state failed");
|
||||
error_problem(
|
||||
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
|
|
|
|||
Loading…
Reference in a new issue