Refuse inconsistent State Anchor acquisition checkpoints

This commit is contained in:
ms2k 2026-07-15 17:41:05 +09:00
commit 8886436841
2 changed files with 104 additions and 10 deletions

View file

@ -159,18 +159,29 @@ impl<'a> StateAnchorCoordinator<'a> {
&expected_lease_held, &expected_lease_held,
&expected_anchor_intent, &expected_anchor_intent,
)? { )? {
AcquisitionCheckpoint::Absent => self.domain_store.apply(&[ AcquisitionCheckpoint::Absent => {
DomainStoreMutation::PutParticipant(Participant::bare_intent( if self.catalog_lease_checkpoint(&lease)? != CatalogLeaseCheckpoint::Absent {
generation_id, return Err(StateAnchorCoordinatorError::InvalidState(
ParticipantRole::PreparedLower, "catalog lease exists before Domain intent",
)), ));
DomainStoreMutation::PutParticipant(expected_prepared_intent.clone()), }
])?, self.domain_store.apply(&[
DomainStoreMutation::PutParticipant(Participant::bare_intent(
generation_id,
ParticipantRole::PreparedLower,
)),
DomainStoreMutation::PutParticipant(expected_prepared_intent.clone()),
])?;
}
AcquisitionCheckpoint::PreparedIntent => {} AcquisitionCheckpoint::PreparedIntent => {}
AcquisitionCheckpoint::Complete => { AcquisitionCheckpoint::Complete => {
self.catalog return if self.catalog_lease_checkpoint(&lease)? == CatalogLeaseCheckpoint::Exact {
.acquire_or_resume_lease_checked(artifact, &lease)?; Ok(lease)
return Ok(lease); } else {
Err(StateAnchorCoordinatorError::InvalidState(
"completed Domain acquisition lacks its catalog lease",
))
};
} }
} }
self.catalog self.catalog
@ -482,6 +493,22 @@ impl<'a> StateAnchorCoordinator<'a> {
Ok(role(&snapshot.participants, generation_id, participant_role)?.clone()) Ok(role(&snapshot.participants, generation_id, participant_role)?.clone())
} }
fn catalog_lease_checkpoint(
&self,
expected: &ArtifactLease,
) -> Result<CatalogLeaseCheckpoint, StateAnchorCoordinatorError> {
let snapshot = self.catalog.snapshot()?;
match snapshot.artifact_leases.iter().find(|lease| {
lease.domain_id == expected.domain_id && lease.generation_id == expected.generation_id
}) {
None => Ok(CatalogLeaseCheckpoint::Absent),
Some(observed) if observed == expected => Ok(CatalogLeaseCheckpoint::Exact),
Some(_) => Err(StateAnchorCoordinatorError::InvalidState(
"catalog generation lease ownership changed",
)),
}
}
fn release_catalog_lease_if_present( fn release_catalog_lease_if_present(
&self, &self,
authority: &StateAnchorReleaseAuthority, authority: &StateAnchorReleaseAuthority,
@ -593,6 +620,12 @@ enum AcquisitionCheckpoint {
Complete, Complete,
} }
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum CatalogLeaseCheckpoint {
Absent,
Exact,
}
fn acquisition_checkpoint( fn acquisition_checkpoint(
participants: &[Participant], participants: &[Participant],
generation_id: GenerationId, generation_id: GenerationId,

View file

@ -286,6 +286,67 @@ fn acquisition_resumes_after_catalog_lease_reopen() {
); );
} }
#[test]
fn completed_domain_checkpoint_refuses_absent_catalog_lease() {
let fixture = AcquisitionFixture::new(19);
let layout = StateAnchorLayout::create(&fixture.configuration).unwrap();
let store = fixture.open_store();
let coordinator = StateAnchorCoordinator::new(&store, &fixture.catalog);
let lease = coordinator
.begin_acquisition(fixture.generation_id, &fixture.artifact, &layout)
.unwrap();
fixture.catalog.release_lease(&lease).unwrap();
drop(store);
let store = fixture.open_store();
let coordinator = StateAnchorCoordinator::new(&store, &fixture.catalog);
assert!(
coordinator
.begin_acquisition(fixture.generation_id, &fixture.artifact, &layout)
.is_err()
);
assert_fixture_states(&store, fixture.generation_id);
assert!(
fixture
.catalog
.snapshot()
.unwrap()
.artifact_leases
.is_empty()
);
}
#[test]
fn absent_domain_checkpoint_refuses_preexisting_catalog_lease() {
let fixture = AcquisitionFixture::new(20);
let layout = StateAnchorLayout::create(&fixture.configuration).unwrap();
let lease = fixture.lease();
fixture
.catalog
.acquire_lease_checked(&fixture.artifact, &lease)
.unwrap();
let store = fixture.open_store();
let coordinator = StateAnchorCoordinator::new(&store, &fixture.catalog);
assert!(
coordinator
.begin_acquisition(fixture.generation_id, &fixture.artifact, &layout)
.is_err()
);
let snapshot = store.snapshot().unwrap();
assert!(!snapshot.participants.iter().any(|participant| {
participant.generation_id() == fixture.generation_id
&& matches!(
participant.role(),
ParticipantRole::PreparedLower | ParticipantRole::StateAnchor
)
}));
assert_eq!(
fixture.catalog.snapshot().unwrap().artifact_leases,
vec![lease]
);
}
fn assert_fixture_states(store: &HostCoordinatorDomainStore, generation_id: GenerationId) { fn assert_fixture_states(store: &HostCoordinatorDomainStore, generation_id: GenerationId) {
let snapshot = store.snapshot().unwrap(); let snapshot = store.snapshot().unwrap();
assert_eq!( assert_eq!(