use std::collections::HashMap;
use std::path::Path;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex, mpsc};
use std::time::Instant;
use haematite::{Database, DatabaseConfig, EventStore};
use liminal::channel::{ChannelConfig, ChannelHandle, ChannelMode, Schema};
use liminal::conversation::{
ConversationSupervisor, CrashPolicy, EchoBehaviour, ParticipantBehaviour,
};
use liminal::durability::bridge::block_on;
use liminal::durability::{
DedupCache, DedupDecision, DurabilityError, DurableStore, EphemeralHaematiteStore,
HaematiteStore, ProcessingReceipt, open_ephemeral,
};
use liminal::protocol::{MessageEnvelope, ProtocolError, SchemaId as ProtocolSchemaId};
use super::conversation::{ConnectionConversation, LiminalConversationResource};
use super::services_cluster::build_channel_cluster;
use super::services_schema::resolve_channel_schema;
use super::worker_front_door::WorkerFrontDoorServices;
use crate::ServerError;
use crate::config::types::{ClusterConfig, ServerConfig, ServiceProfile};
pub use super::services_cluster::ChannelCluster;
type ResponderRegistry = HashMap<String, Arc<dyn ParticipantBehaviour>>;
pub trait SubscriptionResource: std::fmt::Debug + Send {
fn unsubscribe(self: Box<Self>) -> Result<(), ServerError>;
fn try_next(&mut self) -> Option<liminal::envelope::Envelope>;
fn has_pending(&self) -> bool;
fn is_overflowed(&self) -> bool {
false
}
}
#[derive(Debug)]
pub struct ConnectionSubscription {
id: u64,
stream_id: u32,
selected_schema: ProtocolSchemaId,
resource: Box<dyn SubscriptionResource>,
}
impl ConnectionSubscription {
#[must_use]
pub fn new(
id: u64,
selected_schema: ProtocolSchemaId,
resource: Box<dyn SubscriptionResource>,
) -> Self {
Self {
id,
stream_id: 0,
selected_schema,
resource,
}
}
#[must_use]
pub const fn id(&self) -> u64 {
self.id
}
pub(super) const fn set_stream_id(&mut self, stream_id: u32) {
self.stream_id = stream_id;
}
#[must_use]
pub(super) const fn stream_id(&self) -> u32 {
self.stream_id
}
#[must_use]
pub const fn selected_schema(&self) -> ProtocolSchemaId {
self.selected_schema
}
pub(super) fn try_next(&mut self) -> Option<liminal::envelope::Envelope> {
self.resource.try_next()
}
pub(super) fn has_pending(&self) -> bool {
self.resource.has_pending()
}
pub(super) fn is_overflowed(&self) -> bool {
self.resource.is_overflowed()
}
pub(super) fn unsubscribe(self) -> Result<(), ServerError> {
self.resource.unsubscribe()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PublishOutcome {
pub message_id: u64,
pub delivered: bool,
}
pub trait ConnectionServices: std::fmt::Debug + Send + Sync {
fn publish(
&self,
channel: &str,
envelope: &MessageEnvelope,
idempotency_key: Option<&str>,
) -> Result<PublishOutcome, ServerError>;
fn subscribe(
&self,
channel: &str,
accepted_schemas: &[ProtocolSchemaId],
install: Option<liminal::channel::InboxInstall>,
) -> Result<ConnectionSubscription, ServerError>;
fn unsubscribe(&self, subscription: ConnectionSubscription) -> Result<(), ServerError>;
fn open_conversation(
&self,
conversation_id: u64,
subject: &str,
) -> Result<ConnectionConversation, ServerError>;
fn conversation_message(
&self,
conversation: &ConnectionConversation,
envelope: &MessageEnvelope,
) -> Result<(), ServerError>;
fn close_conversation(&self, conversation: ConnectionConversation) -> Result<(), ServerError>;
fn flush_durable_state(&self) -> Result<(), ServerError>;
fn supports_channel_operations(&self) -> bool {
true
}
}
#[derive(Debug)]
pub struct LiminalConnectionServices {
channels: HashMap<String, ConfiguredChannel>,
cluster: ChannelCluster,
durable_store: Arc<dyn DurableStore>,
dedup: DedupCache,
conversation_supervisor: Arc<ConversationSupervisor>,
responders: Mutex<ResponderRegistry>,
next_message_id: AtomicU64,
next_subscription_id: AtomicU64,
}
impl LiminalConnectionServices {
pub fn from_config(config: &ServerConfig) -> Result<Self, ServerError> {
require_full_profile(config)?;
let store = ProductionSubsystems.durable_store(config.persistence_path.as_deref())?;
Self::from_config_with_store_via(config, store, &ProductionSubsystems)
}
pub fn from_config_with_store(
config: &ServerConfig,
durable_store: Arc<dyn DurableStore>,
) -> Result<Self, ServerError> {
require_full_profile(config)?;
Self::from_config_with_store_via(config, durable_store, &ProductionSubsystems)
}
fn from_config_with_store_via(
config: &ServerConfig,
durable_store: Arc<dyn DurableStore>,
subsystems: &dyn SubsystemFactory,
) -> Result<Self, ServerError> {
let cluster = subsystems.channel_cluster(config.cluster.as_ref())?;
let mut channels = HashMap::new();
for channel in &config.channels {
let resolved = resolve_channel_schema(channel);
let schema =
Schema::new(resolved.document).map_err(|error| ServerError::ConfigValidation {
message: format!("failed to initialize channel '{}': {error}", channel.name),
})?;
let channel_config = if channel.durable {
ChannelConfig::new(channel.name.clone(), schema, ChannelMode::Durable)
} else {
ChannelConfig::new(channel.name.clone(), schema, ChannelMode::Ephemeral)
};
let handle = if channel.durable {
ChannelHandle::new_durable_with_supervisor(
channel_config,
Arc::clone(&durable_store),
cluster.supervisor().clone(),
)
.map_err(|error| ServerError::ConfigValidation {
message: format!(
"failed to initialize durable channel '{}': {error}",
channel.name
),
})?
} else {
ChannelHandle::with_supervisor(channel_config, cluster.supervisor().clone())
};
channels.insert(
channel.name.clone(),
ConfiguredChannel {
handle,
protocol_schema: resolved.protocol_id,
},
);
}
let conversation_supervisor = subsystems.conversation_supervisor()?;
let dedup = DedupCache::new(Arc::clone(&durable_store), DELIVERY_DEDUP_NAMESPACE);
Ok(Self {
channels,
cluster,
durable_store,
dedup,
conversation_supervisor,
responders: Mutex::new(HashMap::new()),
next_message_id: AtomicU64::new(1),
next_subscription_id: AtomicU64::new(1),
})
}
pub fn empty() -> Result<Self, ServerError> {
let conversation_supervisor = ProductionSubsystems.conversation_supervisor()?;
let durable_store = build_durable_store(None)?;
let dedup = DedupCache::new(Arc::clone(&durable_store), DELIVERY_DEDUP_NAMESPACE);
Ok(Self {
channels: HashMap::new(),
cluster: build_channel_cluster(None)?,
durable_store,
dedup,
conversation_supervisor,
responders: Mutex::new(HashMap::new()),
next_message_id: AtomicU64::new(1),
next_subscription_id: AtomicU64::new(1),
})
}
#[must_use]
pub const fn channel_cluster(&self) -> &ChannelCluster {
&self.cluster
}
#[must_use]
pub fn durable_store(&self) -> Arc<dyn DurableStore> {
Arc::clone(&self.durable_store)
}
#[must_use]
pub fn conversation_supervisor(&self) -> Arc<ConversationSupervisor> {
Arc::clone(&self.conversation_supervisor)
}
pub fn register_responder(
&self,
subject: impl Into<String>,
behaviour: Arc<dyn ParticipantBehaviour>,
) -> Result<Option<Arc<dyn ParticipantBehaviour>>, ServerError> {
let mut responders = self.lock_responders()?;
Ok(responders.insert(subject.into(), behaviour))
}
pub fn unregister_responder(
&self,
subject: &str,
) -> Result<Option<Arc<dyn ParticipantBehaviour>>, ServerError> {
let mut responders = self.lock_responders()?;
Ok(responders.remove(subject))
}
fn responder_for(&self, subject: &str) -> Result<Arc<dyn ParticipantBehaviour>, ServerError> {
let responders = self.lock_responders()?;
Ok(responders.get(subject).map_or_else(
|| Arc::new(EchoBehaviour) as Arc<dyn ParticipantBehaviour>,
Arc::clone,
))
}
fn lock_responders(&self) -> Result<std::sync::MutexGuard<'_, ResponderRegistry>, ServerError> {
self.responders
.lock()
.map_err(|_poisoned| ServerError::ListenerAccept {
message: "responder registry lock poisoned".to_owned(),
})
}
#[cfg(test)]
pub(crate) fn subscribe_handle_for_test(
&self,
channel: &str,
) -> Result<liminal::channel::SubscriptionHandle, ServerError> {
let configured = self
.channels
.get(channel)
.ok_or_else(|| ServerError::ListenerAccept {
message: format!("channel '{channel}' is not configured"),
})?;
configured
.handle
.subscribe()
.map_err(|error| ServerError::ListenerAccept {
message: format!("liminal subscribe failed for channel '{channel}': {error}"),
})
}
fn claim_delivery(&self, key: &str) -> Result<bool, ServerError> {
let decision = block_on(self.dedup.claim_or_get(key, dedup_timestamp_millis()))
.map_err(|error| ServerError::ListenerAccept {
message: format!("dedup bridge failed for key '{key}': {error}"),
})?
.map_err(|error| ServerError::ListenerAccept {
message: format!("dedup claim failed for key '{key}': {error}"),
})?;
Ok(matches!(decision, DedupDecision::Claimed))
}
fn release_claim(&self, key: &str) {
match block_on(self.dedup.release_claim(key)) {
Ok(Ok(())) => {}
Ok(Err(error)) => {
tracing::error!(
idempotency_key = key,
%error,
"failed to release dedup claim after publish failure; key may stay suppressed"
);
}
Err(error) => {
tracing::error!(
idempotency_key = key,
%error,
"dedup release bridge failed after publish failure; key may stay suppressed"
);
}
}
}
}
fn dedup_timestamp_millis() -> u64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.ok()
.and_then(|duration| u64::try_from(duration.as_millis()).ok())
.unwrap_or(0)
}
const DEFAULT_SHARD_COUNT: usize = 8;
const DELIVERY_DEDUP_NAMESPACE: &str = "liminal:delivery-dedup";
#[cfg(test)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(super) enum SchedulerSubsystem {
ChannelSupervisor,
ConversationSupervisor,
HaematiteStore,
}
pub(super) trait SubsystemFactory {
fn channel_cluster(
&self,
cluster_config: Option<&ClusterConfig>,
) -> Result<ChannelCluster, ServerError>;
fn conversation_supervisor(&self) -> Result<Arc<ConversationSupervisor>, ServerError>;
fn durable_store(
&self,
persistence_path: Option<&Path>,
) -> Result<Arc<dyn DurableStore>, ServerError>;
}
pub(super) struct ProductionSubsystems;
impl SubsystemFactory for ProductionSubsystems {
fn channel_cluster(
&self,
cluster_config: Option<&ClusterConfig>,
) -> Result<ChannelCluster, ServerError> {
build_channel_cluster(cluster_config)
}
fn conversation_supervisor(&self) -> Result<Arc<ConversationSupervisor>, ServerError> {
Ok(Arc::new(ConversationSupervisor::new().map_err(
|error| ServerError::ConfigValidation {
message: format!("failed to start conversation supervisor: {error}"),
},
)?))
}
fn durable_store(
&self,
persistence_path: Option<&Path>,
) -> Result<Arc<dyn DurableStore>, ServerError> {
build_durable_store(persistence_path)
}
}
#[cfg(test)]
#[allow(clippy::expect_used)]
pub(super) mod subsystem_census {
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use liminal::conversation::ConversationSupervisor;
use liminal::durability::{DurableStore, open_ephemeral_rooted};
use super::{
ChannelCluster, DEFAULT_SHARD_COUNT, ProductionSubsystems, SchedulerSubsystem,
SubsystemFactory, build_durable_store_with,
};
use crate::ServerError;
use crate::config::types::ClusterConfig;
pub struct RecordingSubsystems {
census: Mutex<Vec<SchedulerSubsystem>>,
ephemeral_root: PathBuf,
}
impl RecordingSubsystems {
pub fn rooted(ephemeral_root: &Path) -> Self {
Self {
census: Mutex::new(Vec::new()),
ephemeral_root: ephemeral_root.to_path_buf(),
}
}
pub fn recorded(&self) -> Vec<SchedulerSubsystem> {
let mut recorded = self
.census
.lock()
.expect("subsystem census lock is never poisoned in tests")
.clone();
recorded.sort();
recorded
}
fn record(&self, subsystem: SchedulerSubsystem) {
self.census
.lock()
.expect("subsystem census lock is never poisoned in tests")
.push(subsystem);
}
}
impl SubsystemFactory for RecordingSubsystems {
fn channel_cluster(
&self,
cluster_config: Option<&ClusterConfig>,
) -> Result<ChannelCluster, ServerError> {
let cluster = ProductionSubsystems.channel_cluster(cluster_config)?;
self.record(SchedulerSubsystem::ChannelSupervisor);
Ok(cluster)
}
fn conversation_supervisor(&self) -> Result<Arc<ConversationSupervisor>, ServerError> {
let supervisor = ProductionSubsystems.conversation_supervisor()?;
self.record(SchedulerSubsystem::ConversationSupervisor);
Ok(supervisor)
}
fn durable_store(
&self,
persistence_path: Option<&Path>,
) -> Result<Arc<dyn DurableStore>, ServerError> {
let store = build_durable_store_with(persistence_path, || {
open_ephemeral_rooted(&self.ephemeral_root, DEFAULT_SHARD_COUNT)
})?;
self.record(SchedulerSubsystem::HaematiteStore);
Ok(store)
}
}
}
fn require_full_profile(config: &ServerConfig) -> Result<(), ServerError> {
match config.services.profile()? {
ServiceProfile::Full => Ok(()),
ServiceProfile::WorkerFrontDoor => Err(ServerError::ConfigValidation {
message: format!(
"services.profile: \"{}\" cannot construct the full LiminalConnectionServices; \
build profile-selected services via build_connection_services",
ServiceProfile::WORKER_FRONT_DOOR
),
}),
}
}
pub fn build_connection_services(
config: &ServerConfig,
) -> Result<Arc<dyn ConnectionServices>, ServerError> {
build_connection_services_via(config, &ProductionSubsystems)
}
pub(super) fn build_connection_services_via(
config: &ServerConfig,
subsystems: &dyn SubsystemFactory,
) -> Result<Arc<dyn ConnectionServices>, ServerError> {
match config.services.profile()? {
ServiceProfile::Full => {
let store = subsystems.durable_store(config.persistence_path.as_deref())?;
Ok(Arc::new(
LiminalConnectionServices::from_config_with_store_via(config, store, subsystems)?,
))
}
ServiceProfile::WorkerFrontDoor => {
let errors = crate::config::validation::worker_front_door_field_errors(config);
if !errors.is_empty() {
return Err(ServerError::ConfigValidation {
message: errors.join("; "),
});
}
Ok(Arc::new(WorkerFrontDoorServices::new()))
}
}
}
fn build_durable_store(
persistence_path: Option<&Path>,
) -> Result<Arc<dyn DurableStore>, ServerError> {
build_durable_store_with(persistence_path, || open_ephemeral(DEFAULT_SHARD_COUNT))
}
fn build_durable_store_with(
persistence_path: Option<&Path>,
make_ephemeral: impl FnOnce() -> Result<EphemeralHaematiteStore, DurabilityError>,
) -> Result<Arc<dyn DurableStore>, ServerError> {
let Some(path) = persistence_path else {
let store = make_ephemeral().map_err(|error| ServerError::ConfigValidation {
message: format!("failed to open ephemeral durable store: {error}"),
})?;
return Ok(Arc::new(store));
};
let data_dir = path.join("durability");
let database = open_or_create_database(&data_dir)?;
let event_store = EventStore::new(database);
Ok(Arc::new(HaematiteStore::new(Arc::new(event_store))))
}
fn open_or_create_database(data_dir: &Path) -> Result<Database, ServerError> {
let config_file = data_dir.join("config.json");
let result = if config_file.exists() {
Database::open(data_dir)
} else {
Database::create(DatabaseConfig {
data_dir: data_dir.to_path_buf(),
shard_count: DEFAULT_SHARD_COUNT,
sweep_interval: None,
distributed: None,
})
};
result.map_err(|error| ServerError::ConfigValidation {
message: format!(
"failed to open durable store at {}: {error}",
data_dir.display()
),
})
}
impl ConnectionServices for LiminalConnectionServices {
fn publish(
&self,
channel: &str,
envelope: &MessageEnvelope,
idempotency_key: Option<&str>,
) -> Result<PublishOutcome, ServerError> {
let handle = self
.channels
.get(channel)
.map(|configured| configured.handle.clone())
.ok_or_else(|| ServerError::ListenerAccept {
message: format!("channel '{channel}' is not configured"),
})?;
if let Some(key) = idempotency_key {
if !self.claim_delivery(key)? {
crate::metrics::publish_accepted();
return Ok(PublishOutcome {
message_id: self.next_message_id.fetch_add(1, Ordering::Relaxed),
delivered: false,
});
}
}
let delivery = handle.publish_with_delivery(
&envelope.payload,
liminal::envelope::PublisherId::default(),
None,
);
let delivery = match delivery {
Ok(delivery) => delivery,
Err(error) => {
if let Some(key) = idempotency_key {
self.release_claim(key);
}
return Err(ServerError::ListenerAccept {
message: format!("liminal publish failed for channel '{channel}': {error}"),
});
}
};
if let Some(key) = idempotency_key {
block_on(
self.dedup
.complete_receipt(key, ProcessingReceipt::new(Vec::new())),
)
.map_err(|error| ServerError::ListenerAccept {
message: format!("dedup receipt bridge failed for key '{key}': {error}"),
})?
.map_err(|error| ServerError::ListenerAccept {
message: format!("dedup receipt write failed for key '{key}': {error}"),
})?;
}
crate::metrics::publish_accepted();
let delivered_count = u64::try_from(delivery.delivered_count()).unwrap_or(u64::MAX);
crate::metrics::deliveries_recorded(delivered_count);
Ok(PublishOutcome {
message_id: self.next_message_id.fetch_add(1, Ordering::Relaxed),
delivered: delivery.is_delivered(),
})
}
fn subscribe(
&self,
channel: &str,
accepted_schemas: &[ProtocolSchemaId],
install: Option<liminal::channel::InboxInstall>,
) -> Result<ConnectionSubscription, ServerError> {
let configured = self
.channels
.get(channel)
.ok_or_else(|| ServerError::ListenerAccept {
message: format!("channel '{channel}' is not configured"),
})?;
let selected_schema = if accepted_schemas.is_empty() {
configured.protocol_schema
} else {
liminal::protocol::negotiate_schema(configured.protocol_schema, accepted_schemas)
.map_err(|error| server_error_from_protocol(&error))?
};
let subscription = install
.map_or_else(
|| configured.handle.subscribe(),
|install| configured.handle.subscribe_with_install(install),
)
.map_err(|error| ServerError::ListenerAccept {
message: format!("liminal subscribe failed for channel '{channel}': {error}"),
})?;
let id = self.next_subscription_id.fetch_add(1, Ordering::Relaxed);
Ok(ConnectionSubscription::new(
id,
selected_schema,
Box::new(LiminalSubscriptionResource { subscription }),
))
}
fn unsubscribe(&self, subscription: ConnectionSubscription) -> Result<(), ServerError> {
subscription.unsubscribe()
}
fn open_conversation(
&self,
conversation_id: u64,
subject: &str,
) -> Result<ConnectionConversation, ServerError> {
let behaviour = self.responder_for(subject)?;
let (actor, participant) = self
.conversation_supervisor
.spawn_with_participant(behaviour, None, ChannelMode::Ephemeral, CrashPolicy::Fail)
.map_err(|error| ServerError::ListenerAccept {
message: format!(
"failed to spawn supervised conversation {conversation_id} ('{subject}'): {error}"
),
})?;
actor.pid().map_err(|error| ServerError::ListenerAccept {
message: format!(
"failed to boot supervised conversation {conversation_id} ('{subject}'): {error}"
),
})?;
let (exit_tx, exit_rx) = mpsc::sync_channel::<Instant>(1);
actor
.notify_on_participant_exit(participant, exit_tx)
.map_err(|error| ServerError::ListenerAccept {
message: format!(
"failed to arm crash detection for conversation {conversation_id}: {error}"
),
})?;
Ok(ConnectionConversation::new(Box::new(
LiminalConversationResource::new(actor, participant, exit_rx),
)))
}
fn conversation_message(
&self,
conversation: &ConnectionConversation,
envelope: &MessageEnvelope,
) -> Result<(), ServerError> {
conversation.message(envelope)
}
fn close_conversation(&self, conversation: ConnectionConversation) -> Result<(), ServerError> {
conversation.close()
}
fn flush_durable_state(&self) -> Result<(), ServerError> {
for (channel_name, configured) in &self.channels {
if configured.handle.config().mode == ChannelMode::Durable {
configured
.handle
.flush()
.map_err(|error| ServerError::ShutdownFlush {
message: format!(
"failed to flush durable channel '{channel_name}': {error}"
),
})?;
}
}
Ok(())
}
}
#[derive(Debug)]
struct ConfiguredChannel {
handle: ChannelHandle,
protocol_schema: ProtocolSchemaId,
}
#[derive(Debug)]
struct LiminalSubscriptionResource {
subscription: liminal::channel::SubscriptionHandle,
}
impl SubscriptionResource for LiminalSubscriptionResource {
fn unsubscribe(self: Box<Self>) -> Result<(), ServerError> {
drop(self.subscription);
Ok(())
}
fn is_overflowed(&self) -> bool {
self.subscription.is_overflowed()
}
fn has_pending(&self) -> bool {
self.subscription.has_pending()
}
fn try_next(&mut self) -> Option<liminal::envelope::Envelope> {
match self.subscription.try_next() {
Ok(envelope) => envelope,
Err(error) => {
tracing::error!(
%error,
"subscription inbox lock is poisoned; this subscription is now \
permanently silent and will deliver no further messages"
);
None
}
}
}
}
pub(super) fn server_error_from_protocol(error: &ProtocolError) -> ServerError {
ServerError::ListenerAccept {
message: format!("protocol operation failed: {error}"),
}
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
mod durable_store_tests {
use liminal::durability::open_ephemeral_rooted;
use super::subsystem_census::RecordingSubsystems;
use super::{
ConnectionServices, DEFAULT_SHARD_COUNT, LiminalConnectionServices, SchedulerSubsystem,
build_connection_services, build_connection_services_via, build_durable_store_with,
};
use crate::ServerError;
use crate::config::types::{LimitsConfig, ServerConfig, ServicesConfig};
fn entry_count(root: &std::path::Path) -> usize {
std::fs::read_dir(root)
.expect("ephemeral root is readable")
.count()
}
fn config_with_profile(profile: &str) -> ServerConfig {
ServerConfig {
listen_address: "127.0.0.1:0".parse().expect("valid socket addr"),
health_listen_address: "127.0.0.1:1".parse().expect("valid socket addr"),
drain_timeout_ms: 30_000,
channels: Vec::new(),
routing_rules: Vec::new(),
persistence_path: None,
cluster: None,
auth: None,
services: ServicesConfig {
profile: profile.to_owned(),
},
limits: LimitsConfig::default(),
}
}
#[test]
fn worker_front_door_builds_no_store_and_no_temp_dir() {
let front_door_root = tempfile::tempdir().expect("test can create an ephemeral root");
let full_root = tempfile::tempdir().expect("test can create an ephemeral root");
let front_door_subsystems = RecordingSubsystems::rooted(front_door_root.path());
let front_door: std::sync::Arc<dyn ConnectionServices> = build_connection_services_via(
&config_with_profile("worker-front-door"),
&front_door_subsystems,
)
.expect("worker-front-door services build");
assert!(
!front_door.supports_channel_operations(),
"the worker front door serves no channel operations"
);
assert_eq!(
entry_count(front_door_root.path()),
0,
"the worker front door creates no ephemeral store directory (no haematite, no temp dir)"
);
let full_subsystems = RecordingSubsystems::rooted(full_root.path());
let full = build_connection_services_via(&config_with_profile("full"), &full_subsystems)
.expect("full services build");
assert!(
full.supports_channel_operations(),
"full mode serves channel operations"
);
assert_eq!(
entry_count(full_root.path()),
1,
"full mode with no persistence path builds exactly one ephemeral store directory"
);
drop(front_door);
drop(full);
}
#[test]
fn worker_profile_census_is_empty_and_full_profile_records_all_schedulers() {
let worker_root = tempfile::tempdir().expect("test can create an ephemeral root");
let full_root = tempfile::tempdir().expect("test can create an ephemeral root");
let worker_subsystems = RecordingSubsystems::rooted(worker_root.path());
let front_door = build_connection_services_via(
&config_with_profile("worker-front-door"),
&worker_subsystems,
)
.expect("worker-front-door services build");
assert_eq!(
worker_subsystems.recorded(),
Vec::<SchedulerSubsystem>::new(),
"the worker front door constructs no scheduler-owning subsystem"
);
let full_subsystems = RecordingSubsystems::rooted(full_root.path());
let full = build_connection_services_via(&config_with_profile("full"), &full_subsystems)
.expect("full services build");
assert_eq!(
full_subsystems.recorded(),
vec![
SchedulerSubsystem::ChannelSupervisor,
SchedulerSubsystem::ConversationSupervisor,
SchedulerSubsystem::HaematiteStore,
],
"the full profile constructs every scheduler-owning subsystem, once each — \
the positive control proving the census instrument detects them"
);
drop(front_door);
drop(full);
}
#[test]
fn full_only_constructors_reject_worker_profile() {
let config = config_with_profile("worker-front-door");
let from_config = LiminalConnectionServices::from_config(&config);
assert!(
matches!(from_config, Err(ServerError::ConfigValidation { .. })),
"from_config must reject a worker-front-door profile with ConfigValidation, got {from_config:?}"
);
let root = tempfile::tempdir().expect("test can create an ephemeral root");
let store = open_ephemeral_rooted(root.path(), DEFAULT_SHARD_COUNT)
.expect("test store for the rejection check builds");
let from_config_with_store =
LiminalConnectionServices::from_config_with_store(&config, std::sync::Arc::new(store));
assert!(
matches!(
from_config_with_store,
Err(ServerError::ConfigValidation { .. })
),
"from_config_with_store must reject a worker-front-door profile with ConfigValidation"
);
}
#[test]
fn build_connection_services_rejects_worker_profile_with_full_only_fields() {
let mut config = config_with_profile("worker-front-door");
config.channels = vec![crate::config::types::ChannelDef {
name: "orders".to_owned(),
schema_ref: None,
durable: false,
loaded_schema: None,
}];
config.persistence_path = Some(std::path::PathBuf::from("/tmp"));
let result = build_connection_services(&config);
let Err(ServerError::ConfigValidation { message }) = result else {
panic!("expected ConfigValidation for worker profile with full-only fields");
};
assert!(message.contains("builds no channels"), "got: {message}");
assert!(
message.contains("builds no durable store"),
"got: {message}"
);
}
#[test]
fn persistent_store_uses_configured_path_and_creates_no_temp_dir() {
let home = tempfile::tempdir().expect("test can create a temp dir");
let ephemeral_root = tempfile::tempdir().expect("test can create an ephemeral root");
let store = build_durable_store_with(Some(home.path()), || {
open_ephemeral_rooted(ephemeral_root.path(), DEFAULT_SHARD_COUNT)
})
.expect("persistent store builds");
assert!(
home.path().join("durability").join("config.json").exists(),
"the persistent database is created under the configured path"
);
assert_eq!(
entry_count(ephemeral_root.path()),
0,
"the persistent branch creates no ephemeral guard directory"
);
drop(store);
}
#[test]
fn ephemeral_store_directory_is_owned_through_the_build_seam() {
let ephemeral_root = tempfile::tempdir().expect("test can create an ephemeral root");
let store = build_durable_store_with(None, || {
open_ephemeral_rooted(ephemeral_root.path(), DEFAULT_SHARD_COUNT)
})
.expect("ephemeral store builds");
assert_eq!(
entry_count(ephemeral_root.path()),
1,
"the ephemeral branch creates exactly one guard directory"
);
drop(store);
assert_eq!(
entry_count(ephemeral_root.path()),
0,
"dropping the last store handle removes the guard directory — zero residue"
);
}
}