Merge idunn/reincarnation-c2: phase-split handover, owner deadlines, forward retry, continuity yields
Cuts c2-phase-split, c2b-owner-deadlines, c2c-post-fence-forward, c2d-continuity-yields with their Soul passes; installed on Yggdrasil 2026-10-20 00:34 UTC. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
dbbdbf12fe
4 changed files with 4370 additions and 436 deletions
|
|
@ -52,12 +52,18 @@ web app and service through Idunn v2. The live evidence is on Yggdrasil:
|
|||
the admitted incarnation. The proof answers an Idunn challenge and is signed
|
||||
with the credential Idunn issued to that launch. Invariant 3 changes where the
|
||||
proof travels, not what it proves.
|
||||
5. **Every post-fencing phase ends.** Each phase after fencing has an admitted
|
||||
deadline. When a deadline expires, Idunn resolves it: stateless targets abort
|
||||
to the incumbent. Stateful targets get an explicit terminal record with a
|
||||
named recovery. A granted lease that the process does not pick up within its
|
||||
window is a named failure with an Idunn-owned recovery. It is never a silent
|
||||
wait. Resumable-forever is gone.
|
||||
5. **Every post-fencing phase has a deadline and is never a silent wait.** A
|
||||
deployment's stateless phase that passes its deadline aborts to its
|
||||
incumbent. A continuity past its deadline is left to the operator
|
||||
(operator-required incident) and retries its phase forward on a capped
|
||||
backoff, because aborting it would stop the only running copy of the admitted
|
||||
body (ruling post-fence-forward-retry), until a deployment that the
|
||||
deployment brake has released awaits the handover of its target and the
|
||||
continuity's route proof has not succeeded: then it yields through its own
|
||||
post-fencing abort, and the deployment fences what is admitted (rulings
|
||||
continuity-yields-to-deploy, c2d-yield-only-unproven-to-movable). A
|
||||
continuity that has proven its route is serving and never yields. A
|
||||
stateful deployment is left to the operator.
|
||||
6. **The admitted receipt describes the running incarnation.** Continuity
|
||||
commits update the admitted generation atomically, so the rollback path never
|
||||
meets drift between the receipt and the projection. Rollback demotes the exact
|
||||
|
|
|
|||
4574
src/control_plane.rs
4574
src/control_plane.rs
File diff suppressed because it is too large
Load diff
|
|
@ -475,6 +475,13 @@ pub const DEFAULT_AWAITING_READY_DEADLINE_SECONDS: u32 = 300;
|
|||
pub const DEFAULT_ROUTING_DEADLINE_SECONDS: u32 = 120;
|
||||
pub const DEFAULT_COMMITTING_DEADLINE_SECONDS: u32 = 120;
|
||||
|
||||
/// Idunn's own deadlines, not binding-declared: a new `PhaseDeadlines` field
|
||||
/// would change every v3 plan id (`recomputed_plan_id` re-encodes the plan).
|
||||
/// Warming is timed for every transaction, owner or not.
|
||||
pub const WARMING_DEADLINE_SECONDS: u32 = 600;
|
||||
/// A pre-fencing abort is timed from its beginning.
|
||||
pub const PRE_FENCING_ABORT_DEADLINE_SECONDS: u32 = 300;
|
||||
|
||||
/// The resolved per-phase deadline durations a plan carries. Entering a
|
||||
/// post-fencing phase stamps its deadline from them, and the deadline
|
||||
/// resolver ends the phase once it passes.
|
||||
|
|
|
|||
|
|
@ -32,6 +32,9 @@ pub(crate) const INCIDENT_RETENTION_MILLIS: u64 = 7 * 24 * 60 * 60 * 1000;
|
|||
#[serde(rename_all = "kebab-case")]
|
||||
pub(crate) enum IncidentCondition {
|
||||
ContinuityExhausted,
|
||||
/// A transaction past its deadline in a state Idunn will not end by
|
||||
/// itself. Subject: the transaction id.
|
||||
OperatorRequired,
|
||||
}
|
||||
|
||||
impl IncidentCondition {
|
||||
|
|
@ -40,6 +43,7 @@ impl IncidentCondition {
|
|||
pub(crate) const fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::ContinuityExhausted => "continuity-exhausted",
|
||||
Self::OperatorRequired => "operator-required",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -49,6 +53,7 @@ impl IncidentCondition {
|
|||
pub(crate) const fn ends_with_admission(self) -> bool {
|
||||
match self {
|
||||
Self::ContinuityExhausted => true,
|
||||
Self::OperatorRequired => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -271,6 +276,17 @@ impl IncidentStore {
|
|||
Ok(true)
|
||||
}
|
||||
|
||||
/// The subjects of every open incident of `condition`.
|
||||
pub(crate) fn open_subjects(&self, condition: IncidentCondition) -> Result<Vec<String>> {
|
||||
Ok(self
|
||||
.snapshot()?
|
||||
.records
|
||||
.into_iter()
|
||||
.filter(|record| record.is_open() && record.condition == condition)
|
||||
.map(|record| record.subject)
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Close the open incident for `(condition, subject)`, if there is one.
|
||||
/// `Ok(true)` means this call wrote the closure.
|
||||
pub(crate) fn close(
|
||||
|
|
@ -468,11 +484,12 @@ pub(crate) mod tests {
|
|||
);
|
||||
// The wire spelling readers match on is the spelling the key and
|
||||
// status use.
|
||||
let payload = rmp_serde::to_vec(&IncidentCondition::ContinuityExhausted).unwrap();
|
||||
assert_eq!(
|
||||
rmp_serde::from_slice::<String>(&payload).unwrap(),
|
||||
IncidentCondition::ContinuityExhausted.name()
|
||||
);
|
||||
for condition in [IncidentCondition::ContinuityExhausted, IncidentCondition::OperatorRequired] {
|
||||
let payload = rmp_serde::to_vec(&condition).unwrap();
|
||||
assert_eq!(rmp_serde::from_slice::<String>(&payload).unwrap(), condition.name());
|
||||
}
|
||||
assert_eq!(IncidentCondition::OperatorRequired.name(), "operator-required");
|
||||
assert!(!IncidentCondition::OperatorRequired.ends_with_admission());
|
||||
let payload = rmp_serde::to_vec(&CloseReason::NoLongerAdmitted).unwrap();
|
||||
assert_eq!(
|
||||
rmp_serde::from_slice::<String>(&payload).unwrap(),
|
||||
|
|
|
|||
Loading…
Reference in a new issue