Expose typed Prepared Lower failures

This commit is contained in:
ms2k 2026-07-19 03:18:50 +09:00
commit 30cd36dcdd

View file

@ -28,6 +28,7 @@ use crate::layer::{
verify_layout,
};
use crate::transport::TransportedImage;
use crate::{PreparedLowerError, PreparedLowerResult};
const PHYSICAL_LOCK_WAIT: Duration = Duration::from_secs(5);
const LOCK_RETRY: Duration = Duration::from_millis(2);
@ -74,7 +75,11 @@ impl PreparedLowerStore {
/// # Errors
///
/// Returns an error if the root cannot be opened or any fixed path is not a real directory.
pub fn initialize(root: impl Into<PathBuf>, catalog: Catalog) -> Result<Self> {
pub fn initialize(root: impl Into<PathBuf>, catalog: Catalog) -> PreparedLowerResult<Self> {
Self::initialize_inner(root, catalog).map_err(PreparedLowerError::confinement)
}
fn initialize_inner(root: impl Into<PathBuf>, catalog: Catalog) -> Result<Self> {
let root = root.into();
let root = File::open(&root).context("opening Storage Root")?;
Self::initialize_retained(root, catalog)
@ -84,7 +89,15 @@ impl PreparedLowerStore {
///
/// # Errors
/// Returns an error if Storage Root authority drifts or a fixed child is not a real directory.
pub fn under_storage_root(storage_root: &ConfinedPathPlan, catalog: Catalog) -> Result<Self> {
pub fn under_storage_root(
storage_root: &ConfinedPathPlan,
catalog: Catalog,
) -> PreparedLowerResult<Self> {
Self::under_storage_root_inner(storage_root, catalog)
.map_err(PreparedLowerError::confinement)
}
fn under_storage_root_inner(storage_root: &ConfinedPathPlan, catalog: Catalog) -> Result<Self> {
let storage_root = File::from(
storage_root
.open_authority()
@ -127,7 +140,15 @@ impl PreparedLowerStore {
///
/// # Errors
/// Refuses a non-canonical artifact record, catalog drift, or physical identity drift.
pub fn open_artifact_authority(&self, artifact: &PreparedArtifact) -> Result<File> {
pub fn open_artifact_authority(
&self,
artifact: &PreparedArtifact,
) -> PreparedLowerResult<File> {
self.open_artifact_authority_inner(artifact)
.map_err(PreparedLowerError::confinement)
}
fn open_artifact_authority_inner(&self, artifact: &PreparedArtifact) -> Result<File> {
let catalog_artifact = self
.catalog_artifact(artifact.prepared_artifact_id)?
.context("prepared artifact is absent from the catalog")?;
@ -145,7 +166,7 @@ impl PreparedLowerStore {
///
/// Refuses corrupt or unsupported OCI input, forged physical ownership, catalog conflicts,
/// resource limits, and publication failures.
pub fn prepare(&self, request: &PrepareRequest) -> Result<PreparedArtifact> {
pub fn prepare(&self, request: &PrepareRequest) -> PreparedLowerResult<PreparedArtifact> {
self.prepare_from_source(request, &LayoutSource::Ambient(&request.layout_path))
}
@ -159,10 +180,21 @@ impl PreparedLowerStore {
image: &TransportedImage,
id_map: IdMap,
destinations: Vec<PreparedDestination>,
) -> Result<PreparedArtifact> {
) -> PreparedLowerResult<PreparedArtifact> {
self.prepare_transported_inner(operation_id, image, id_map, destinations)
}
fn prepare_transported_inner(
&self,
operation_id: PrepareOperationId,
image: &TransportedImage,
id_map: IdMap,
destinations: Vec<PreparedDestination>,
) -> PreparedLowerResult<PreparedArtifact> {
let layout = image
.retained_layout()
.context("transported image has no retained layout authority")?;
.context("transported image has no retained layout authority")
.map_err(PreparedLowerError::confinement)?;
let request = PrepareRequest {
operation_id,
layout_path: image.layout_path.clone(),
@ -178,32 +210,46 @@ impl PreparedLowerStore {
&self,
request: &PrepareRequest,
layout: &LayoutSource<'_>,
) -> Result<PreparedArtifact> {
) -> PreparedLowerResult<PreparedArtifact> {
if request.source_identity.is_empty() || request.source_identity.len() > 4096 {
bail!("source identity is empty or exceeds 4096 bytes");
return Err(PreparedLowerError::layer_validation(anyhow::anyhow!(
"source identity is empty or exceeds 4096 bytes"
)));
}
let verified = verify_layout(layout, self.limits)?;
let verified =
verify_layout(layout, self.limits).map_err(PreparedLowerError::layer_validation)?;
if request.selected_manifest_digest != verified.manifest_digest {
bail!("selected manifest identity does not match verified OCI layout");
return Err(PreparedLowerError::manifest_selection(anyhow::anyhow!(
"selected manifest identity does not match verified OCI layout"
)));
}
let identities = Identities::new(request)?;
if let Some(existing) = self.available_artifact(identities.artifact_id)? {
identities.require_equivalent(&existing)?;
self.require_artifact_identity(&existing)?;
let identities = Identities::new(request).map_err(PreparedLowerError::layer_validation)?;
if let Some(existing) = self
.available_artifact(identities.artifact_id)
.map_err(PreparedLowerError::catalog)?
{
identities
.require_equivalent(&existing)
.map_err(PreparedLowerError::catalog)?;
self.require_artifact_identity(&existing)
.map_err(PreparedLowerError::confinement)?;
return Ok(existing);
}
let stage_relative = stage_path(request.operation_id);
let intent = identities.intent(request.operation_id, &stage_relative);
self.catalog.record_prepare(&intent)?;
self.catalog
.record_prepare(&intent)
.map_err(PreparedLowerError::catalog)?;
let stage = match self.create_stage(&stage_relative) {
Ok(stage) => stage,
Err(error) => {
let _ = self.catalog.remove_prepare(&intent);
return Err(error);
return Err(PreparedLowerError::materialization(error));
}
};
let (device, inode) = file_identity(&stage)?;
let (device, inode) =
file_identity(&stage).map_err(PreparedLowerError::operating_system)?;
let owned = PrepareOperation {
stage_state: PrepareStageState::Owned,
device: Some(device),
@ -217,7 +263,7 @@ impl PreparedLowerStore {
{
let _ = self.catalog.remove_prepare(&intent);
}
return Err(error.into());
return Err(PreparedLowerError::catalog(error));
}
if let Err(error) = apply_layout(layout, &stage, &verified, &request.id_map, self.limits)
.and_then(|()| materialize_destinations(&stage, &request.destinations, &request.id_map))
@ -229,9 +275,11 @@ impl PreparedLowerStore {
{
let _ = self.catalog.remove_prepare(&owned);
}
return Err(error);
return Err(PreparedLowerError::materialization(error));
}
self.publish_owned(&owned, &identities)
let physical_lock = self.physical_lock()?;
self.publish_owned(&owned, &identities, &physical_lock)
.map_err(PreparedLowerError::publication)
}
/// Recovers one exact durable prepare operation without adopting foreign paths.
@ -240,7 +288,15 @@ impl PreparedLowerStore {
///
/// Refuses unexpected paths, missing ownership evidence, identity mismatch, and catalog or
/// filesystem failures.
pub fn recover_prepare(&self, operation: &PrepareOperation) -> Result<PrepareRecovery> {
pub fn recover_prepare(
&self,
operation: &PrepareOperation,
) -> PreparedLowerResult<PrepareRecovery> {
self.recover_prepare_inner(operation)
.map_err(PreparedLowerError::recovery)
}
fn recover_prepare_inner(&self, operation: &PrepareOperation) -> Result<PrepareRecovery> {
let expected_stage = stage_path(operation.operation_id);
if operation.stage_path.as_bytes() != path_bytes(&expected_stage) {
bail!("prepare operation stage path is not canonical");
@ -281,12 +337,19 @@ impl PreparedLowerStore {
/// # 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)>> {
pub fn recover_preparations(
&self,
) -> PreparedLowerResult<Vec<(PrepareOperationId, PrepareRecovery)>> {
self.recover_preparations_inner()
.map_err(PreparedLowerError::recovery)
}
fn recover_preparations_inner(&self) -> Result<Vec<(PrepareOperationId, PrepareRecovery)>> {
let operations = self.catalog.snapshot()?.prepare_operations;
operations
.iter()
.map(|operation| {
self.recover_prepare(operation)
self.recover_prepare_inner(operation)
.map(|outcome| (operation.operation_id, outcome))
})
.collect()
@ -300,19 +363,32 @@ impl PreparedLowerStore {
pub fn gc_artifacts(
&self,
selected: &[PreparedArtifactId],
) -> Result<Vec<PreparedArtifactGcResult>> {
) -> PreparedLowerResult<Vec<PreparedArtifactGcResult>> {
self.gc_artifacts_inner(selected)
}
fn gc_artifacts_inner(
&self,
selected: &[PreparedArtifactId],
) -> PreparedLowerResult<Vec<PreparedArtifactGcResult>> {
let mut unique = std::collections::HashSet::new();
if selected.iter().any(|id| !unique.insert(*id)) {
bail!("prepared-artifact GC target is duplicated");
return Err(PreparedLowerError::reclaim(anyhow::anyhow!(
"prepared-artifact GC target is duplicated"
)));
}
let snapshot = self.catalog.snapshot()?;
let snapshot = self
.catalog
.snapshot()
.map_err(PreparedLowerError::catalog)?;
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")?;
.context("prepared-artifact GC target is absent")
.map_err(PreparedLowerError::reclaim)?;
let outcome = match artifact.state {
ArtifactState::Available => self.reclaim(artifact)?,
ArtifactState::Reclaiming => self.resume_reclaim(artifact)?,
@ -331,8 +407,11 @@ impl PreparedLowerStore {
///
/// Refuses leases, forged catalog rows, physical identity mismatch, foreign paths, and
/// filesystem or catalog failures.
pub fn reclaim(&self, expected: &PreparedArtifact) -> Result<ReclaimOutcome> {
let reclaiming = self.catalog.begin_reclaim(expected)?;
pub fn reclaim(&self, expected: &PreparedArtifact) -> PreparedLowerResult<ReclaimOutcome> {
let reclaiming = self
.catalog
.begin_reclaim(expected)
.map_err(PreparedLowerError::catalog)?;
self.resume_reclaim(&reclaiming)
}
@ -342,7 +421,16 @@ impl PreparedLowerStore {
///
/// Refuses non-reclaiming state, path/identity mismatch, missing physical evidence, and
/// catalog or filesystem failures.
pub fn resume_reclaim(&self, reclaiming: &PreparedArtifact) -> Result<ReclaimOutcome> {
pub fn resume_reclaim(
&self,
reclaiming: &PreparedArtifact,
) -> PreparedLowerResult<ReclaimOutcome> {
let _physical_lock = self.physical_lock()?;
self.resume_reclaim_inner(reclaiming)
.map_err(PreparedLowerError::reclaim)
}
fn resume_reclaim_inner(&self, reclaiming: &PreparedArtifact) -> Result<ReclaimOutcome> {
if reclaiming.state != ArtifactState::Reclaiming {
bail!("artifact is not reclaiming");
}
@ -352,7 +440,6 @@ impl PreparedLowerStore {
}
let quarantine = quarantine_path(reclaiming);
{
let _lock = self.physical_lock()?;
if let Some(canonical) = self.open_relative(&expected_artifact)? {
if file_identity(&canonical)? == (reclaiming.device, reclaiming.inode) {
self.rename_relative_noreplace(&expected_artifact, &quarantine)?;
@ -395,10 +482,10 @@ impl PreparedLowerStore {
&self,
operation: &PrepareOperation,
identities: &Identities,
_physical_lock: &ExclusiveFileLock,
) -> Result<PreparedArtifact> {
let stage = stage_path(operation.operation_id);
let artifact_path = artifact_path(operation.expected_artifact_id);
let _lock = self.physical_lock()?;
if let Some(existing) = self.open_relative(&artifact_path)? {
if let Some(row) = self.available_artifact(operation.expected_artifact_id)? {
identities.require_equivalent(&row)?;
@ -526,20 +613,22 @@ impl PreparedLowerStore {
Ok(())
}
fn physical_lock(&self) -> Result<ExclusiveFileLock> {
fn physical_lock(&self) -> PreparedLowerResult<ExclusiveFileLock> {
let deadline = Instant::now() + PHYSICAL_LOCK_WAIT;
loop {
let file = open_lock_at(&self.root)?;
let file = open_lock_at(&self.root).map_err(PreparedLowerError::locking)?;
match try_exclusive_flock(file) {
Ok(lock) => return Ok(lock),
Err(error) if error.kind() == io::ErrorKind::WouldBlock => {
let now = Instant::now();
if now >= deadline {
bail!("prepared-lower physical lock wait expired");
return Err(PreparedLowerError::locking(anyhow::anyhow!(
"prepared-lower physical lock wait expired"
)));
}
thread::sleep(LOCK_RETRY.min(deadline.saturating_duration_since(now)));
}
Err(error) => return Err(error.into()),
Err(error) => return Err(PreparedLowerError::locking(error)),
}
}
}