Bind prepared artifact selection against reclaim
This commit is contained in:
parent
d799cf4837
commit
1bd1d395d9
1 changed files with 209 additions and 21 deletions
|
|
@ -14,7 +14,9 @@ use lazaret_core::identity::{
|
|||
SelectedManifestDigest, SourceIdentity,
|
||||
};
|
||||
use lazaret_core::records::{ArtifactState, PrepareOperation, PrepareStageState, PreparedArtifact};
|
||||
use lazaret_linux::filesystem::{ExclusiveFileLock, try_exclusive_flock};
|
||||
use lazaret_linux::filesystem::{
|
||||
ExclusiveFileLock, SharedFileLock, try_exclusive_flock, try_shared_flock,
|
||||
};
|
||||
use lazaret_storage_catalog::{Catalog, CatalogError, CatalogRow};
|
||||
use rustix::fs::{
|
||||
AtFlags, Mode, OFlags, RenameFlags, ResolveFlags, mkdirat, openat2, renameat, renameat_with,
|
||||
|
|
@ -56,6 +58,7 @@ pub enum PrepareRecovery {
|
|||
pub enum ReclaimOutcome {
|
||||
Removed,
|
||||
ReplacementPreserved,
|
||||
RetainedBySelection,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
|
|
@ -64,6 +67,25 @@ pub struct PreparedArtifactGcResult {
|
|||
pub outcome: ReclaimOutcome,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PreparedArtifactSelection {
|
||||
artifact: PreparedArtifact,
|
||||
authority: File,
|
||||
claim: SharedFileLock,
|
||||
}
|
||||
|
||||
impl PreparedArtifactSelection {
|
||||
#[must_use]
|
||||
pub fn artifact(&self) -> &PreparedArtifact {
|
||||
&self.artifact
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn into_parts(self) -> (PreparedArtifact, File, SharedFileLock) {
|
||||
(self.artifact, self.authority, self.claim)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PreparedLowerStore {
|
||||
root: Arc<File>,
|
||||
|
|
@ -178,6 +200,52 @@ impl PreparedLowerStore {
|
|||
.context("prepared artifact path is absent")
|
||||
}
|
||||
|
||||
/// Selects the current equivalent artifact and retains a claim that excludes reclaim.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses semantic identity drift, catalog inconsistency, or physical identity drift.
|
||||
pub fn select_artifact(
|
||||
&self,
|
||||
expected: &PreparedArtifact,
|
||||
) -> PreparedLowerResult<Option<PreparedArtifactSelection>> {
|
||||
let physical_lock = self.physical_lock()?;
|
||||
self.select_artifact_inner(expected, &physical_lock)
|
||||
.map_err(PreparedLowerError::confinement)
|
||||
}
|
||||
|
||||
fn select_artifact_inner(
|
||||
&self,
|
||||
expected: &PreparedArtifact,
|
||||
_physical_lock: &ExclusiveFileLock,
|
||||
) -> Result<Option<PreparedArtifactSelection>> {
|
||||
let Some(artifact) = self.available_artifact(expected.prepared_artifact_id)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
require_equivalent_artifact(expected, &artifact)?;
|
||||
let path = artifact_path(artifact.prepared_artifact_id);
|
||||
let authority = self
|
||||
.open_relative(&path)?
|
||||
.context("selected artifact path is absent")?;
|
||||
require_identity(&authority, artifact.device, artifact.inode)?;
|
||||
let claim_file = self
|
||||
.open_relative(&path)?
|
||||
.context("selected artifact claim path is absent")?;
|
||||
require_identity(&claim_file, artifact.device, artifact.inode)?;
|
||||
let claim = try_shared_flock(claim_file)?;
|
||||
if self
|
||||
.catalog_artifact(artifact.prepared_artifact_id)?
|
||||
.as_ref()
|
||||
!= Some(&artifact)
|
||||
{
|
||||
bail!("selected artifact catalog ownership changed");
|
||||
}
|
||||
Ok(Some(PreparedArtifactSelection {
|
||||
artifact,
|
||||
authority,
|
||||
claim,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Materializes and atomically publishes one verified immutable prepared lower.
|
||||
///
|
||||
/// # Errors
|
||||
|
|
@ -498,11 +566,26 @@ impl PreparedLowerStore {
|
|||
/// Refuses leases, forged catalog rows, physical identity mismatch, foreign paths, and
|
||||
/// filesystem or catalog failures.
|
||||
pub fn reclaim(&self, expected: &PreparedArtifact) -> PreparedLowerResult<ReclaimOutcome> {
|
||||
let _physical_lock = self.physical_lock()?;
|
||||
let artifact = self
|
||||
.open_relative(&artifact_path(expected.prepared_artifact_id))
|
||||
.map_err(PreparedLowerError::reclaim)?
|
||||
.ok_or_else(|| PreparedLowerError::reclaim(anyhow::anyhow!("artifact is absent")))?;
|
||||
require_identity(&artifact, expected.device, expected.inode)
|
||||
.map_err(PreparedLowerError::reclaim)?;
|
||||
let _claim = match try_exclusive_flock(artifact) {
|
||||
Ok(claim) => claim,
|
||||
Err(error) if error.kind() == io::ErrorKind::WouldBlock => {
|
||||
return Ok(ReclaimOutcome::RetainedBySelection);
|
||||
}
|
||||
Err(error) => return Err(PreparedLowerError::reclaim(error)),
|
||||
};
|
||||
let reclaiming = self
|
||||
.catalog
|
||||
.begin_reclaim(expected)
|
||||
.map_err(PreparedLowerError::catalog)?;
|
||||
self.resume_reclaim(&reclaiming)
|
||||
self.resume_reclaim_inner(&reclaiming)
|
||||
.map_err(PreparedLowerError::reclaim)
|
||||
}
|
||||
|
||||
/// Resumes an exact durable reclaim after interruption.
|
||||
|
|
@ -516,10 +599,28 @@ impl PreparedLowerStore {
|
|||
reclaiming: &PreparedArtifact,
|
||||
) -> PreparedLowerResult<ReclaimOutcome> {
|
||||
let _physical_lock = self.physical_lock()?;
|
||||
let _claim = self
|
||||
.lock_reclaiming_artifact(reclaiming)
|
||||
.map_err(PreparedLowerError::reclaim)?;
|
||||
self.resume_reclaim_inner(reclaiming)
|
||||
.map_err(PreparedLowerError::reclaim)
|
||||
}
|
||||
|
||||
fn lock_reclaiming_artifact(&self, reclaiming: &PreparedArtifact) -> Result<ExclusiveFileLock> {
|
||||
for path in [
|
||||
artifact_path(reclaiming.prepared_artifact_id),
|
||||
quarantine_path(reclaiming),
|
||||
] {
|
||||
let Some(file) = self.open_relative(&path)? else {
|
||||
continue;
|
||||
};
|
||||
if file_identity(&file)? == (reclaiming.device, reclaiming.inode) {
|
||||
return Ok(try_exclusive_flock(file)?);
|
||||
}
|
||||
}
|
||||
bail!("reclaiming artifact and quarantine are both absent")
|
||||
}
|
||||
|
||||
fn resume_reclaim_inner(&self, reclaiming: &PreparedArtifact) -> Result<ReclaimOutcome> {
|
||||
if reclaiming.state != ArtifactState::Reclaiming {
|
||||
bail!("artifact is not reclaiming");
|
||||
|
|
@ -903,6 +1004,24 @@ fn artifact_from_operation(
|
|||
}
|
||||
}
|
||||
|
||||
fn require_equivalent_artifact(
|
||||
expected: &PreparedArtifact,
|
||||
observed: &PreparedArtifact,
|
||||
) -> Result<()> {
|
||||
if expected.state != ArtifactState::Available
|
||||
|| expected.prepared_artifact_id != observed.prepared_artifact_id
|
||||
|| expected.source_identity != observed.source_identity
|
||||
|| expected.selected_manifest_digest != observed.selected_manifest_digest
|
||||
|| expected.policy_id != observed.policy_id
|
||||
|| expected.id_map_digest != observed.id_map_digest
|
||||
|| expected.path != observed.path
|
||||
|| observed.state != ArtifactState::Available
|
||||
{
|
||||
bail!("selected prepared artifact identity inputs differ");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn file_identity(file: &File) -> Result<(DeviceId, InodeId)> {
|
||||
let stat = rustix::fs::fstat(file)?;
|
||||
Ok((DeviceId::new(stat.st_dev), InodeId::new(stat.st_ino)))
|
||||
|
|
|
|||
Loading…
Reference in a new issue