Route lifecycle admission through the control registry

This commit is contained in:
ms2k 2026-07-16 16:08:12 +09:00
commit 510dfccf5a
3 changed files with 431 additions and 158 deletions

View file

@ -39,6 +39,9 @@ use sha2::{Digest, Sha256};
use time::{Duration, OffsetDateTime}; use time::{Duration, OffsetDateTime};
use crate::domain_store::{DomainStoreError, DomainStoreMutation, HostCoordinatorDomainStore}; use crate::domain_store::{DomainStoreError, DomainStoreMutation, HostCoordinatorDomainStore};
use crate::lifecycle::{
HostCoordinatorLifecycle, LifecycleAction, SessionAdmissionIdentity, SessionLifecycleAdapter,
};
pub const CONTROL_ALPN: &[u8] = b"lazaret-control/1"; pub const CONTROL_ALPN: &[u8] = b"lazaret-control/1";
pub const CONTROL_SERVER_NAME: &str = "coordinator.lazaret.invalid"; pub const CONTROL_SERVER_NAME: &str = "coordinator.lazaret.invalid";
@ -387,6 +390,15 @@ pub struct RegisteredSession {
pub session_anchor_id: SessionAnchorId, pub session_anchor_id: SessionAnchorId,
} }
#[derive(Clone)]
pub struct ControlSessionAdmission {
pub request: SessionRegistrationRequest,
pub credential: TlsCredential,
}
pub type ControlHostCoordinatorLifecycle<'store, A> =
HostCoordinatorLifecycle<'store, A, Arc<SessionRegistry>>;
#[derive(Default)] #[derive(Default)]
struct RegistryState { struct RegistryState {
enrolled: BTreeMap<CertificateDigest, TlsCredential>, enrolled: BTreeMap<CertificateDigest, TlsCredential>,
@ -478,11 +490,26 @@ impl SessionRegistry {
|| registered.binding.cli_id == binding.cli_id || registered.binding.cli_id == binding.cli_id
|| registered.session_anchor_id == session_anchor_id || registered.session_anchor_id == session_anchor_id
}); });
let resumable_waiter = snapshot.sessions.iter().any(|session| {
session.generation_id == binding.generation_identity.generation_id
&& session.session_id == binding.session_id
&& session.cli_id == binding.cli_id
&& session.session_anchor_id == session_anchor_id
&& session.session_phase == SessionPhase::Waiting
});
let duplicate_session = snapshot.sessions.iter().any(|session| { let duplicate_session = snapshot.sessions.iter().any(|session| {
session.session_id == binding.session_id let same_session_allowed = resumable_waiter
&& session.session_id == binding.session_id
&& (session.session_phase == SessionPhase::Closed
|| (session.generation_id == binding.generation_identity.generation_id
&& session.cli_id == binding.cli_id
&& session.session_anchor_id == session_anchor_id
&& session.session_phase == SessionPhase::Waiting));
!same_session_allowed
&& (session.session_id == binding.session_id
|| (is_live_session(session.session_phase) || (is_live_session(session.session_phase)
&& (session.cli_id == binding.cli_id && (session.cli_id == binding.cli_id
|| session.session_anchor_id == session_anchor_id)) || session.session_anchor_id == session_anchor_id)))
}); });
if duplicate_in_memory || duplicate_session { if duplicate_in_memory || duplicate_session {
return Err(ControlError::DuplicateIdentity); return Err(ControlError::DuplicateIdentity);
@ -580,6 +607,69 @@ impl SessionRegistry {
} }
} }
impl SessionLifecycleAdapter for Arc<SessionRegistry> {
type Error = ControlError;
type Admission = ControlSessionAdmission;
fn admission_identity(&self, admission: &Self::Admission) -> SessionAdmissionIdentity {
let binding = &admission.request.session_binding;
SessionAdmissionIdentity {
generation_id: binding.generation_identity.generation_id,
session_id: binding.session_id,
configuration_fingerprint: binding.generation_identity.configuration_fingerprint,
}
}
fn register_session(
&self,
_store: &HostCoordinatorDomainStore,
entry_action: LifecycleAction,
admission: Self::Admission,
) -> Result<Session, Self::Error> {
if !matches!(
entry_action,
LifecycleAction::StartReadySession | LifecycleAction::AdvanceWaitingSession
) {
return Err(ControlError::InvalidRequest);
}
let registered = self.register(admission.request, &admission.credential)?;
Ok(Session {
generation_id: registered.binding.generation_identity.generation_id,
session_id: registered.binding.session_id,
cli_id: registered.binding.cli_id,
session_anchor_id: registered.session_anchor_id,
session_phase: SessionPhase::Attached,
})
}
fn disconnect_session(
&self,
_store: &HostCoordinatorDomainStore,
session: &Session,
) -> Result<(), Self::Error> {
let mut state = self.lock()?;
let registered = state.connected.get(&session.session_id);
if let Some(registered) = registered
&& (registered.binding.generation_identity.generation_id != session.generation_id
|| registered.binding.cli_id != session.cli_id
|| registered.session_anchor_id != session.session_anchor_id)
{
return Err(ControlError::InvalidRequest);
}
if session.session_phase == SessionPhase::Attached && registered.is_none() {
return Err(ControlError::InvalidRequest);
}
state.connected.remove(&session.session_id);
drop(state);
self.store
.apply(&[DomainStoreMutation::PutSession(Session {
session_phase: SessionPhase::Closed,
..session.clone()
})])?;
Ok(())
}
}
struct ControlPaths { struct ControlPaths {
root: PathBuf, root: PathBuf,
root_file: File, root_file: File,

View file

@ -47,6 +47,37 @@ pub trait LifecycleAdapter {
) -> Result<(), Self::Error>; ) -> Result<(), Self::Error>;
} }
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct SessionAdmissionIdentity {
pub generation_id: lazaret_core::identity::GenerationId,
pub session_id: SessionId,
pub configuration_fingerprint: ConfigurationFingerprint,
}
pub trait SessionLifecycleAdapter {
type Error;
type Admission;
fn admission_identity(&self, admission: &Self::Admission) -> SessionAdmissionIdentity;
/// # Errors
/// Returns credential, identity, duplicate-session, or durable registration failure.
fn register_session(
&self,
store: &HostCoordinatorDomainStore,
entry_action: LifecycleAction,
admission: Self::Admission,
) -> Result<Session, Self::Error>;
/// # Errors
/// Returns exact-session or durable disconnect failure.
fn disconnect_session(
&self,
store: &HostCoordinatorDomainStore,
session: &Session,
) -> Result<(), Self::Error>;
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DomainPhase { pub enum DomainPhase {
Absent, Absent,
@ -120,9 +151,10 @@ impl RuntimeProgress {
} }
} }
pub struct HostCoordinatorLifecycle<'store, A> { pub struct HostCoordinatorLifecycle<'store, A, S> {
store: &'store HostCoordinatorDomainStore, store: &'store HostCoordinatorDomainStore,
adapter: A, adapter: A,
session_adapter: S,
phase: DomainPhase, phase: DomainPhase,
generation: Option<Generation>, generation: Option<Generation>,
bootstrap_owner: Option<SessionId>, bootstrap_owner: Option<SessionId>,
@ -133,12 +165,17 @@ pub struct HostCoordinatorLifecycle<'store, A> {
registered: HashSet<SessionId>, registered: HashSet<SessionId>,
} }
impl<'store, A: LifecycleAdapter> HostCoordinatorLifecycle<'store, A> { impl<'store, A, S> HostCoordinatorLifecycle<'store, A, S>
where
A: LifecycleAdapter,
S: SessionLifecycleAdapter<Error = A::Error>,
{
#[must_use] #[must_use]
pub fn new(store: &'store HostCoordinatorDomainStore, adapter: A) -> Self { pub fn new(store: &'store HostCoordinatorDomainStore, adapter: A, session_adapter: S) -> Self {
Self { Self {
store, store,
adapter, adapter,
session_adapter,
phase: DomainPhase::Absent, phase: DomainPhase::Absent,
generation: None, generation: None,
bootstrap_owner: None, bootstrap_owner: None,
@ -215,26 +252,47 @@ impl<'store, A: LifecycleAdapter> HostCoordinatorLifecycle<'store, A> {
} }
/// # Errors /// # Errors
/// Refuses an unpublished, stale, mismatched, or duplicate session. /// Refuses an unpublished, stale, mismatched, duplicate, or unauthenticated session.
pub fn start_ready_session( pub fn admit_session(
&mut self, &mut self,
session: Session, admission: S::Admission,
fingerprint: ConfigurationFingerprint,
) -> Result<(), LifecycleError<A::Error>> { ) -> Result<(), LifecycleError<A::Error>> {
self.require_ready_join(LifecycleAction::StartReadySession, &session, fingerprint)?; let identity = self.session_adapter.admission_identity(&admission);
let entry_action = match self.sessions.get(&identity.session_id) {
None => LifecycleAction::StartReadySession,
Some(session) if session.session_phase == SessionPhase::Waiting => {
LifecycleAction::AdvanceWaitingSession
}
Some(_) => {
return Err(LifecycleError::InvalidTransition {
action: LifecycleAction::RegisterSession,
reason: "session is not New or Waiting",
});
}
};
let generation = self.current_generation(entry_action)?;
Self::require( Self::require(
LifecycleAction::StartReadySession, entry_action,
session.session_phase == SessionPhase::New self.admission_ready()
&& !self.sessions.contains_key(&session.session_id), && identity.generation_id == generation.generation_id
"session is not a distinct New session", && identity.configuration_fingerprint == generation.configuration_fingerprint,
"Domain is not published for the admission generation",
)?; )?;
self.execute_current(LifecycleAction::StartReadySession)?; let attached = self
self.put_new_session( .session_adapter
LifecycleAction::StartReadySession, .register_session(self.store, entry_action, admission)
session, .map_err(LifecycleError::Adapter)?;
SessionPhase::Registering, Self::require(
true, LifecycleAction::RegisterSession,
) attached.generation_id == identity.generation_id
&& attached.session_id == identity.session_id
&& attached.session_phase == SessionPhase::Attached,
"session adapter returned mismatched registration evidence",
)?;
self.connected.insert(attached.session_id);
self.registered.insert(attached.session_id);
self.sessions.insert(attached.session_id, attached);
Ok(())
} }
/// # Errors /// # Errors
@ -285,7 +343,7 @@ impl<'store, A: LifecycleAdapter> HostCoordinatorLifecycle<'store, A> {
generation.durable_decision == DurableDecision::None, generation.durable_decision == DurableDecision::None,
"new generation decision is not None", "new generation decision is not None",
)?; )?;
let mut session = self let session = self
.session(session_id, LifecycleAction::RestartWaitingDomain)? .session(session_id, LifecycleAction::RestartWaitingDomain)?
.clone(); .clone();
Self::require( Self::require(
@ -303,21 +361,36 @@ impl<'store, A: LifecycleAdapter> HostCoordinatorLifecycle<'store, A> {
&successor_session, &successor_session,
)?; )?;
self.execute(LifecycleAction::RestartWaitingDomain, &generation)?; self.execute(LifecycleAction::RestartWaitingDomain, &generation)?;
let previous = Session { let waiters = self
.sessions
.values()
.filter(|candidate| candidate.session_phase == SessionPhase::Waiting)
.cloned()
.collect::<Vec<_>>();
let mut mutations = Vec::with_capacity(waiters.len() * 2 + 1);
for waiter in &waiters {
mutations.push(DomainStoreMutation::PutSession(Session {
session_phase: SessionPhase::Closed, session_phase: SessionPhase::Closed,
..session.clone() ..waiter.clone()
}; }));
session.generation_id = generation.generation_id; }
self.store.apply(&[ mutations.push(DomainStoreMutation::PutGeneration(generation.clone()));
DomainStoreMutation::PutSession(previous), for waiter in &waiters {
DomainStoreMutation::PutGeneration(generation.clone()), mutations.push(DomainStoreMutation::PutSession(Session {
DomainStoreMutation::PutSession(session.clone()), generation_id: generation.generation_id,
])?; ..waiter.clone()
}));
}
self.store.apply(&mutations)?;
self.phase = DomainPhase::Starting; self.phase = DomainPhase::Starting;
self.bootstrap_owner = Some(session_id); self.bootstrap_owner = Some(session_id);
let generation_id = generation.generation_id;
self.generation = Some(generation); self.generation = Some(generation);
self.progress = RuntimeProgress::default(); self.progress = RuntimeProgress::default();
self.sessions.insert(session_id, session); for mut waiter in waiters {
waiter.generation_id = generation_id;
self.sessions.insert(waiter.session_id, waiter);
}
Ok(()) Ok(())
} }
@ -463,31 +536,6 @@ impl<'store, A: LifecycleAdapter> HostCoordinatorLifecycle<'store, A> {
Ok(()) Ok(())
} }
/// # Errors
/// Refuses a non-waiting session, configuration mismatch, or unavailable Domain.
pub fn advance_waiting_session(
&mut self,
session_id: SessionId,
fingerprint: ConfigurationFingerprint,
) -> Result<(), LifecycleError<A::Error>> {
self.require_fingerprint(LifecycleAction::AdvanceWaitingSession, fingerprint)?;
let mut session = self
.session(session_id, LifecycleAction::AdvanceWaitingSession)?
.clone();
Self::require(
LifecycleAction::AdvanceWaitingSession,
session.session_phase == SessionPhase::Waiting && self.admission_ready(),
"waiting session is not admissible",
)?;
self.execute_current(LifecycleAction::AdvanceWaitingSession)?;
session.session_phase = SessionPhase::Registering;
self.store
.apply(&[DomainStoreMutation::PutSession(session.clone())])?;
self.connected.insert(session_id);
self.sessions.insert(session_id, session);
Ok(())
}
/// # Errors /// # Errors
/// Refuses unless a new or waiting session differs from the ready generation. /// Refuses unless a new or waiting session differs from the ready generation.
pub fn reject_new_configuration_mismatch( pub fn reject_new_configuration_mismatch(
@ -558,31 +606,6 @@ impl<'store, A: LifecycleAdapter> HostCoordinatorLifecycle<'store, A> {
Ok(()) Ok(())
} }
/// # Errors
/// Refuses a disconnected, stale, or non-registering session.
pub fn register_session(
&mut self,
session_id: SessionId,
) -> Result<(), LifecycleError<A::Error>> {
let mut session = self
.session(session_id, LifecycleAction::RegisterSession)?
.clone();
Self::require(
LifecycleAction::RegisterSession,
self.admission_ready()
&& session.session_phase == SessionPhase::Registering
&& self.connected.contains(&session_id),
"session is not admissible",
)?;
self.execute_current(LifecycleAction::RegisterSession)?;
session.session_phase = SessionPhase::Attached;
self.store
.apply(&[DomainStoreMutation::PutSession(session.clone())])?;
self.registered.insert(session_id);
self.sessions.insert(session_id, session);
Ok(())
}
/// # Errors /// # Errors
/// Refuses an absent or terminal session, or an adapter/store failure. /// Refuses an absent or terminal session, or an adapter/store failure.
pub fn disconnect_session( pub fn disconnect_session(
@ -604,12 +627,12 @@ impl<'store, A: LifecycleAdapter> HostCoordinatorLifecycle<'store, A> {
), ),
"session is not live", "session is not live",
)?; )?;
self.execute_current(LifecycleAction::DisconnectSession)?;
let leader_lost = let leader_lost =
self.phase == DomainPhase::Starting && self.bootstrap_owner == Some(session_id); self.phase == DomainPhase::Starting && self.bootstrap_owner == Some(session_id);
self.session_adapter
.disconnect_session(self.store, &session)
.map_err(LifecycleError::Adapter)?;
session.session_phase = SessionPhase::Closed; session.session_phase = SessionPhase::Closed;
self.store
.apply(&[DomainStoreMutation::PutSession(session.clone())])?;
self.connected.remove(&session_id); self.connected.remove(&session_id);
self.registered.remove(&session_id); self.registered.remove(&session_id);
self.sessions.insert(session_id, session); self.sessions.insert(session_id, session);
@ -741,8 +764,8 @@ impl<'store, A: LifecycleAdapter> HostCoordinatorLifecycle<'store, A> {
Ok(()) Ok(())
} }
pub fn into_adapter(self) -> A { pub fn into_adapters(self) -> (A, S) {
self.adapter (self.adapter, self.session_adapter)
} }
fn startup_action( fn startup_action(
@ -804,29 +827,6 @@ impl<'store, A: LifecycleAdapter> HostCoordinatorLifecycle<'store, A> {
Ok(()) Ok(())
} }
fn require_ready_join(
&self,
action: LifecycleAction,
session: &Session,
fingerprint: ConfigurationFingerprint,
) -> Result<(), LifecycleError<A::Error>> {
Self::require(
action,
self.admission_ready(),
"Domain is not published and ready",
)?;
Self::require(
action,
session.generation_id == self.current_generation(action)?.generation_id,
"session generation differs",
)?;
Self::require(
action,
fingerprint == self.current_generation(action)?.configuration_fingerprint,
"configuration differs",
)
}
fn require_new_generation( fn require_new_generation(
&self, &self,
action: LifecycleAction, action: LifecycleAction,
@ -843,23 +843,14 @@ impl<'store, A: LifecycleAdapter> HostCoordinatorLifecycle<'store, A> {
action, action,
generation.domain_id == snapshot.domain.domain_id generation.domain_id == snapshot.domain.domain_id
&& session.generation_id == generation.generation_id && session.generation_id == generation.generation_id
&& generation.coordinator_id.as_bytes() != generation.network_sidecar_id.as_bytes()
&& generation.coordinator_id.as_bytes() != generation.sandbox_id.as_bytes()
&& generation.network_sidecar_id.as_bytes() != generation.sandbox_id.as_bytes()
&& latest.is_none_or(|current| generation.generation_id > current), && latest.is_none_or(|current| generation.generation_id > current),
"generation is not a fresh successor for this Domain", "generation is not a fresh successor for this Domain",
) )
} }
fn require_fingerprint(
&self,
action: LifecycleAction,
fingerprint: ConfigurationFingerprint,
) -> Result<(), LifecycleError<A::Error>> {
Self::require(
action,
fingerprint == self.current_generation(action)?.configuration_fingerprint,
"configuration differs",
)
}
fn admission_ready(&self) -> bool { fn admission_ready(&self) -> bool {
self.phase == DomainPhase::Ready && self.published self.phase == DomainPhase::Ready && self.published
} }

View file

@ -1,36 +1,138 @@
use std::convert::Infallible;
use std::fs::OpenOptions; use std::fs::OpenOptions;
use std::sync::Mutex; use std::sync::{Arc, Mutex};
use lazaret_core::identity::{ use lazaret_core::identity::{
ClientId, ConfigurationFingerprint, CoordinatorId, DomainId, GenerationId, HostPath, ClientId, ConfigurationFingerprint, CoordinatorId, DomainId, GenerationId, HostPath,
NetworkSidecarId, SandboxId, SessionAnchorId, SessionId, NetworkSidecarId, SandboxId, SessionAnchorId, SessionId,
}; };
use lazaret_core::records::{Domain, DurableDecision, Generation, Session, SessionPhase}; use lazaret_core::records::{Domain, DurableDecision, Generation, Session, SessionPhase};
use lazaret_host_coordinator::domain_store::HostCoordinatorDomainStore; use lazaret_host_coordinator::domain_store::{DomainStoreMutation, HostCoordinatorDomainStore};
use lazaret_host_coordinator::lifecycle::{ use lazaret_host_coordinator::lifecycle::{
DomainPhase, HostCoordinatorLifecycle, LifecycleAction, LifecycleAdapter, LifecycleError, DomainPhase, HostCoordinatorLifecycle, LifecycleAction, LifecycleAdapter, LifecycleError,
SessionAdmissionIdentity, SessionLifecycleAdapter,
}; };
use lazaret_linux::filesystem::try_exclusive_flock; use lazaret_linux::filesystem::try_exclusive_flock;
#[derive(Default)] #[derive(Clone, Default)]
struct RecordingAdapter { struct RecordingAdapter {
actions: Vec<LifecycleAction>, actions: Arc<Mutex<Vec<LifecycleAction>>>,
} }
impl LifecycleAdapter for RecordingAdapter { impl LifecycleAdapter for RecordingAdapter {
type Error = Infallible; type Error = &'static str;
fn execute( fn execute(
&mut self, &mut self,
action: LifecycleAction, action: LifecycleAction,
_generation: &Generation, _generation: &Generation,
) -> Result<(), Self::Error> { ) -> Result<(), Self::Error> {
self.actions.push(action); self.actions.lock().unwrap().push(action);
Ok(()) Ok(())
} }
} }
#[derive(Clone)]
struct TestAdmission {
session: Session,
fingerprint: ConfigurationFingerprint,
}
#[derive(Clone)]
struct RecordingSessionAdapter {
actions: Arc<Mutex<Vec<LifecycleAction>>>,
}
impl SessionLifecycleAdapter for RecordingSessionAdapter {
type Error = &'static str;
type Admission = TestAdmission;
fn admission_identity(&self, admission: &Self::Admission) -> SessionAdmissionIdentity {
SessionAdmissionIdentity {
generation_id: admission.session.generation_id,
session_id: admission.session.session_id,
configuration_fingerprint: admission.fingerprint,
}
}
fn register_session(
&self,
store: &HostCoordinatorDomainStore,
entry_action: LifecycleAction,
admission: Self::Admission,
) -> Result<Session, Self::Error> {
let snapshot = store.snapshot().map_err(|_| "snapshot")?;
if snapshot.sessions.iter().any(|current| {
current.session_id == admission.session.session_id
|| (matches!(
current.session_phase,
SessionPhase::Waiting
| SessionPhase::Registering
| SessionPhase::Attached
| SessionPhase::Prompting
| SessionPhase::HostExecuting
) && (current.cli_id == admission.session.cli_id
|| current.session_anchor_id == admission.session.session_anchor_id))
}) && entry_action == LifecycleAction::StartReadySession
{
return Err("duplicate identity");
}
self.actions.lock().unwrap().push(entry_action);
self.actions
.lock()
.unwrap()
.push(LifecycleAction::RegisterSession);
let registering = Session {
session_phase: SessionPhase::Registering,
..admission.session
};
let attached = Session {
session_phase: SessionPhase::Attached,
..registering.clone()
};
store
.apply(&[
DomainStoreMutation::PutSession(registering),
DomainStoreMutation::PutSession(attached.clone()),
])
.map_err(|_| "registration")?;
Ok(attached)
}
fn disconnect_session(
&self,
store: &HostCoordinatorDomainStore,
session: &Session,
) -> Result<(), Self::Error> {
self.actions
.lock()
.unwrap()
.push(LifecycleAction::DisconnectSession);
store
.apply(&[DomainStoreMutation::PutSession(Session {
session_phase: SessionPhase::Closed,
..session.clone()
})])
.map_err(|_| "disconnect")
}
}
fn adapters() -> (RecordingAdapter, RecordingSessionAdapter) {
let actions = Arc::new(Mutex::new(Vec::new()));
(
RecordingAdapter {
actions: Arc::clone(&actions),
},
RecordingSessionAdapter { actions },
)
}
fn admission(session: Session) -> TestAdmission {
TestAdmission {
session,
fingerprint: fingerprint(2),
}
}
fn bytes(value: u8) -> [u8; 32] { fn bytes(value: u8) -> [u8; 32] {
[value; 32] [value; 32]
} }
@ -82,7 +184,9 @@ fn open_store(directory: &tempfile::TempDir) -> HostCoordinatorDomainStore {
.unwrap() .unwrap()
} }
fn complete_startup(lifecycle: &mut HostCoordinatorLifecycle<'_, RecordingAdapter>) { fn complete_startup(
lifecycle: &mut HostCoordinatorLifecycle<'_, RecordingAdapter, RecordingSessionAdapter>,
) {
lifecycle.create_trust_anchor().unwrap(); lifecycle.create_trust_anchor().unwrap();
lifecycle.start_runtime_supervisor().unwrap(); lifecycle.start_runtime_supervisor().unwrap();
lifecycle.lease_prepared_lower().unwrap(); lifecycle.lease_prepared_lower().unwrap();
@ -99,7 +203,8 @@ fn complete_startup(lifecycle: &mut HostCoordinatorLifecycle<'_, RecordingAdapte
fn exact_start_join_disconnect_and_teardown_actions_are_ordered() { fn exact_start_join_disconnect_and_teardown_actions_are_ordered() {
let directory = tempfile::tempdir().unwrap(); let directory = tempfile::tempdir().unwrap();
let store = open_store(&directory); let store = open_store(&directory);
let mut lifecycle = HostCoordinatorLifecycle::new(&store, RecordingAdapter::default()); let (domain_adapter, session_adapter) = adapters();
let mut lifecycle = HostCoordinatorLifecycle::new(&store, domain_adapter, session_adapter);
let owner = session(10, 1); let owner = session(10, 1);
let waiter = session(11, 1); let waiter = session(11, 1);
@ -108,14 +213,8 @@ fn exact_start_join_disconnect_and_teardown_actions_are_ordered() {
.unwrap(); .unwrap();
lifecycle.wait_for_domain(waiter.clone()).unwrap(); lifecycle.wait_for_domain(waiter.clone()).unwrap();
complete_startup(&mut lifecycle); complete_startup(&mut lifecycle);
lifecycle lifecycle.admit_session(admission(owner.clone())).unwrap();
.advance_waiting_session(owner.session_id, fingerprint(2)) lifecycle.admit_session(admission(waiter.clone())).unwrap();
.unwrap();
lifecycle.register_session(owner.session_id).unwrap();
lifecycle
.advance_waiting_session(waiter.session_id, fingerprint(2))
.unwrap();
lifecycle.register_session(waiter.session_id).unwrap();
lifecycle.disconnect_session(owner.session_id).unwrap(); lifecycle.disconnect_session(owner.session_id).unwrap();
assert!(!lifecycle.is_teardown_ready()); assert!(!lifecycle.is_teardown_ready());
lifecycle.disconnect_session(waiter.session_id).unwrap(); lifecycle.disconnect_session(waiter.session_id).unwrap();
@ -131,7 +230,7 @@ fn exact_start_join_disconnect_and_teardown_actions_are_ordered() {
assert_eq!(lifecycle.phase(), DomainPhase::Absent); assert_eq!(lifecycle.phase(), DomainPhase::Absent);
assert!(!lifecycle.is_published()); assert!(!lifecycle.is_published());
assert_eq!( assert_eq!(
lifecycle.into_adapter().actions, lifecycle.into_adapters().0.actions.lock().unwrap().clone(),
vec![ vec![
LifecycleAction::StartNewDomain, LifecycleAction::StartNewDomain,
LifecycleAction::WaitForDomain, LifecycleAction::WaitForDomain,
@ -177,7 +276,8 @@ fn exact_start_join_disconnect_and_teardown_actions_are_ordered() {
fn publication_and_invalid_transitions_fail_before_adapter_execution() { fn publication_and_invalid_transitions_fail_before_adapter_execution() {
let directory = tempfile::tempdir().unwrap(); let directory = tempfile::tempdir().unwrap();
let store = open_store(&directory); let store = open_store(&directory);
let mut lifecycle = HostCoordinatorLifecycle::new(&store, RecordingAdapter::default()); let (domain_adapter, session_adapter) = adapters();
let mut lifecycle = HostCoordinatorLifecycle::new(&store, domain_adapter, session_adapter);
lifecycle lifecycle
.start_new_domain(generation(1), session(10, 1)) .start_new_domain(generation(1), session(10, 1))
.unwrap(); .unwrap();
@ -204,7 +304,7 @@ fn publication_and_invalid_transitions_fail_before_adapter_execution() {
}) })
)); ));
assert_eq!( assert_eq!(
lifecycle.into_adapter().actions, lifecycle.into_adapters().0.actions.lock().unwrap().clone(),
vec![LifecycleAction::StartNewDomain] vec![LifecycleAction::StartNewDomain]
); );
assert_eq!( assert_eq!(
@ -213,11 +313,40 @@ fn publication_and_invalid_transitions_fail_before_adapter_execution() {
); );
} }
#[test]
fn aliased_generation_role_identities_are_refused_before_adapter_or_store() {
let directory = tempfile::tempdir().unwrap();
let store = open_store(&directory);
let (domain_adapter, session_adapter) = adapters();
let mut lifecycle = HostCoordinatorLifecycle::new(&store, domain_adapter, session_adapter);
let mut aliased = generation(1);
aliased.network_sidecar_id = NetworkSidecarId::new(*aliased.coordinator_id.as_bytes());
assert!(matches!(
lifecycle.start_new_domain(aliased, session(10, 1)),
Err(LifecycleError::InvalidTransition {
action: LifecycleAction::StartNewDomain,
..
})
));
assert!(store.snapshot().unwrap().generations.is_empty());
assert!(
lifecycle
.into_adapters()
.0
.actions
.lock()
.unwrap()
.is_empty()
);
}
#[test] #[test]
fn configuration_mismatch_is_refused_without_registration() { fn configuration_mismatch_is_refused_without_registration() {
let directory = tempfile::tempdir().unwrap(); let directory = tempfile::tempdir().unwrap();
let store = open_store(&directory); let store = open_store(&directory);
let mut lifecycle = HostCoordinatorLifecycle::new(&store, RecordingAdapter::default()); let (domain_adapter, session_adapter) = adapters();
let mut lifecycle = HostCoordinatorLifecycle::new(&store, domain_adapter, session_adapter);
let owner = session(10, 1); let owner = session(10, 1);
lifecycle lifecycle
.start_new_domain(generation(1), owner.clone()) .start_new_domain(generation(1), owner.clone())
@ -226,7 +355,10 @@ fn configuration_mismatch_is_refused_without_registration() {
let mismatched = session(11, 1); let mismatched = session(11, 1);
assert!(matches!( assert!(matches!(
lifecycle.start_ready_session(mismatched.clone(), fingerprint(9)), lifecycle.admit_session(TestAdmission {
session: mismatched.clone(),
fingerprint: fingerprint(9),
}),
Err(LifecycleError::InvalidTransition { Err(LifecycleError::InvalidTransition {
action: LifecycleAction::StartReadySession, action: LifecycleAction::StartReadySession,
.. ..
@ -246,26 +378,35 @@ fn configuration_mismatch_is_refused_without_registration() {
fn waiting_session_restarts_after_normal_teardown() { fn waiting_session_restarts_after_normal_teardown() {
let directory = tempfile::tempdir().unwrap(); let directory = tempfile::tempdir().unwrap();
let store = open_store(&directory); let store = open_store(&directory);
let mut lifecycle = HostCoordinatorLifecycle::new(&store, RecordingAdapter::default()); let (domain_adapter, session_adapter) = adapters();
let mut lifecycle = HostCoordinatorLifecycle::new(&store, domain_adapter, session_adapter);
let owner = session(10, 1); let owner = session(10, 1);
lifecycle lifecycle
.start_new_domain(generation(1), owner.clone()) .start_new_domain(generation(1), owner.clone())
.unwrap(); .unwrap();
complete_startup(&mut lifecycle); complete_startup(&mut lifecycle);
lifecycle lifecycle.admit_session(admission(owner.clone())).unwrap();
.advance_waiting_session(owner.session_id, fingerprint(2))
.unwrap();
lifecycle.register_session(owner.session_id).unwrap();
lifecycle.disconnect_session(owner.session_id).unwrap(); lifecycle.disconnect_session(owner.session_id).unwrap();
lifecycle.begin_teardown().unwrap(); lifecycle.begin_teardown().unwrap();
let waiter = session(11, 1); let waiter = session(11, 1);
let second_waiter = session(12, 1);
lifecycle.wait_for_domain(waiter.clone()).unwrap(); lifecycle.wait_for_domain(waiter.clone()).unwrap();
lifecycle.wait_for_domain(second_waiter.clone()).unwrap();
lifecycle.stop_sandbox().unwrap(); lifecycle.stop_sandbox().unwrap();
lifecycle.stop_sidecar().unwrap(); lifecycle.stop_sidecar().unwrap();
lifecycle.unmount_state_anchor().unwrap(); lifecycle.unmount_state_anchor().unwrap();
lifecycle.release_prepared_lower_lease().unwrap(); lifecycle.release_prepared_lower_lease().unwrap();
lifecycle.graceful_supervisor_exit().unwrap(); lifecycle.graceful_supervisor_exit().unwrap();
lifecycle.finish_teardown().unwrap(); lifecycle.finish_teardown().unwrap();
let mut aliased_successor = generation(2);
aliased_successor.sandbox_id = SandboxId::new(*aliased_successor.coordinator_id.as_bytes());
assert!(matches!(
lifecycle.restart_waiting_domain(aliased_successor, waiter.session_id),
Err(LifecycleError::InvalidTransition {
action: LifecycleAction::RestartWaitingDomain,
..
})
));
lifecycle lifecycle
.restart_waiting_domain(generation(2), waiter.session_id) .restart_waiting_domain(generation(2), waiter.session_id)
.unwrap(); .unwrap();
@ -282,13 +423,66 @@ fn waiting_session_restarts_after_normal_teardown() {
&& row.session_id == waiter.session_id && row.session_id == waiter.session_id
&& row.session_phase == SessionPhase::Waiting && row.session_phase == SessionPhase::Waiting
})); }));
assert!(snapshot.sessions.iter().any(|row| {
row.generation_id == GenerationId::new(2)
&& row.session_id == second_waiter.session_id
&& row.session_phase == SessionPhase::Waiting
}));
drop(snapshot);
complete_startup(&mut lifecycle);
lifecycle
.admit_session(admission(Session {
generation_id: GenerationId::new(2),
..second_waiter
}))
.unwrap();
assert_eq!(
lifecycle.session_phase(SessionId::new(bytes(12))),
Some(SessionPhase::Attached)
);
}
#[test]
fn admission_adapter_refuses_duplicate_cli_and_anchor_identities() {
let directory = tempfile::tempdir().unwrap();
let store = open_store(&directory);
let (domain_adapter, session_adapter) = adapters();
let mut lifecycle = HostCoordinatorLifecycle::new(&store, domain_adapter, session_adapter);
let owner = session(10, 1);
lifecycle
.start_new_domain(generation(1), owner.clone())
.unwrap();
complete_startup(&mut lifecycle);
lifecycle.admit_session(admission(owner.clone())).unwrap();
let duplicate = Session {
session_id: SessionId::new(bytes(11)),
session_anchor_id: SessionAnchorId::new(bytes(99)),
..owner
};
assert!(matches!(
lifecycle.admit_session(admission(duplicate)),
Err(LifecycleError::Adapter("duplicate identity"))
));
assert_eq!(lifecycle.registered_sessions(), 1);
assert_eq!(
store
.snapshot()
.unwrap()
.sessions
.iter()
.filter(|row| row.session_phase == SessionPhase::Attached)
.count(),
1
);
} }
#[test] #[test]
fn serialized_concurrent_joins_register_distinct_sessions() { fn serialized_concurrent_joins_register_distinct_sessions() {
let directory = tempfile::tempdir().unwrap(); let directory = tempfile::tempdir().unwrap();
let store = open_store(&directory); let store = open_store(&directory);
let mut lifecycle = HostCoordinatorLifecycle::new(&store, RecordingAdapter::default()); let (domain_adapter, session_adapter) = adapters();
let mut lifecycle = HostCoordinatorLifecycle::new(&store, domain_adapter, session_adapter);
let owner = session(10, 1); let owner = session(10, 1);
lifecycle lifecycle
.start_new_domain(generation(1), owner.clone()) .start_new_domain(generation(1), owner.clone())
@ -302,10 +496,7 @@ fn serialized_concurrent_joins_register_distinct_sessions() {
scope.spawn(move || { scope.spawn(move || {
let candidate = session(id, 1); let candidate = session(id, 1);
let mut guard = lifecycle.lock().unwrap(); let mut guard = lifecycle.lock().unwrap();
guard guard.admit_session(admission(candidate)).unwrap();
.start_ready_session(candidate.clone(), fingerprint(2))
.unwrap();
guard.register_session(candidate.session_id).unwrap();
}); });
} }
}); });
@ -328,7 +519,8 @@ fn serialized_concurrent_joins_register_distinct_sessions() {
fn bootstrap_owner_disconnect_enters_failed_without_teardown_or_recovery() { fn bootstrap_owner_disconnect_enters_failed_without_teardown_or_recovery() {
let directory = tempfile::tempdir().unwrap(); let directory = tempfile::tempdir().unwrap();
let store = open_store(&directory); let store = open_store(&directory);
let mut lifecycle = HostCoordinatorLifecycle::new(&store, RecordingAdapter::default()); let (domain_adapter, session_adapter) = adapters();
let mut lifecycle = HostCoordinatorLifecycle::new(&store, domain_adapter, session_adapter);
let owner = session(10, 1); let owner = session(10, 1);
lifecycle lifecycle
.start_new_domain(generation(1), owner.clone()) .start_new_domain(generation(1), owner.clone())