Unify host coordinator lifecycle state

This commit is contained in:
ms2k 2026-07-19 04:17:25 +09:00
commit 2f8a9f9178

View file

@ -1,5 +1,7 @@
use std::cell::Cell;
use std::fs::OpenOptions;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use lazaret_core::identity::{
@ -16,6 +18,31 @@ use lazaret_host_coordinator::lifecycle::{
use lazaret_linux::filesystem::try_exclusive_flock;
use rustix::pipe::{PipeFlags, pipe_with};
static NEXT_DIRECTORY: AtomicU64 = AtomicU64::new(0);
struct TestDirectory(PathBuf);
impl TestDirectory {
fn new() -> Self {
let sequence = NEXT_DIRECTORY.fetch_add(1, Ordering::Relaxed);
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../target/test-artifacts/host-lifecycle")
.join(format!("{}-{sequence}", std::process::id()));
std::fs::create_dir_all(&path).unwrap();
Self(path)
}
fn path(&self) -> &Path {
&self.0
}
}
impl Drop for TestDirectory {
fn drop(&mut self) {
std::fs::remove_dir_all(&self.0).unwrap();
}
}
#[derive(Clone, Default)]
struct RecordingAdapter {
actions: Arc<Mutex<Vec<LifecycleAction>>>,
@ -200,7 +227,7 @@ fn session(id: u8, generation_id: u64) -> Session {
}
}
fn open_store(directory: &tempfile::TempDir) -> HostCoordinatorDomainStore {
fn open_store(directory: &TestDirectory) -> HostCoordinatorDomainStore {
let lock_file = OpenOptions::new()
.read(true)
.write(true)
@ -228,9 +255,55 @@ fn complete_startup(
lifecycle.publish_domain().unwrap();
}
#[test]
fn absent_aggregate_refuses_every_non_entry_action_before_effects() {
let directory = TestDirectory::new();
let store = open_store(&directory);
let (domain_adapter, session_adapter) = adapters();
let actions = Arc::clone(&domain_adapter.actions);
let mut lifecycle = HostCoordinatorLifecycle::new(&store, domain_adapter, session_adapter);
macro_rules! refused {
($operation:expr) => {
assert!(matches!(
$operation,
Err(LifecycleError::InvalidTransition { .. })
));
};
}
refused!(lifecycle.admit_session(admission(session(10, 1))));
refused!(lifecycle.wait_for_domain(session(10, 1)));
refused!(lifecycle.restart_waiting_domain(generation(1), SessionId::new(bytes(10))));
refused!(lifecycle.create_trust_anchor());
refused!(lifecycle.start_runtime_supervisor());
refused!(lifecycle.lease_prepared_lower());
refused!(lifecycle.mount_state_anchor());
refused!(lifecycle.start_sidecar());
refused!(lifecycle.start_connectivity_helper());
refused!(lifecycle.mark_network_ready());
refused!(lifecycle.arm_runtime_causal_edge());
refused!(lifecycle.start_sandbox());
refused!(lifecycle.publish_domain());
refused!(lifecycle.reject_new_configuration_mismatch(session(10, 1), fingerprint(9)));
refused!(lifecycle.reject_configuration_mismatch(SessionId::new(bytes(10)), fingerprint(9)));
refused!(lifecycle.disconnect_session(SessionId::new(bytes(10))));
refused!(lifecycle.reconcile_published_bootstrap_owner(SessionId::new(bytes(10)), false));
refused!(lifecycle.begin_teardown());
refused!(lifecycle.stop_sandbox());
refused!(lifecycle.stop_sidecar());
refused!(lifecycle.unmount_state_anchor());
refused!(lifecycle.release_prepared_lower_lease());
refused!(lifecycle.graceful_supervisor_exit());
refused!(lifecycle.finish_teardown());
assert!(actions.lock().unwrap().is_empty());
assert!(store.snapshot().unwrap().generations.is_empty());
}
#[test]
fn bootstrap_owner_guard_failure_uses_exact_modeled_disconnect_transition() {
let directory = tempfile::tempdir().unwrap();
let directory = TestDirectory::new();
let store = open_store(&directory);
let (domain_adapter, session_adapter) = adapters();
let actions = Arc::clone(&domain_adapter.actions);
@ -260,7 +333,7 @@ fn bootstrap_owner_guard_failure_uses_exact_modeled_disconnect_transition() {
#[test]
fn published_bootstrap_owner_eof_disconnects_waiting_session_and_tears_down() {
let directory = tempfile::tempdir().unwrap();
let directory = TestDirectory::new();
let store = open_store(&directory);
let (domain_adapter, session_adapter) = adapters();
let mut lifecycle = HostCoordinatorLifecycle::new(&store, domain_adapter, session_adapter);
@ -287,7 +360,7 @@ fn published_bootstrap_owner_eof_disconnects_waiting_session_and_tears_down() {
#[test]
fn admitted_bootstrap_owner_ignores_later_bootstrap_pipe_eof() {
let directory = tempfile::tempdir().unwrap();
let directory = TestDirectory::new();
let store = open_store(&directory);
let (domain_adapter, session_adapter) = adapters();
let mut lifecycle = HostCoordinatorLifecycle::new(&store, domain_adapter, session_adapter);
@ -315,7 +388,7 @@ fn admitted_bootstrap_owner_ignores_later_bootstrap_pipe_eof() {
#[test]
fn exact_start_join_disconnect_and_teardown_actions_are_ordered() {
let directory = tempfile::tempdir().unwrap();
let directory = TestDirectory::new();
let store = open_store(&directory);
let (domain_adapter, session_adapter) = adapters();
let mut lifecycle = HostCoordinatorLifecycle::new(&store, domain_adapter, session_adapter);
@ -383,7 +456,7 @@ fn exact_start_join_disconnect_and_teardown_actions_are_ordered() {
#[test]
fn cold_start_entry_reaches_publication_in_exact_modeled_order() {
let directory = tempfile::tempdir().unwrap();
let directory = TestDirectory::new();
let store = open_store(&directory);
let (domain_adapter, session_adapter) = adapters();
let mut lifecycle = HostCoordinatorLifecycle::new(&store, domain_adapter, session_adapter);
@ -414,7 +487,7 @@ fn cold_start_entry_reaches_publication_in_exact_modeled_order() {
#[test]
fn publication_exposure_failure_retains_recoverable_durable_decision() {
let directory = tempfile::tempdir().unwrap();
let directory = TestDirectory::new();
let store = open_store(&directory);
let (_, session_adapter) = adapters();
let mut lifecycle =
@ -435,7 +508,7 @@ fn publication_exposure_failure_retains_recoverable_durable_decision() {
#[test]
fn publication_and_invalid_transitions_fail_before_adapter_execution() {
let directory = tempfile::tempdir().unwrap();
let directory = TestDirectory::new();
let store = open_store(&directory);
let (domain_adapter, session_adapter) = adapters();
let mut lifecycle = HostCoordinatorLifecycle::new(&store, domain_adapter, session_adapter);
@ -476,7 +549,7 @@ 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 directory = TestDirectory::new();
let store = open_store(&directory);
let (domain_adapter, session_adapter) = adapters();
let mut lifecycle = HostCoordinatorLifecycle::new(&store, domain_adapter, session_adapter);
@ -504,7 +577,7 @@ fn aliased_generation_role_identities_are_refused_before_adapter_or_store() {
#[test]
fn configuration_mismatch_is_refused_without_registration() {
let directory = tempfile::tempdir().unwrap();
let directory = TestDirectory::new();
let store = open_store(&directory);
let (domain_adapter, session_adapter) = adapters();
let mut lifecycle = HostCoordinatorLifecycle::new(&store, domain_adapter, session_adapter);
@ -537,7 +610,7 @@ fn configuration_mismatch_is_refused_without_registration() {
#[test]
fn waiting_session_restarts_after_normal_teardown() {
let directory = tempfile::tempdir().unwrap();
let directory = TestDirectory::new();
let store = open_store(&directory);
let (domain_adapter, session_adapter) = adapters();
let mut lifecycle = HostCoordinatorLifecycle::new(&store, domain_adapter, session_adapter);
@ -605,7 +678,7 @@ fn waiting_session_restarts_after_normal_teardown() {
#[test]
fn admission_adapter_refuses_duplicate_cli_and_anchor_identities() {
let directory = tempfile::tempdir().unwrap();
let directory = TestDirectory::new();
let store = open_store(&directory);
let (domain_adapter, session_adapter) = adapters();
let mut lifecycle = HostCoordinatorLifecycle::new(&store, domain_adapter, session_adapter);
@ -640,7 +713,7 @@ fn admission_adapter_refuses_duplicate_cli_and_anchor_identities() {
#[test]
fn serialized_concurrent_joins_register_distinct_sessions() {
let directory = tempfile::tempdir().unwrap();
let directory = TestDirectory::new();
let store = open_store(&directory);
let (domain_adapter, session_adapter) = adapters();
let mut lifecycle = HostCoordinatorLifecycle::new(&store, domain_adapter, session_adapter);
@ -678,7 +751,7 @@ fn serialized_concurrent_joins_register_distinct_sessions() {
#[test]
fn bootstrap_owner_disconnect_enters_failed_without_teardown_or_recovery() {
let directory = tempfile::tempdir().unwrap();
let directory = TestDirectory::new();
let store = open_store(&directory);
let (domain_adapter, session_adapter) = adapters();
let mut lifecycle = HostCoordinatorLifecycle::new(&store, domain_adapter, session_adapter);