Idunn: pin static redeploy and stuck-flip behavior; document both
Release ids are per transaction, so redeploying an unchanged tree with a changed site file yields a new release (the test asserts distinct ids, the first release keeping its carry, current carrying the new file); previous_release_id can never equal release_id and no refusal is needed. A flip whose current link was replaced by a directory after the intent retries visibly: last_error is shown by idunn status as 'waiting on', cancel is refused, and the docs name the by-hand fix. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
dc86c6326c
commit
be98521120
2 changed files with 45 additions and 1 deletions
|
|
@ -210,7 +210,15 @@ What `current_link` names is observed from the symlink and confirmed by the
|
|||
latest Admitted static transaction's `flip.release_id`; it is stored nowhere
|
||||
else. A rollback is a flip back to `flip.previous_release_id`: release
|
||||
directories stay, and Idunn prunes none. The brake is read exactly as for a
|
||||
service, naming the binding's runtime id and the sealed release id. A new
|
||||
service, naming the binding's runtime id and the sealed release id. Release ids are per transaction, so
|
||||
redeploying an unchanged tarball with a changed site file is a new release that
|
||||
carries the new file, never an in-place re-carry of an existing one: the carry
|
||||
is part of a release. If `current_link` is replaced by a directory or by a link
|
||||
that names nothing under the release root after the flip was recorded, the
|
||||
transaction keeps retrying with backoff: `idunn status --command <id>` shows the
|
||||
error as "waiting on ...", operator cancel is refused (Starting, not held), and
|
||||
the fix is by hand (restore the link or remove the directory); it then
|
||||
completes. A bounded retry is follow-up work. A new
|
||||
upstream tarball enters as a recipe `external_input` with a pinned url and
|
||||
sha256, so each release is a recipe commit whose pin is its provenance.
|
||||
|
||||
|
|
|
|||
|
|
@ -26102,6 +26102,11 @@ GAMECULT_RUNTIME_PRESENCE_IDENTITY = "/etc/gamecult/service/runtime-presence-ide
|
|||
let first_release = first_done.flip.context("no first flip")?.release_id;
|
||||
assert_eq!(w.current_release().as_deref(), Some(first_release.as_str()));
|
||||
|
||||
// The same tree is deployed again with a changed site config. Release
|
||||
// ids are per transaction (the plan names its own incarnation), so the
|
||||
// redeploy is a new release that carries the new file; the first keeps
|
||||
// the bytes it was flipped with. No redeploy can name its own release.
|
||||
std::fs::write(w.site_config(), SITE_CONFIG_TWO)?;
|
||||
enqueue_deploy(&w.world, "up-service-2")?;
|
||||
let second_id = w.transaction_of("up-service-2")?;
|
||||
let second = w.run_to_brake(&second_id)?;
|
||||
|
|
@ -26115,6 +26120,11 @@ GAMECULT_RUNTIME_PRESENCE_IDENTITY = "/etc/gamecult/service/runtime-presence-ide
|
|||
assert_eq!(flip.previous_release_id.as_deref(), Some(first_release.as_str()));
|
||||
assert_ne!(flip.release_id, first_release);
|
||||
assert_eq!(w.current_release().as_deref(), Some(flip.release_id.as_str()));
|
||||
assert_eq!(std::fs::read(w.current_link().join("config.json"))?, SITE_CONFIG_TWO);
|
||||
assert_eq!(
|
||||
std::fs::read(w.site.join("releases").join(&first_release).join("config.json"))?,
|
||||
SITE_CONFIG_ONE
|
||||
);
|
||||
// The first release is still on disk: rollback is a flip back to it.
|
||||
assert!(w.site.join("releases").join(&first_release).join("dist/index.html").is_file());
|
||||
Ok(())
|
||||
|
|
@ -26315,5 +26325,31 @@ GAMECULT_RUNTIME_PRESENCE_IDENTITY = "/etc/gamecult/service/runtime-presence-ide
|
|||
assert_eq!(lifted, expected, "some field changed in the lift");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn static_release_with_a_foreign_current_retries_visibly_and_cannot_be_cancelled() -> Result<()> {
|
||||
let w = static_world()?;
|
||||
let id = w.transaction_of("up-service")?;
|
||||
let waiting = w.run_to_brake(&id)?;
|
||||
w.release_brake(&waiting)?;
|
||||
w.tick_until(&id, "the flip intent", |t| t.flip.is_some())?;
|
||||
// The operator replaces the link's place with a directory.
|
||||
std::fs::create_dir(w.current_link())?;
|
||||
for _ in 0..4 {
|
||||
w.world.engine.resume_backoff.lock().unwrap().clear();
|
||||
let _ = w.world.engine.run_scheduler_tick();
|
||||
}
|
||||
let stuck = latest_id(&w.world, &id)?;
|
||||
assert!(!stuck.is_terminal() && stuck.phase == DeploymentPhase::Starting, "{stuck:?}");
|
||||
assert!(stuck.pre_fencing_abort.is_none(), "an error after the intent ended the flip");
|
||||
let reason = stuck.last_error.clone().context("the retry kept no error")?;
|
||||
assert!(reason.contains("not a symlink"), "{reason}");
|
||||
let printed = printed_status(&w.world)?;
|
||||
assert!(printed.contains(&format!("waiting on {reason}")), "{printed}");
|
||||
let refused = cancel(&w.world.state_store, "up-service", "operator").unwrap_err();
|
||||
assert!(format!("{refused:#}").contains("can only be cancelled"), "{refused:#}");
|
||||
Ok(())
|
||||
}
|
||||
// END static-release tests
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue