Complete nested prepared-lower reclaim ordering

This commit is contained in:
ms2k 2026-07-15 15:43:44 +09:00
commit 898271c430
2 changed files with 74 additions and 4 deletions

View file

@ -234,8 +234,8 @@ impl PreparedLowerStore {
.context("canonical replacement exists without quarantined old artifact")?;
require_identity(&quarantined, reclaiming.device, reclaiming.inode)?;
let replacement = self
.available_artifact(reclaiming.prepared_artifact_id)?
.context("canonical replacement exists without available ownership")?;
.catalog_artifact(reclaiming.prepared_artifact_id)?
.context("canonical replacement exists without catalog ownership")?;
require_identity(&canonical, replacement.device, replacement.inode)?;
}
} else {
@ -252,8 +252,8 @@ impl PreparedLowerStore {
row: CatalogRow::PreparedArtifact,
}) => {
let replacement = self
.available_artifact(reclaiming.prepared_artifact_id)?
.context("reclaim row changed without an available replacement")?;
.catalog_artifact(reclaiming.prepared_artifact_id)?
.context("reclaim row changed without an owned replacement")?;
self.require_artifact_identity(&replacement)?;
Ok(ReclaimOutcome::ReplacementPreserved)
}
@ -334,6 +334,15 @@ impl PreparedLowerStore {
}))
}
fn catalog_artifact(&self, id: PreparedArtifactId) -> Result<Option<PreparedArtifact>> {
Ok(self
.catalog
.snapshot()?
.prepared_artifacts
.into_iter()
.find(|artifact| artifact.prepared_artifact_id == id))
}
fn require_artifact_identity(&self, artifact: &PreparedArtifact) -> Result<()> {
let path = PathBuf::from(std::ffi::OsString::from_vec(
artifact.path.as_bytes().to_vec(),

View file

@ -719,6 +719,67 @@ fn replacement_winning_before_reclaimer_resumes_still_cleans_old_quarantine() {
);
}
#[test]
fn replacement_reclaim_start_does_not_orphan_prior_quarantine() {
let fixture = Fixture::new();
let bytes = layer(&[Entry {
path: "payload",
kind: EntryType::Regular,
data: b"same",
target: None,
mode: 0o644,
}]);
let first_request = fixture.request(55, &[(bytes, OCI_TAR)]);
let first = fixture.store.prepare(&first_request).unwrap();
let first_reclaiming = fixture.catalog.begin_reclaim(&first).unwrap();
let replacement_request = PrepareRequest {
operation_id: PrepareOperationId::new([56; 32]),
..first_request
};
let replacement = fixture.store.prepare(&replacement_request).unwrap();
let replacement_reclaiming = fixture.catalog.begin_reclaim(&replacement).unwrap();
assert_eq!(
fixture.store.resume_reclaim(&first_reclaiming).unwrap(),
ReclaimOutcome::ReplacementPreserved
);
assert!(fixture.artifact_root(&replacement).is_dir());
assert!(
fixture
.root
.join("quarantine")
.read_dir()
.unwrap()
.next()
.is_none()
);
assert_eq!(
fixture
.store
.resume_reclaim(&replacement_reclaiming)
.unwrap(),
ReclaimOutcome::Removed
);
assert!(
fixture
.catalog
.snapshot()
.unwrap()
.prepared_artifacts
.is_empty()
);
assert!(!fixture.artifact_root(&replacement).exists());
assert!(
fixture
.root
.join("quarantine")
.read_dir()
.unwrap()
.next()
.is_none()
);
}
#[test]
fn reclaim_refuses_preexisting_foreign_quarantine_path() {
let fixture = Fixture::new();