Watch
0
0
Fork
You've already forked hyperhive
0

Roll back stored settings when a creation fails, and keep form ids unique

create_agent puts back the caps and inference settings it replaced if a
later store or the job insert fails, so a refused creation leaves nothing
behind for the name. The shared field components take an id prefix so the
create dialog and an open editor never share a DOM id. The queue check now
follows the hive roster check, the 422 is documented, and the copy says
empty rather than closed and that destroy keeps stored settings.
This commit is contained in:
iris 2026-10-15 11:44:16 +02:00 • committed by mara
commit 37c91eb916
7 changed files with 288 additions and 47 deletions

View file

@ -257,24 +257,28 @@ pub(crate) async fn put_agent_limits(
Ok(Json(value.into()))
}
/// What [`store`] replaced: the caps that were stored, or `None` for no key.
pub(crate) struct Replaced(Option<AgentCaps>);
/// Replace `agent`'s stored caps with `value`.
pub(crate) async fn store(
client: &async_nats::Client,
agent: &str,
value: &AgentCaps,
) -> Result<(), ProblemDetails> {
) -> Result<Replaced, ProblemDetails> {
let store = open_or_create(client).await?;
for _ in 0..MAX_ATTEMPTS {
let (current, revision, undecodable) = read_or_default::<AgentCaps>(&store, agent)
.await
.map_err(|e| queue_problem(&e))?;
let replaced = Replaced(revision.map(|_| current.clone()));
if (current == *value && !undecodable)
|| write(&store, agent, value, revision)
.await
.map_err(|e| queue_problem(&e))?
{
tracing::info!(%agent, ?value, "set agent limits");
return Ok(());
return Ok(replaced);
}
}
Err(queue_problem(&anyhow::anyhow!(
@ -282,6 +286,24 @@ pub(crate) async fn store(
)))
}
/// Put back what [`store`] replaced. Best effort: a failure is logged, since
/// the caller is already on its way to an error of its own.
pub(crate) async fn restore(client: &async_nats::Client, agent: &str, replaced: Replaced) {
let put_back = match replaced.0 {
Some(previous) => store(client, agent, &previous).await.map(|_| ()),
None => match open_or_create(client).await {
Ok(store) => store
.delete(agent)
.await
.map_err(|e| queue_problem(&anyhow::Error::new(e))),
Err(e) => Err(e),
},
};
if let Err(e) = put_back {
tracing::warn!(%agent, error = ?e, "could not put back the agent's limits");
}
}
#[utoipa::path(
delete,
path = "/api/agents/{name}/limits",
@ -441,7 +463,7 @@ pub(crate) mod tests {
}
/// One hive, `pr1ma`, on `client`.
fn state(client: &async_nats::Client) -> crate::AppState {
pub(crate) fn state(client: &async_nats::Client) -> crate::AppState {
crate::AppState {
hives: Arc::new(vec![crate::HiveEntry {
name: "pr1ma".to_owned(),
@ -512,7 +534,7 @@ pub(crate) mod tests {
/// The raw value a hive reads for `agent`, or `None` for no key or a
/// deleted one.
async fn published(client: &async_nats::Client, agent: &str) -> Option<String> {
pub(crate) async fn published(client: &async_nats::Client, agent: &str) -> Option<String> {
let store = swarm_queue_client::agent_limits::open_or_create(client)
.await
.expect("bucket");
@ -735,4 +757,33 @@ pub(crate) mod tests {
assert_eq!(answer.0, Caps::default());
assert_eq!(published(&client, "atlas").await.as_deref(), Some("{}"));
}
/// `restore` puts back the caps a `store` replaced, or takes the key out
/// again when there was none.
#[tokio::test]
async fn restore_undoes_a_store() {
let nats = TestNats::start();
let client = nats.client().await;
let caps = |json: &str| {
serde_json::from_str::<Caps>(json)
.expect("caps")
.into_stored()
.expect("valid")
};
let first = super::store(&client, "atlas", &caps(CAPPED))
.await
.expect("stored");
super::restore(&client, "atlas", first).await;
assert_eq!(published(&client, "atlas").await, None);
super::store(&client, "atlas", &caps(CAPPED))
.await
.expect("stored");
let second = super::store(&client, "atlas", &caps(r#"{"cpu_quota":"100%"}"#))
.await
.expect("replaced");
super::restore(&client, "atlas", second).await;
assert_eq!(published(&client, "atlas").await.as_deref(), Some(CAPPED));
}
}