Route lifecycle admission through the control registry
This commit is contained in:
parent
671473106c
commit
510dfccf5a
3 changed files with 431 additions and 158 deletions
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue