Implement exact bootstrap recovery and artifact GC
This commit is contained in:
parent
0f4845d4f8
commit
d25942596f
3 changed files with 1631 additions and 48 deletions
|
|
@ -7,5 +7,7 @@ mod transport;
|
|||
pub use layer::{
|
||||
IdMap, IdMapRange, PreparationLimits, PreparedDestination, PreparedDestinationKind,
|
||||
};
|
||||
pub use store::{PrepareRecovery, PrepareRequest, PreparedLowerStore, ReclaimOutcome};
|
||||
pub use store::{
|
||||
PrepareRecovery, PrepareRequest, PreparedArtifactGcResult, PreparedLowerStore, ReclaimOutcome,
|
||||
};
|
||||
pub use transport::{OciRegistryTransport, Platform, RegistryCredentials, TransportedImage};
|
||||
|
|
|
|||
|
|
@ -55,6 +55,12 @@ pub enum ReclaimOutcome {
|
|||
ReplacementPreserved,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct PreparedArtifactGcResult {
|
||||
pub prepared_artifact_id: PreparedArtifactId,
|
||||
pub outcome: ReclaimOutcome,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PreparedLowerStore {
|
||||
root: Arc<File>,
|
||||
|
|
@ -270,6 +276,55 @@ impl PreparedLowerStore {
|
|||
}
|
||||
}
|
||||
|
||||
/// Recovers the complete catalog snapshot of interrupted preparation operations.
|
||||
///
|
||||
/// # Errors
|
||||
/// Stops at the first foreign, mismatched, unreadable, or changed operation without treating
|
||||
/// it as owned absence.
|
||||
pub fn recover_preparations(&self) -> Result<Vec<(PrepareOperationId, PrepareRecovery)>> {
|
||||
let operations = self.catalog.snapshot()?.prepare_operations;
|
||||
operations
|
||||
.iter()
|
||||
.map(|operation| {
|
||||
self.recover_prepare(operation)
|
||||
.map(|outcome| (operation.operation_id, outcome))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Reclaims only the caller-selected exact catalog artifacts.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses an unknown target, duplicate target, any lease, catalog drift, or physical
|
||||
/// ownership mismatch. Reclaiming rows resume their existing exact operation.
|
||||
pub fn gc_artifacts(
|
||||
&self,
|
||||
selected: &[PreparedArtifactId],
|
||||
) -> Result<Vec<PreparedArtifactGcResult>> {
|
||||
let mut unique = std::collections::HashSet::new();
|
||||
if selected.iter().any(|id| !unique.insert(*id)) {
|
||||
bail!("prepared-artifact GC target is duplicated");
|
||||
}
|
||||
let snapshot = self.catalog.snapshot()?;
|
||||
let mut results = Vec::with_capacity(selected.len());
|
||||
for id in selected {
|
||||
let artifact = snapshot
|
||||
.prepared_artifacts
|
||||
.iter()
|
||||
.find(|artifact| artifact.prepared_artifact_id == *id)
|
||||
.context("prepared-artifact GC target is absent")?;
|
||||
let outcome = match artifact.state {
|
||||
ArtifactState::Available => self.reclaim(artifact)?,
|
||||
ArtifactState::Reclaiming => self.resume_reclaim(artifact)?,
|
||||
};
|
||||
results.push(PreparedArtifactGcResult {
|
||||
prepared_artifact_id: *id,
|
||||
outcome,
|
||||
});
|
||||
}
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// Moves an exact zero-lease artifact to quarantine, deletes it, and closes reclaim intent.
|
||||
///
|
||||
/// # Errors
|
||||
|
|
|
|||
|
|
@ -1135,6 +1135,43 @@ fn reclaim_refuses_forged_identity_and_leases() {
|
|||
assert!(fixture.artifact_root(&artifact).exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selected_gc_is_idempotent_only_at_the_durable_reclaim_boundary() {
|
||||
let fixture = Fixture::new();
|
||||
let artifact = fixture
|
||||
.store
|
||||
.prepare(&fixture.request(
|
||||
71,
|
||||
&[(
|
||||
layer(&[Entry {
|
||||
path: "payload",
|
||||
kind: EntryType::Regular,
|
||||
data: b"gc",
|
||||
target: None,
|
||||
mode: 0o644,
|
||||
}]),
|
||||
OCI_TAR,
|
||||
)],
|
||||
))
|
||||
.unwrap();
|
||||
let id = artifact.prepared_artifact_id;
|
||||
assert!(fixture.store.gc_artifacts(&[id, id]).is_err());
|
||||
|
||||
let lease = lazaret_core::records::ArtifactLease {
|
||||
prepared_artifact_id: id,
|
||||
domain_id: lazaret_core::identity::DomainId::new([72; 32]),
|
||||
generation_id: lazaret_core::identity::GenerationId::new(1),
|
||||
};
|
||||
fixture.catalog.acquire_lease(&lease).unwrap();
|
||||
assert!(fixture.store.gc_artifacts(&[id]).is_err());
|
||||
fixture.catalog.release_lease(&lease).unwrap();
|
||||
|
||||
let result = fixture.store.gc_artifacts(&[id]).unwrap();
|
||||
assert_eq!(result[0].prepared_artifact_id, id);
|
||||
assert_eq!(result[0].outcome, ReclaimOutcome::Removed);
|
||||
assert!(fixture.store.gc_artifacts(&[id]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn destination_layout_is_materialized_and_canonical_in_artifact_identity() {
|
||||
let fixture = Fixture::new();
|
||||
|
|
|
|||
Loading…
Reference in a new issue