Implement exact bootstrap recovery and artifact GC

This commit is contained in:
ms2k 2026-07-17 05:38:43 +09:00
commit d25942596f
3 changed files with 1631 additions and 48 deletions

View file

@ -495,6 +495,9 @@ impl Participant {
| ParticipantState::Committed,
ParticipantState::ReleaseIntent,
) => validate_participant_release(self.ownership.as_ref(), next.ownership.as_ref()),
(ParticipantState::ReleaseIntent, ParticipantState::ReleaseIntent) => {
validate_participant_release(self.ownership.as_ref(), next.ownership.as_ref())
}
(old_state, new_state)
if old_state == new_state
&& matches!(
@ -502,7 +505,6 @@ impl Participant {
ParticipantState::LeaseHeld
| ParticipantState::Prepared
| ParticipantState::Committed
| ParticipantState::ReleaseIntent
| ParticipantState::Clean
)
&& self.ownership == next.ownership =>
@ -979,6 +981,15 @@ pub struct PreparedLowerOwnership {
pub path: Option<HostPath>,
pub device: Option<DeviceId>,
pub inode: Option<InodeId>,
pub released_state_anchor: Option<StateAnchorReleaseEvidence>,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct StateAnchorReleaseEvidence {
pub daemon: PidfsProcessIdentityV1,
pub user_namespace: NamespaceIdentity,
pub mount_namespace: NamespaceIdentity,
pub mount: MountIdentity,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
@ -1043,6 +1054,7 @@ impl PreparedLowerOwnership {
path: None,
device: None,
inode: None,
released_state_anchor: None,
}
}
@ -1053,6 +1065,7 @@ impl PreparedLowerOwnership {
&& self.path.is_none()
&& self.device.is_none()
&& self.inode.is_none()
&& self.released_state_anchor.is_none()
}
fn is_lease_held(&self) -> bool {
@ -1062,6 +1075,7 @@ impl PreparedLowerOwnership {
&& self.path.is_some()
&& self.device.is_some()
&& self.inode.is_some()
&& self.released_state_anchor.is_none()
}
fn validate_progress_to(&self, next: &Self) -> Result<(), RecordTransitionError> {
@ -1073,7 +1087,11 @@ impl PreparedLowerOwnership {
validate_prepared_lower_lease_progress(self.lease_state, next.lease_state)?;
validate_option_progress(self.path.as_ref(), next.path.as_ref())?;
validate_option_progress(self.device.as_ref(), next.device.as_ref())?;
validate_option_progress(self.inode.as_ref(), next.inode.as_ref())
validate_option_progress(self.inode.as_ref(), next.inode.as_ref())?;
validate_option_progress(
self.released_state_anchor.as_ref(),
next.released_state_anchor.as_ref(),
)
}
fn validate_release_to(&self, next: &Self) -> Result<(), RecordTransitionError> {
@ -1085,6 +1103,15 @@ impl PreparedLowerOwnership {
{
return Err(RecordTransitionError::EvidenceChanged);
}
let release_evidence_valid = match self.lease_state {
Some(ParticipantState::ReleaseIntent) => {
self.released_state_anchor == next.released_state_anchor
}
_ => next.released_state_anchor.is_some() || self.released_state_anchor.is_none(),
};
if !release_evidence_valid {
return Err(RecordTransitionError::EvidenceChanged);
}
match (self.lease_state, next.lease_state) {
(None, None)
| (Some(ParticipantState::ReleaseIntent), Some(ParticipantState::ReleaseIntent)) => {

View file

@ -366,6 +366,7 @@ fn prepared_ownership() -> PreparedLowerOwnership {
path: Some(HostPath::new(vec![b'/', 0xff])),
device: Some(DeviceId::new(27)),
inode: Some(InodeId::new(28)),
released_state_anchor: None,
}
}

View file

@ -514,6 +514,51 @@ fn prepared_lower_release_changes_only_the_lease_phase() {
.unwrap();
assert_eq!(active.validate_transition_to(&release), Ok(()));
let ParticipantOwnership::PreparedLower(mut with_anchor_evidence) =
release_ownership(ParticipantRole::PreparedLower)
else {
unreachable!()
};
with_anchor_evidence.released_state_anchor = Some(StateAnchorReleaseEvidence {
daemon: process(120),
user_namespace: namespace(),
mount_namespace: NamespaceIdentity {
device: DeviceId::new(121),
inode: InodeId::new(122),
},
mount: mount(),
});
let release_with_anchor_evidence = Participant::snapshot(
generation_id(),
ParticipantRole::PreparedLower,
ParticipantState::ReleaseIntent,
Some(ParticipantOwnership::PreparedLower(
with_anchor_evidence.clone(),
)),
)
.unwrap();
assert_eq!(
active.validate_transition_to(&release_with_anchor_evidence),
Ok(())
);
with_anchor_evidence
.released_state_anchor
.as_mut()
.unwrap()
.mount
.mount_id = MountId::new(123);
let changed_release_evidence = Participant::snapshot(
generation_id(),
ParticipantRole::PreparedLower,
ParticipantState::ReleaseIntent,
Some(ParticipantOwnership::PreparedLower(with_anchor_evidence)),
)
.unwrap();
assert_eq!(
release_with_anchor_evidence.validate_transition_to(&changed_release_evidence),
Err(RecordTransitionError::EvidenceChanged)
);
let ParticipantOwnership::PreparedLower(mut changed) =
release_ownership(ParticipantRole::PreparedLower)
else {