Bind prepared artifact selection against reclaim
This commit is contained in:
parent
d799cf4837
commit
1bd1d395d9
8 changed files with 209 additions and 21 deletions
|
|
@ -11,6 +11,7 @@ use lazaret_core::records::{
|
||||||
Generation, GenerationIdentity, Participant, ParticipantOwnership, ParticipantRole,
|
Generation, GenerationIdentity, Participant, ParticipantOwnership, ParticipantRole,
|
||||||
ParticipantState, PreparedArtifact,
|
ParticipantState, PreparedArtifact,
|
||||||
};
|
};
|
||||||
|
use lazaret_linux::filesystem::SharedFileLock;
|
||||||
use lazaret_linux::process::{ProcessOwnershipContext, current_process_identity};
|
use lazaret_linux::process::{ProcessOwnershipContext, current_process_identity};
|
||||||
use lazaret_runtime_supervisor::sandbox::SandboxBundle;
|
use lazaret_runtime_supervisor::sandbox::SandboxBundle;
|
||||||
use lazaret_runtime_supervisor::supervision::RuntimeSupervisorLaunch;
|
use lazaret_runtime_supervisor::supervision::RuntimeSupervisorLaunch;
|
||||||
|
|
@ -65,6 +66,7 @@ pub struct BootstrapPlan<'a> {
|
||||||
pub configuration: &'a ResolvedDomainConfiguration,
|
pub configuration: &'a ResolvedDomainConfiguration,
|
||||||
pub generation_identity: GenerationIdentity,
|
pub generation_identity: GenerationIdentity,
|
||||||
pub prepared_artifact: &'a PreparedArtifact,
|
pub prepared_artifact: &'a PreparedArtifact,
|
||||||
|
pub prepared_artifact_selection: Option<SharedFileLock>,
|
||||||
pub state_anchor_layout: &'a StateAnchorLayout<'a>,
|
pub state_anchor_layout: &'a StateAnchorLayout<'a>,
|
||||||
pub runtime_launch: RuntimeSupervisorLaunch,
|
pub runtime_launch: RuntimeSupervisorLaunch,
|
||||||
pub sandbox_bundle: &'a SandboxBundle,
|
pub sandbox_bundle: &'a SandboxBundle,
|
||||||
|
|
@ -281,15 +283,21 @@ impl<'a> CoordinatorLifecycleAdapter<'a> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn lease_prepared_lower(&self) -> Result<(), LifecycleServiceError> {
|
fn lease_prepared_lower(&mut self) -> Result<(), LifecycleServiceError> {
|
||||||
self.state_anchor()
|
self.state_anchor()
|
||||||
.begin_acquisition(
|
.begin_acquisition(
|
||||||
self.plan.generation_identity.generation_id,
|
self.plan.generation_identity.generation_id,
|
||||||
self.plan.prepared_artifact,
|
self.plan.prepared_artifact,
|
||||||
self.plan.state_anchor_layout,
|
self.plan.state_anchor_layout,
|
||||||
)
|
)
|
||||||
.map(|_| ())
|
.map_err(failure)?;
|
||||||
.map_err(failure)
|
drop(
|
||||||
|
self.plan
|
||||||
|
.prepared_artifact_selection
|
||||||
|
.take()
|
||||||
|
.ok_or_else(|| LifecycleServiceError("prepared artifact claim is absent".into()))?,
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mount_state_anchor(&mut self) -> Result<(), LifecycleServiceError> {
|
fn mount_state_anchor(&mut self) -> Result<(), LifecycleServiceError> {
|
||||||
|
|
|
||||||
|
|
@ -132,7 +132,7 @@ fn run_private_client_bootstrap(arguments: &[OsString]) -> Result<(), PrivateCli
|
||||||
.enable_all()
|
.enable_all()
|
||||||
.build()
|
.build()
|
||||||
.map_err(failure)?;
|
.map_err(failure)?;
|
||||||
let selection = runtime
|
let mut selection = runtime
|
||||||
.block_on(select_runtime(
|
.block_on(select_runtime(
|
||||||
&configuration,
|
&configuration,
|
||||||
Arc::clone(&store),
|
Arc::clone(&store),
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ use lazaret_core::records::{
|
||||||
Generation, GenerationIdentity, HostProcessOwnershipSlot, PreparedArtifact, Session,
|
Generation, GenerationIdentity, HostProcessOwnershipSlot, PreparedArtifact, Session,
|
||||||
};
|
};
|
||||||
use lazaret_ignored_workspace::git_common_directory;
|
use lazaret_ignored_workspace::git_common_directory;
|
||||||
|
use lazaret_linux::filesystem::SharedFileLock;
|
||||||
use lazaret_linux::filesystem::{RegularFileMailbox, mount_identity, namespace_identity};
|
use lazaret_linux::filesystem::{RegularFileMailbox, mount_identity, namespace_identity};
|
||||||
use lazaret_linux::process::{
|
use lazaret_linux::process::{
|
||||||
ProcessOwnershipContext, TargetCommand, child_broker, current_process_identity,
|
ProcessOwnershipContext, TargetCommand, child_broker, current_process_identity,
|
||||||
|
|
@ -76,6 +77,7 @@ pub struct ColdStartInputs<'a> {
|
||||||
pub generation: Generation,
|
pub generation: Generation,
|
||||||
pub prepared_artifact: &'a PreparedArtifact,
|
pub prepared_artifact: &'a PreparedArtifact,
|
||||||
pub prepared_artifact_authority: &'a File,
|
pub prepared_artifact_authority: &'a File,
|
||||||
|
pub prepared_artifact_selection: SharedFileLock,
|
||||||
pub prepared_artifact_label: &'a Path,
|
pub prepared_artifact_label: &'a Path,
|
||||||
pub root_process: RootProcess,
|
pub root_process: RootProcess,
|
||||||
pub container_id: ContainerId,
|
pub container_id: ContainerId,
|
||||||
|
|
@ -135,6 +137,7 @@ pub struct ColdStart<'a> {
|
||||||
generation_identity: GenerationIdentity,
|
generation_identity: GenerationIdentity,
|
||||||
prepared_artifact: &'a PreparedArtifact,
|
prepared_artifact: &'a PreparedArtifact,
|
||||||
state_anchor_layout: StateAnchorLayout<'a>,
|
state_anchor_layout: StateAnchorLayout<'a>,
|
||||||
|
prepared_artifact_selection: Option<SharedFileLock>,
|
||||||
runtime_launch: Option<RuntimeSupervisorLaunch>,
|
runtime_launch: Option<RuntimeSupervisorLaunch>,
|
||||||
sandbox_bundle: SandboxBundle,
|
sandbox_bundle: SandboxBundle,
|
||||||
network_policy: lazaret_core::network::CanonicalDerivedNetworkPolicy,
|
network_policy: lazaret_core::network::CanonicalDerivedNetworkPolicy,
|
||||||
|
|
@ -256,6 +259,7 @@ impl<'a> ColdStart<'a> {
|
||||||
generation_identity,
|
generation_identity,
|
||||||
prepared_artifact: inputs.prepared_artifact,
|
prepared_artifact: inputs.prepared_artifact,
|
||||||
state_anchor_layout,
|
state_anchor_layout,
|
||||||
|
prepared_artifact_selection: Some(inputs.prepared_artifact_selection),
|
||||||
runtime_launch: Some(runtime_launch),
|
runtime_launch: Some(runtime_launch),
|
||||||
sandbox_bundle,
|
sandbox_bundle,
|
||||||
network_policy,
|
network_policy,
|
||||||
|
|
@ -371,6 +375,7 @@ impl<'a> ColdStart<'a> {
|
||||||
configuration: self.configuration,
|
configuration: self.configuration,
|
||||||
generation_identity: self.generation_identity.clone(),
|
generation_identity: self.generation_identity.clone(),
|
||||||
prepared_artifact: self.prepared_artifact,
|
prepared_artifact: self.prepared_artifact,
|
||||||
|
prepared_artifact_selection: self.prepared_artifact_selection.take(),
|
||||||
state_anchor_layout: &self.state_anchor_layout,
|
state_anchor_layout: &self.state_anchor_layout,
|
||||||
runtime_launch: self
|
runtime_launch: self
|
||||||
.runtime_launch
|
.runtime_launch
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ use lazaret_core::identity::{
|
||||||
PrepareOperationId, SandboxId,
|
PrepareOperationId, SandboxId,
|
||||||
};
|
};
|
||||||
use lazaret_core::records::{DurableDecision, Generation, PreparedArtifact, VolumeKind};
|
use lazaret_core::records::{DurableDecision, Generation, PreparedArtifact, VolumeKind};
|
||||||
|
use lazaret_linux::filesystem::SharedFileLock;
|
||||||
use lazaret_linux::platform::{PlatformObservation, check_platform};
|
use lazaret_linux::platform::{PlatformObservation, check_platform};
|
||||||
use lazaret_linux::user_namespace::UserNamespaceMap;
|
use lazaret_linux::user_namespace::UserNamespaceMap;
|
||||||
use lazaret_prepared_lower::{
|
use lazaret_prepared_lower::{
|
||||||
|
|
@ -32,6 +33,8 @@ use crate::prepared_worker::prepare_in_user_namespace;
|
||||||
use crate::recovery::recover_before_cold_start;
|
use crate::recovery::recover_before_cold_start;
|
||||||
use crate::sandbox_init::PRIVATE_SANDBOX_INIT_ROLE;
|
use crate::sandbox_init::PRIVATE_SANDBOX_INIT_ROLE;
|
||||||
|
|
||||||
|
const PREPARED_SELECTION_ATTEMPTS: usize = 16;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum RuntimeSelectionError {
|
pub enum RuntimeSelectionError {
|
||||||
PreparedLower(PreparedLowerError),
|
PreparedLower(PreparedLowerError),
|
||||||
|
|
@ -78,6 +81,7 @@ pub struct RuntimeSelection {
|
||||||
catalog: Catalog,
|
catalog: Catalog,
|
||||||
artifact: PreparedArtifact,
|
artifact: PreparedArtifact,
|
||||||
artifact_authority: File,
|
artifact_authority: File,
|
||||||
|
artifact_selection: Option<SharedFileLock>,
|
||||||
artifact_label: PathBuf,
|
artifact_label: PathBuf,
|
||||||
generation: Generation,
|
generation: Generation,
|
||||||
container_id: ContainerId,
|
container_id: ContainerId,
|
||||||
|
|
@ -92,7 +96,7 @@ impl RuntimeSelection {
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// Refuses a configuration other than the exact one used for selection.
|
/// Refuses a configuration other than the exact one used for selection.
|
||||||
pub fn cold_start_inputs<'a>(
|
pub fn cold_start_inputs<'a>(
|
||||||
&'a self,
|
&'a mut self,
|
||||||
configuration: &'a ResolvedDomainConfiguration,
|
configuration: &'a ResolvedDomainConfiguration,
|
||||||
) -> Result<ColdStartInputs<'a>, RuntimeSelectionError> {
|
) -> Result<ColdStartInputs<'a>, RuntimeSelectionError> {
|
||||||
if configuration.domain_id() != self.domain_id
|
if configuration.domain_id() != self.domain_id
|
||||||
|
|
@ -107,6 +111,10 @@ impl RuntimeSelection {
|
||||||
generation: self.generation.clone(),
|
generation: self.generation.clone(),
|
||||||
prepared_artifact: &self.artifact,
|
prepared_artifact: &self.artifact,
|
||||||
prepared_artifact_authority: &self.artifact_authority,
|
prepared_artifact_authority: &self.artifact_authority,
|
||||||
|
prepared_artifact_selection: self
|
||||||
|
.artifact_selection
|
||||||
|
.take()
|
||||||
|
.ok_or_else(|| failure("selected artifact claim was already consumed"))?,
|
||||||
prepared_artifact_label: &self.artifact_label,
|
prepared_artifact_label: &self.artifact_label,
|
||||||
root_process: RootProcess::new(
|
root_process: RootProcess::new(
|
||||||
SANDBOX_INIT_DESTINATION,
|
SANDBOX_INIT_DESTINATION,
|
||||||
|
|
@ -174,6 +182,8 @@ async fn select_runtime_for_platform(
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(RuntimeSelectionError::PreparedLower)?;
|
.map_err(RuntimeSelectionError::PreparedLower)?;
|
||||||
|
let mut selection = None;
|
||||||
|
for _ in 0..PREPARED_SELECTION_ATTEMPTS {
|
||||||
let artifact = prepare_in_user_namespace(
|
let artifact = prepare_in_user_namespace(
|
||||||
configuration,
|
configuration,
|
||||||
&image,
|
&image,
|
||||||
|
|
@ -182,9 +192,17 @@ async fn select_runtime_for_platform(
|
||||||
&user_namespace_map,
|
&user_namespace_map,
|
||||||
)
|
)
|
||||||
.map_err(|error| failure(format!("mapped preparation worker failed: {error}")))?;
|
.map_err(|error| failure(format!("mapped preparation worker failed: {error}")))?;
|
||||||
let artifact_authority = prepared
|
if let Some(selected) = prepared
|
||||||
.open_artifact_authority(&artifact)
|
.select_artifact(&artifact)
|
||||||
.map_err(RuntimeSelectionError::PreparedLower)?;
|
.map_err(RuntimeSelectionError::PreparedLower)?
|
||||||
|
{
|
||||||
|
selection = Some(selected);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let selection = selection
|
||||||
|
.ok_or_else(|| failure("prepared artifact was repeatedly reclaimed before selection"))?;
|
||||||
|
let (artifact, artifact_authority, artifact_selection) = selection.into_parts();
|
||||||
let artifact_label = PathBuf::from(std::ffi::OsString::from_vec(
|
let artifact_label = PathBuf::from(std::ffi::OsString::from_vec(
|
||||||
configuration
|
configuration
|
||||||
.storage_root()
|
.storage_root()
|
||||||
|
|
@ -218,6 +236,7 @@ async fn select_runtime_for_platform(
|
||||||
catalog,
|
catalog,
|
||||||
artifact,
|
artifact,
|
||||||
artifact_authority,
|
artifact_authority,
|
||||||
|
artifact_selection: Some(artifact_selection),
|
||||||
artifact_label,
|
artifact_label,
|
||||||
generation,
|
generation,
|
||||||
container_id: ContainerId::new(random_id()?),
|
container_id: ContainerId::new(random_id()?),
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ use lazaret_core::identity::{
|
||||||
use lazaret_core::records::{ArtifactState, Domain, DurableDecision, Generation, PreparedArtifact};
|
use lazaret_core::records::{ArtifactState, Domain, DurableDecision, Generation, PreparedArtifact};
|
||||||
use lazaret_host_coordinator::cold_start::{ColdStart, ColdStartInputs};
|
use lazaret_host_coordinator::cold_start::{ColdStart, ColdStartInputs};
|
||||||
use lazaret_host_coordinator::domain_store::HostCoordinatorDomainStore;
|
use lazaret_host_coordinator::domain_store::HostCoordinatorDomainStore;
|
||||||
use lazaret_linux::filesystem::try_exclusive_flock;
|
use lazaret_linux::filesystem::{try_exclusive_flock, try_shared_flock};
|
||||||
use lazaret_linux::user_namespace::{
|
use lazaret_linux::user_namespace::{
|
||||||
REQUIRED_SUBORDINATE_IDS, UserNamespaceIdRange, UserNamespaceMap,
|
REQUIRED_SUBORDINATE_IDS, UserNamespaceIdRange, UserNamespaceMap,
|
||||||
};
|
};
|
||||||
|
|
@ -267,6 +267,10 @@ impl ColdStartFixture {
|
||||||
generation: self.generation.clone(),
|
generation: self.generation.clone(),
|
||||||
prepared_artifact: &self.artifact,
|
prepared_artifact: &self.artifact,
|
||||||
prepared_artifact_authority: &self.artifact_authority,
|
prepared_artifact_authority: &self.artifact_authority,
|
||||||
|
prepared_artifact_selection: try_shared_flock(
|
||||||
|
File::open(&self.prepared_lower).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
prepared_artifact_label: Path::new(
|
prepared_artifact_label: Path::new(
|
||||||
std::str::from_utf8(self.artifact.path.as_bytes()).unwrap(),
|
std::str::from_utf8(self.artifact.path.as_bytes()).unwrap(),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -351,6 +351,13 @@ pub fn try_exclusive_flock(file: File) -> io::Result<ExclusiveFileLock> {
|
||||||
Ok(ExclusiveFileLock(file))
|
Ok(ExclusiveFileLock(file))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// # Errors
|
||||||
|
/// Returns an error when the nonblocking shared lock is unavailable.
|
||||||
|
pub fn try_shared_flock(file: File) -> io::Result<SharedFileLock> {
|
||||||
|
flock(file.as_fd(), FlockOperation::NonBlockingLockShared)?;
|
||||||
|
Ok(SharedFileLock(file))
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ExclusiveFileLock(File);
|
pub struct ExclusiveFileLock(File);
|
||||||
|
|
||||||
|
|
@ -360,6 +367,15 @@ impl AsFd for ExclusiveFileLock {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SharedFileLock(File);
|
||||||
|
|
||||||
|
impl AsFd for SharedFileLock {
|
||||||
|
fn as_fd(&self) -> std::os::fd::BorrowedFd<'_> {
|
||||||
|
self.0.as_fd()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// Returns an error unless the replacement and containing directory are synced.
|
/// Returns an error unless the replacement and containing directory are synced.
|
||||||
pub fn atomic_replace(path: &Path, bytes: &[u8]) -> io::Result<()> {
|
pub fn atomic_replace(path: &Path, bytes: &[u8]) -> io::Result<()> {
|
||||||
|
|
@ -459,7 +475,7 @@ pub fn require_mount_identity(path: &Path, expected: MountIdentity) -> io::Resul
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::fs::OpenOptions;
|
use std::fs::{File, OpenOptions};
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
use std::os::unix::fs::symlink;
|
use std::os::unix::fs::symlink;
|
||||||
|
|
||||||
|
|
@ -468,6 +484,7 @@ mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
RegularFileMailbox, atomic_replace, current_mount_identity, namespace_identity,
|
RegularFileMailbox, atomic_replace, current_mount_identity, namespace_identity,
|
||||||
require_mount_identity, require_write_sealed, sealed_memfd, try_exclusive_flock,
|
require_mount_identity, require_write_sealed, sealed_memfd, try_exclusive_flock,
|
||||||
|
try_shared_flock,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn test_directory(name: &str) -> std::path::PathBuf {
|
fn test_directory(name: &str) -> std::path::PathBuf {
|
||||||
|
|
@ -552,6 +569,21 @@ mod tests {
|
||||||
std::fs::remove_dir_all(directory).unwrap();
|
std::fs::remove_dir_all(directory).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shared_flock_excludes_reclaim_until_every_claim_closes() {
|
||||||
|
let directory = test_directory("shared-flock");
|
||||||
|
let path = directory.join("artifact");
|
||||||
|
std::fs::create_dir(&path).unwrap();
|
||||||
|
let first = try_shared_flock(File::open(&path).unwrap()).unwrap();
|
||||||
|
let second = try_shared_flock(File::open(&path).unwrap()).unwrap();
|
||||||
|
assert!(try_exclusive_flock(File::open(&path).unwrap()).is_err());
|
||||||
|
drop(first);
|
||||||
|
assert!(try_exclusive_flock(File::open(&path).unwrap()).is_err());
|
||||||
|
drop(second);
|
||||||
|
assert!(try_exclusive_flock(File::open(&path).unwrap()).is_ok());
|
||||||
|
std::fs::remove_dir_all(directory).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn namespace_identity_is_stable_for_same_namespace() {
|
fn namespace_identity_is_stable_for_same_namespace() {
|
||||||
let first = namespace_identity(std::path::Path::new("/proc/self/ns/mnt")).unwrap();
|
let first = namespace_identity(std::path::Path::new("/proc/self/ns/mnt")).unwrap();
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ pub use layer::{
|
||||||
IdMap, IdMapRange, PreparationLimits, PreparedDestination, PreparedDestinationKind,
|
IdMap, IdMapRange, PreparationLimits, PreparedDestination, PreparedDestinationKind,
|
||||||
};
|
};
|
||||||
pub use store::{
|
pub use store::{
|
||||||
PrepareRecovery, PrepareRequest, PreparedArtifactGcResult, PreparedLowerStore, ReclaimOutcome,
|
PrepareRecovery, PrepareRequest, PreparedArtifactGcResult, PreparedArtifactSelection,
|
||||||
|
PreparedLowerStore, ReclaimOutcome,
|
||||||
};
|
};
|
||||||
pub use transport::{OciRegistryTransport, Platform, RegistryCredentials, TransportedImage};
|
pub use transport::{OciRegistryTransport, Platform, RegistryCredentials, TransportedImage};
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,9 @@ use lazaret_core::identity::{
|
||||||
SelectedManifestDigest, SourceIdentity,
|
SelectedManifestDigest, SourceIdentity,
|
||||||
};
|
};
|
||||||
use lazaret_core::records::{ArtifactState, PrepareOperation, PrepareStageState, PreparedArtifact};
|
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 lazaret_storage_catalog::{Catalog, CatalogError, CatalogRow};
|
||||||
use rustix::fs::{
|
use rustix::fs::{
|
||||||
AtFlags, Mode, OFlags, RenameFlags, ResolveFlags, mkdirat, openat2, renameat, renameat_with,
|
AtFlags, Mode, OFlags, RenameFlags, ResolveFlags, mkdirat, openat2, renameat, renameat_with,
|
||||||
|
|
@ -56,6 +58,7 @@ pub enum PrepareRecovery {
|
||||||
pub enum ReclaimOutcome {
|
pub enum ReclaimOutcome {
|
||||||
Removed,
|
Removed,
|
||||||
ReplacementPreserved,
|
ReplacementPreserved,
|
||||||
|
RetainedBySelection,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
|
@ -64,6 +67,25 @@ pub struct PreparedArtifactGcResult {
|
||||||
pub outcome: ReclaimOutcome,
|
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)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct PreparedLowerStore {
|
pub struct PreparedLowerStore {
|
||||||
root: Arc<File>,
|
root: Arc<File>,
|
||||||
|
|
@ -178,6 +200,52 @@ impl PreparedLowerStore {
|
||||||
.context("prepared artifact path is absent")
|
.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.
|
/// Materializes and atomically publishes one verified immutable prepared lower.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
|
|
@ -498,11 +566,26 @@ impl PreparedLowerStore {
|
||||||
/// Refuses leases, forged catalog rows, physical identity mismatch, foreign paths, and
|
/// Refuses leases, forged catalog rows, physical identity mismatch, foreign paths, and
|
||||||
/// filesystem or catalog failures.
|
/// filesystem or catalog failures.
|
||||||
pub fn reclaim(&self, expected: &PreparedArtifact) -> PreparedLowerResult<ReclaimOutcome> {
|
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
|
let reclaiming = self
|
||||||
.catalog
|
.catalog
|
||||||
.begin_reclaim(expected)
|
.begin_reclaim(expected)
|
||||||
.map_err(PreparedLowerError::catalog)?;
|
.map_err(PreparedLowerError::catalog)?;
|
||||||
self.resume_reclaim(&reclaiming)
|
self.resume_reclaim_inner(&reclaiming)
|
||||||
|
.map_err(PreparedLowerError::reclaim)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resumes an exact durable reclaim after interruption.
|
/// Resumes an exact durable reclaim after interruption.
|
||||||
|
|
@ -516,10 +599,28 @@ impl PreparedLowerStore {
|
||||||
reclaiming: &PreparedArtifact,
|
reclaiming: &PreparedArtifact,
|
||||||
) -> PreparedLowerResult<ReclaimOutcome> {
|
) -> PreparedLowerResult<ReclaimOutcome> {
|
||||||
let _physical_lock = self.physical_lock()?;
|
let _physical_lock = self.physical_lock()?;
|
||||||
|
let _claim = self
|
||||||
|
.lock_reclaiming_artifact(reclaiming)
|
||||||
|
.map_err(PreparedLowerError::reclaim)?;
|
||||||
self.resume_reclaim_inner(reclaiming)
|
self.resume_reclaim_inner(reclaiming)
|
||||||
.map_err(PreparedLowerError::reclaim)
|
.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> {
|
fn resume_reclaim_inner(&self, reclaiming: &PreparedArtifact) -> Result<ReclaimOutcome> {
|
||||||
if reclaiming.state != ArtifactState::Reclaiming {
|
if reclaiming.state != ArtifactState::Reclaiming {
|
||||||
bail!("artifact is not 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)> {
|
fn file_identity(file: &File) -> Result<(DeviceId, InodeId)> {
|
||||||
let stat = rustix::fs::fstat(file)?;
|
let stat = rustix::fs::fstat(file)?;
|
||||||
Ok((DeviceId::new(stat.st_dev), InodeId::new(stat.st_ino)))
|
Ok((DeviceId::new(stat.st_dev), InodeId::new(stat.st_ino)))
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue