Integrate legacy managed update maintenance and fenced recovery before reconciliation

This commit is contained in:
archipelago
2026-10-07 02:34:51 -04:00
parent 46fdc2764c
commit f79ecd11ae
7 changed files with 399 additions and 43 deletions
@@ -46,6 +46,7 @@ enum Phase {
Aborted,
Editing,
Starting,
Restoring,
Committed,
Restored,
}
@@ -57,19 +58,39 @@ struct Journal {
package: String,
phase: Phase,
members: Vec<Member>,
#[serde(default)]
cleanup_done: bool,
#[serde(default)]
target_startup_began: bool,
}
#[derive(Clone, Copy, Debug, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum Completion {
Committed,
Restored,
Aborted,
}
pub(crate) trait Supervisor: Sync {
/// Admission must be fenced and in-flight application work drained under
/// this durable operation before snapshots. A paused process is not proof.
/// Called only after every original writable recovery image is durable.
/// Legacy acquisition may gracefully stop AutoRemove writers, so its
/// destructive obligation is journaled before entering the controller.
/// It must fence/drain writers and finish coherent mounted-data backup;
/// image snapshots alone never establish application consistency.
/// Acquisition/release are idempotent and operation-owned across restart.
fn begin_barrier(
&self,
operation: &str,
originals: &[Unit],
recovery: bool,
) -> impl Future<Output = Result<()>> + Send;
fn verify_barrier(&self, operation: &str) -> impl Future<Output = Result<()>> + Send;
fn release_barrier(&self, operation: &str) -> impl Future<Output = Result<()>> + Send;
fn release_barrier(
&self,
operation: &str,
outcome: Completion,
) -> impl Future<Output = Result<()>> + Send;
/// Only an internal reviewed signed-manifest planner may supply this value;
/// browser parameters must never become a unit body or hook recipe.
fn prepare_target(
@@ -518,9 +539,8 @@ fn records(guard: &Guard) -> Result<Vec<Journal>> {
}
pub(crate) fn require_clear(guard: &Guard) -> Result<()> {
anyhow::ensure!(
records(guard)?
.iter()
.all(|r| matches!(r.phase, Phase::Committed | Phase::Restored | Phase::Aborted)),
records(guard)?.iter().all(|r| r.cleanup_done
&& matches!(r.phase, Phase::Committed | Phase::Restored | Phase::Aborted)),
"A supervised update needs recovery first"
);
Ok(())
@@ -574,6 +594,8 @@ pub(crate) async fn execute(
package: package.into(),
phase: Phase::Prepared,
members,
cleanup_done: false,
target_startup_began: false,
};
save(guard, &record)?;
for member in &record.members {
@@ -593,12 +615,6 @@ pub(crate) async fn execute(
Ok(())
}
async fn apply(guard: &Guard, record: &mut Journal, supervisor: &impl Supervisor) -> Result<()> {
let originals: Vec<_> = record
.members
.iter()
.map(|member| member.original.clone())
.collect();
supervisor.begin_barrier(&record.id, &originals).await?;
for index in 0..record.members.len() {
let member = &record.members[index];
supervisor.validate_original_file(&member.original).await?;
@@ -606,7 +622,6 @@ async fn apply(guard: &Guard, record: &mut Journal, supervisor: &impl Supervisor
supervisor.read(&member.original.name).await? == member.original.body,
"Unit edited before update; originals retained"
);
supervisor.verify_barrier(&record.id).await?;
let image = supervisor
.snapshot(&member.original, &record.id, &member.original_tag)
.await?;
@@ -626,9 +641,19 @@ async fn apply(guard: &Guard, record: &mut Journal, supervisor: &impl Supervisor
// The image acknowledgement becomes durable before any original stop.
save(guard, record)?;
}
supervisor.verify_barrier(&record.id).await?;
record.phase = Phase::Editing;
save(guard, record)?;
// The legacy controller may stop original writers. Every writable layer
// is already recoverable and this obligation survives cancellation.
let originals: Vec<_> = record
.members
.iter()
.map(|member| member.original.clone())
.collect();
supervisor
.begin_barrier(&record.id, &originals, false)
.await?;
supervisor.verify_barrier(&record.id).await?;
for member in record.members.iter().rev() {
supervisor.verify_barrier(&record.id).await?;
supervisor.stop(&member.original.name).await?;
@@ -644,6 +669,8 @@ async fn apply(guard: &Guard, record: &mut Journal, supervisor: &impl Supervisor
}
supervisor.reload().await?;
record.phase = Phase::Starting;
record.target_startup_began = true;
record.cleanup_done = false;
save(guard, record)?;
for member in &record.members {
supervisor.start(&member.original.name).await?;
@@ -663,11 +690,14 @@ async fn apply(guard: &Guard, record: &mut Journal, supervisor: &impl Supervisor
}
record.phase = Phase::Committed;
save(guard, record)?;
supervisor.release_barrier(&record.id).await?;
supervisor
.release_barrier(&record.id, Completion::Committed)
.await?;
for member in &record.members {
guard.release_hold(&member.original.name, &record.id)?;
}
Ok(())
record.cleanup_done = true;
save(guard, record)
}
async fn restore(guard: &Guard, record: &mut Journal, supervisor: &impl Supervisor) -> Result<()> {
if record.phase == Phase::Prepared {
@@ -689,12 +719,25 @@ async fn restore(guard: &Guard, record: &mut Journal, supervisor: &impl Supervis
}
record.phase = Phase::Aborted;
save(guard, record)?;
supervisor.release_barrier(&record.id).await?;
supervisor
.release_barrier(&record.id, Completion::Aborted)
.await?;
for member in &record.members {
guard.release_hold(&member.original.name, &record.id)?;
}
return Ok(());
record.cleanup_done = true;
return save(guard, record);
}
let originals: Vec<_> = record
.members
.iter()
.map(|member| member.original.clone())
.collect();
record.phase = Phase::Restoring;
save(guard, record)?;
supervisor
.begin_barrier(&record.id, &originals, true)
.await?;
supervisor.verify_barrier(&record.id).await?;
// Refuse to overwrite a foreign edit before stopping any surviving member.
for member in &record.members {
@@ -767,25 +810,44 @@ async fn restore(guard: &Guard, record: &mut Journal, supervisor: &impl Supervis
}
record.phase = Phase::Restored;
save(guard, record)?;
supervisor.release_barrier(&record.id).await
supervisor
.release_barrier(&record.id, Completion::Restored)
.await?;
record.cleanup_done = true;
save(guard, record)
}
pub(crate) async fn recover(guard: &Guard, supervisor: &impl Supervisor) -> Result<()> {
for mut record in records(guard)? {
if record.cleanup_done {
continue;
}
match record.phase {
Phase::Committed | Phase::Aborted => {
supervisor.release_barrier(&record.id).await?;
let outcome = if record.phase == Phase::Committed {
Completion::Committed
} else {
Completion::Aborted
};
supervisor.release_barrier(&record.id, outcome).await?;
for member in &record.members {
guard.release_hold(&member.original.name, &record.id)?;
}
}
Phase::Restored => {
supervisor.release_barrier(&record.id).await?;
supervisor
.release_barrier(&record.id, Completion::Restored)
.await?;
} // Never release a newer owner.
_ => restore(guard, &mut record, supervisor).await?,
}
record.cleanup_done = true;
save(guard, &record)?;
}
Ok(())
}
pub(crate) fn needs_recovery(guard: &Guard) -> Result<bool> {
Ok(records(guard)?.iter().any(|record| !record.cleanup_done))
}
#[cfg(test)]
mod tests {
@@ -837,7 +899,12 @@ mod tests {
}
}
impl Supervisor for Mock {
async fn begin_barrier(&self, operation: &str, _originals: &[Unit]) -> Result<()> {
async fn begin_barrier(
&self,
operation: &str,
_originals: &[Unit],
_recovery: bool,
) -> Result<()> {
let mut held = self.barrier.lock().unwrap();
anyhow::ensure!(
held.as_deref().is_none_or(|id| id == operation),
@@ -853,7 +920,7 @@ mod tests {
);
Ok(())
}
async fn release_barrier(&self, operation: &str) -> Result<()> {
async fn release_barrier(&self, operation: &str, _outcome: Completion) -> Result<()> {
anyhow::ensure!(
!self.fail_barrier_release.load(Ordering::SeqCst),
"Barrier release unavailable"
@@ -1062,7 +1129,9 @@ mod tests {
execute(&guard, "movie", &[Mock::target()], &runtime)
.await
.unwrap();
let record = records(&guard).unwrap().pop().unwrap();
let mut record = records(&guard).unwrap().pop().unwrap();
record.cleanup_done = false;
save(&guard, &record).unwrap();
guard.hold("movie", &record.id).unwrap();
runtime.calls.lock().unwrap().clear();
recover(&guard, &runtime).await.unwrap();
@@ -1125,6 +1194,7 @@ mod tests {
.unwrap();
let mut record = records(&guard).unwrap().pop().unwrap();
record.phase = Phase::Starting;
record.cleanup_done = false;
*runtime.barrier.lock().unwrap() = Some(record.id.clone());
save(&guard, &record).unwrap();
runtime.calls.lock().unwrap().clear();
@@ -1150,6 +1220,7 @@ mod tests {
.unwrap();
let mut record = records(&guard).unwrap().pop().unwrap();
record.phase = Phase::Starting;
record.cleanup_done = false;
*runtime.barrier.lock().unwrap() = Some(record.id.clone());
save(&guard, &record).unwrap();
*runtime.body.lock().unwrap() = "operator replaced unit".into();