Require fresh State Anchor absence before lease release
This commit is contained in:
parent
4dd2b150d2
commit
3b76b59bdf
4 changed files with 543 additions and 194 deletions
|
|
@ -26,7 +26,10 @@ use crate::lifecycle::{
|
|||
SessionLifecycleAdapter,
|
||||
};
|
||||
use crate::network::NetworkCoordinator;
|
||||
use crate::runtime::{AwaitingAttachedRunsc, CommittedSandboxRuntime, SandboxCoordinator};
|
||||
use crate::runtime::{
|
||||
AwaitingAttachedRunsc, CommittedSandboxRuntime, SandboxCoordinator,
|
||||
StateAnchorAbsenceObservation,
|
||||
};
|
||||
use crate::state_anchor::{StateAnchorCoordinator, StateAnchorReleaseAuthority};
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
@ -80,6 +83,7 @@ pub struct ProductionLifecycleAdapter<'a> {
|
|||
runtime: Option<CommittedSandboxRuntime>,
|
||||
state_anchor_mount: Option<MountIdentity>,
|
||||
state_anchor_release: Option<StateAnchorReleaseAuthority>,
|
||||
state_anchor_absence: Option<StateAnchorAbsenceObservation>,
|
||||
}
|
||||
|
||||
pub type ProductionHostCoordinatorLifecycle<'store, 'resources> = HostCoordinatorLifecycle<
|
||||
|
|
@ -175,6 +179,7 @@ impl<'a> ProductionLifecycleAdapter<'a> {
|
|||
runtime: None,
|
||||
state_anchor_mount: None,
|
||||
state_anchor_release: None,
|
||||
state_anchor_absence: None,
|
||||
},
|
||||
ProductionSessionRegistry { registry },
|
||||
)
|
||||
|
|
@ -409,18 +414,26 @@ impl<'a> ProductionLifecycleAdapter<'a> {
|
|||
)
|
||||
.map_err(failure)?,
|
||||
);
|
||||
self.runtime
|
||||
let runtime = self
|
||||
.runtime
|
||||
.as_mut()
|
||||
.ok_or_else(|| ProductionLifecycleError("committed runtime is absent".into()))?
|
||||
.unmount_state_anchor(
|
||||
self.state_anchor_release
|
||||
.as_ref()
|
||||
.ok_or_else(|| {
|
||||
ProductionLifecycleError("State Anchor release is absent".into())
|
||||
})?
|
||||
.mount(),
|
||||
)
|
||||
.ok_or_else(|| ProductionLifecycleError("committed runtime is absent".into()))?;
|
||||
let release = self
|
||||
.state_anchor_release
|
||||
.as_ref()
|
||||
.ok_or_else(|| ProductionLifecycleError("State Anchor release is absent".into()))?;
|
||||
runtime
|
||||
.unmount_state_anchor(release.mount())
|
||||
.map_err(failure)?;
|
||||
self.state_anchor_absence = Some(
|
||||
runtime
|
||||
.observe_state_anchor_absence(
|
||||
release.user_namespace(),
|
||||
release.mount_namespace(),
|
||||
release.mount(),
|
||||
)
|
||||
.map_err(failure)?,
|
||||
);
|
||||
self.state_anchor_mount = None;
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -428,11 +441,44 @@ impl<'a> ProductionLifecycleAdapter<'a> {
|
|||
fn release_prepared_lower(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||
let release = self
|
||||
.state_anchor_release
|
||||
.take()
|
||||
.as_ref()
|
||||
.ok_or_else(|| ProductionLifecycleError("State Anchor release is absent".into()))?;
|
||||
self.state_anchor()
|
||||
.complete_release_after_unmount(&release, self.plan.state_anchor_layout)
|
||||
.map_err(failure)
|
||||
let first_observation = self.state_anchor_absence.ok_or_else(|| {
|
||||
ProductionLifecycleError("State Anchor absence evidence is absent".into())
|
||||
})?;
|
||||
let runtime = self
|
||||
.runtime
|
||||
.as_mut()
|
||||
.ok_or_else(|| ProductionLifecycleError("committed runtime is absent".into()))?;
|
||||
let coordinator = StateAnchorCoordinator::new(&self.plan.store, self.plan.catalog);
|
||||
coordinator
|
||||
.complete_release_after_unmount(
|
||||
release,
|
||||
self.plan.state_anchor_layout,
|
||||
first_observation,
|
||||
|| {
|
||||
runtime
|
||||
.observe_state_anchor_absence(
|
||||
release.user_namespace(),
|
||||
release.mount_namespace(),
|
||||
release.mount(),
|
||||
)
|
||||
.map_err(|error| {
|
||||
crate::state_anchor::StateAnchorCoordinatorError::InvalidState(
|
||||
match error {
|
||||
crate::runtime::SandboxCoordinatorError::InvalidState(
|
||||
detail,
|
||||
) => detail,
|
||||
_ => "Runtime Supervisor absence observation failed",
|
||||
},
|
||||
)
|
||||
})
|
||||
},
|
||||
)
|
||||
.map_err(failure)?;
|
||||
self.state_anchor_release = None;
|
||||
self.state_anchor_absence = None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn graceful_exit(&mut self) -> Result<(), ProductionLifecycleError> {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,14 @@ use crate::network::{NetworkCoordinator, NetworkCoordinatorError};
|
|||
|
||||
const SUPERVISOR_EXIT_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct StateAnchorAbsenceObservation {
|
||||
pub user_namespace: NamespaceIdentity,
|
||||
pub mount_namespace: NamespaceIdentity,
|
||||
pub mount: MountIdentity,
|
||||
pub observed: MountIdentity,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SandboxCoordinatorError {
|
||||
Store(DomainStoreError),
|
||||
|
|
@ -967,6 +975,45 @@ impl CommittedSandboxRuntime {
|
|||
}
|
||||
}
|
||||
|
||||
/// Requests a fresh namespace-local observation of exact State Anchor absence.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses an RS refusal, echoed mount, mismatched authority, or channel failure.
|
||||
pub fn observe_state_anchor_absence(
|
||||
&mut self,
|
||||
user_namespace: NamespaceIdentity,
|
||||
mount_namespace: NamespaceIdentity,
|
||||
mount: MountIdentity,
|
||||
) -> Result<StateAnchorAbsenceObservation, SandboxCoordinatorError> {
|
||||
self.control
|
||||
.observe_state_anchor_absence(user_namespace, mount_namespace, mount)?;
|
||||
match self.control.receive()? {
|
||||
SupervisorEvent::StateAnchorAbsenceObserved {
|
||||
user_namespace: observed_user_namespace,
|
||||
mount_namespace: observed_mount_namespace,
|
||||
mount: observed_mount,
|
||||
observed,
|
||||
} if observed_user_namespace == user_namespace
|
||||
&& observed_mount_namespace == mount_namespace
|
||||
&& observed_mount == mount
|
||||
&& observed != mount =>
|
||||
{
|
||||
Ok(StateAnchorAbsenceObservation {
|
||||
user_namespace: observed_user_namespace,
|
||||
mount_namespace: observed_mount_namespace,
|
||||
mount: observed_mount,
|
||||
observed,
|
||||
})
|
||||
}
|
||||
SupervisorEvent::Refused(detail) => {
|
||||
Err(SandboxCoordinatorError::Io(io::Error::other(detail)))
|
||||
}
|
||||
_ => Err(SandboxCoordinatorError::InvalidState(
|
||||
"Runtime Supervisor did not provide exact State Anchor absence evidence",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gracefully exits and reaps the resource-empty exact Runtime Supervisor.
|
||||
///
|
||||
/// # Errors
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ use lazaret_state_anchor::{StateAnchorError, StateAnchorLayout, VerifiedStateAnc
|
|||
use lazaret_storage_catalog::{Catalog, CatalogError};
|
||||
|
||||
use crate::domain_store::{DomainStoreError, DomainStoreMutation, HostCoordinatorDomainStore};
|
||||
use crate::runtime::StateAnchorAbsenceObservation;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum StateAnchorCoordinatorError {
|
||||
|
|
@ -367,11 +368,17 @@ impl<'a> StateAnchorCoordinator<'a> {
|
|||
///
|
||||
/// # Errors
|
||||
/// Refuses a stale authority, any remaining mount, or release-evidence mismatch.
|
||||
pub fn complete_release_after_unmount(
|
||||
pub fn complete_release_after_unmount<Observe>(
|
||||
&self,
|
||||
authority: &StateAnchorReleaseAuthority,
|
||||
layout: &StateAnchorLayout<'_>,
|
||||
) -> Result<(), StateAnchorCoordinatorError> {
|
||||
first_observation: StateAnchorAbsenceObservation,
|
||||
mut observe_again: Observe,
|
||||
) -> Result<(), StateAnchorCoordinatorError>
|
||||
where
|
||||
Observe: FnMut() -> Result<StateAnchorAbsenceObservation, StateAnchorCoordinatorError>,
|
||||
{
|
||||
require_absence_observation(authority, first_observation)?;
|
||||
let expected_layout = layout.ownership_intent(authority.lease.prepared_artifact_id);
|
||||
let expected_merged = expected_layout.merged_path.as_ref().ok_or(
|
||||
StateAnchorCoordinatorError::InvalidState("State Anchor merged path is absent"),
|
||||
|
|
@ -407,7 +414,7 @@ impl<'a> StateAnchorCoordinator<'a> {
|
|||
));
|
||||
}
|
||||
}
|
||||
self.release_catalog_lease_if_present(authority)?;
|
||||
self.release_catalog_lease_if_present(authority, &mut observe_again)?;
|
||||
self.domain_store
|
||||
.apply(&[DomainStoreMutation::CleanParticipantAfterAbsence(
|
||||
clean_participant(authority.generation_id, ParticipantRole::PreparedLower)?,
|
||||
|
|
@ -528,10 +535,14 @@ impl<'a> StateAnchorCoordinator<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
fn release_catalog_lease_if_present(
|
||||
fn release_catalog_lease_if_present<Observe>(
|
||||
&self,
|
||||
authority: &StateAnchorReleaseAuthority,
|
||||
) -> Result<(), StateAnchorCoordinatorError> {
|
||||
observe_again: &mut Observe,
|
||||
) -> Result<(), StateAnchorCoordinatorError>
|
||||
where
|
||||
Observe: FnMut() -> Result<StateAnchorAbsenceObservation, StateAnchorCoordinatorError>,
|
||||
{
|
||||
let snapshot = self.catalog.snapshot()?;
|
||||
let generation_lease = snapshot.artifact_leases.iter().find(|lease| {
|
||||
lease.domain_id == authority.lease.domain_id
|
||||
|
|
@ -570,12 +581,33 @@ impl<'a> StateAnchorCoordinator<'a> {
|
|||
"Domain release evidence is not committed",
|
||||
)));
|
||||
}
|
||||
let observation =
|
||||
observe_again().map_err(|error| CatalogError::Io(io::Error::other(error)))?;
|
||||
require_absence_observation(authority, observation)
|
||||
.map_err(|error| CatalogError::Io(io::Error::other(error)))?;
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn require_absence_observation(
|
||||
authority: &StateAnchorReleaseAuthority,
|
||||
observation: StateAnchorAbsenceObservation,
|
||||
) -> Result<(), StateAnchorCoordinatorError> {
|
||||
if observation.user_namespace == authority.user_namespace
|
||||
&& observation.mount_namespace == authority.mount_namespace
|
||||
&& observation.mount == authority.mount
|
||||
&& observation.observed != authority.mount
|
||||
{
|
||||
Ok(())
|
||||
} else {
|
||||
Err(StateAnchorCoordinatorError::InvalidState(
|
||||
"State Anchor absence evidence mismatches release authority",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn release_authority(
|
||||
domain: &lazaret_core::records::Domain,
|
||||
anchor: &Participant,
|
||||
|
|
|
|||
|
|
@ -182,6 +182,44 @@ fn durable_phases_resume_release_without_collapsing_lease_order() {
|
|||
ParticipantState::ReleaseIntent,
|
||||
);
|
||||
assert_eq!(catalog.snapshot().unwrap().artifact_leases, vec![lease]);
|
||||
let valid = absence_observation(&authority);
|
||||
let invalid_observations = [
|
||||
StateAnchorAbsenceObservation {
|
||||
observed: authority.mount(),
|
||||
..valid
|
||||
},
|
||||
StateAnchorAbsenceObservation {
|
||||
user_namespace: NamespaceIdentity {
|
||||
device: DeviceId::new(193),
|
||||
inode: InodeId::new(194),
|
||||
},
|
||||
..valid
|
||||
},
|
||||
StateAnchorAbsenceObservation {
|
||||
mount_namespace: NamespaceIdentity {
|
||||
device: DeviceId::new(195),
|
||||
inode: InodeId::new(196),
|
||||
},
|
||||
..valid
|
||||
},
|
||||
StateAnchorAbsenceObservation {
|
||||
mount: valid.observed,
|
||||
..valid
|
||||
},
|
||||
];
|
||||
for invalid in invalid_observations {
|
||||
assert!(
|
||||
coordinator
|
||||
.complete_release_after_unmount(&authority, &layout, invalid, || Ok(valid))
|
||||
.is_err()
|
||||
);
|
||||
assert_states(
|
||||
&domain_store,
|
||||
ParticipantState::LeaseHeld,
|
||||
ParticipantState::ReleaseIntent,
|
||||
);
|
||||
assert_eq!(catalog.snapshot().unwrap().artifact_leases, vec![lease]);
|
||||
}
|
||||
let snapshot = domain_store.snapshot().unwrap();
|
||||
let prepared = role(
|
||||
&snapshot.participants,
|
||||
|
|
@ -197,9 +235,42 @@ fn durable_phases_resume_release_without_collapsing_lease_order() {
|
|||
DomainStoreMutation::PutParticipant(prepared_release_intent(prepared).unwrap()),
|
||||
])
|
||||
.unwrap();
|
||||
for invalid in invalid_observations {
|
||||
assert!(
|
||||
coordinator
|
||||
.complete_release_after_unmount(&authority, &layout, valid, || Ok(invalid))
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(catalog.snapshot().unwrap().artifact_leases, vec![lease]);
|
||||
}
|
||||
assert!(
|
||||
coordinator
|
||||
.complete_release_after_unmount(&authority, &layout, valid, || {
|
||||
Err(StateAnchorCoordinatorError::InvalidState(
|
||||
"State Anchor absence evidence is unreadable",
|
||||
))
|
||||
})
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(catalog.snapshot().unwrap().artifact_leases, vec![lease]);
|
||||
assert!(
|
||||
coordinator
|
||||
.complete_release_after_unmount(&authority, &layout, valid, || {
|
||||
Err(StateAnchorCoordinatorError::InvalidState(
|
||||
"second State Anchor observation was skipped",
|
||||
))
|
||||
})
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(catalog.snapshot().unwrap().artifact_leases, vec![lease]);
|
||||
let mut observations = 0;
|
||||
coordinator
|
||||
.complete_release_after_unmount(&authority, &layout)
|
||||
.complete_release_after_unmount(&authority, &layout, valid, || {
|
||||
observations += 1;
|
||||
Ok(valid)
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(observations, 1);
|
||||
assert_states(
|
||||
&domain_store,
|
||||
ParticipantState::Clean,
|
||||
|
|
@ -208,6 +279,19 @@ fn durable_phases_resume_release_without_collapsing_lease_order() {
|
|||
assert!(catalog.snapshot().unwrap().artifact_leases.is_empty());
|
||||
}
|
||||
|
||||
fn absence_observation(authority: &StateAnchorReleaseAuthority) -> StateAnchorAbsenceObservation {
|
||||
StateAnchorAbsenceObservation {
|
||||
user_namespace: authority.user_namespace(),
|
||||
mount_namespace: authority.mount_namespace(),
|
||||
mount: authority.mount(),
|
||||
observed: MountIdentity {
|
||||
mount_id: MountId::new(authority.mount().mount_id.get() + 1),
|
||||
device: authority.mount().device,
|
||||
inode: authority.mount().inode,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn acquisition_resumes_after_prepared_intent_reopen() {
|
||||
let fixture = AcquisitionFixture::new(17);
|
||||
|
|
|
|||
Loading…
Reference in a new issue