#[path = "publication_identity.rs"]
mod publication_identity;
use liminal::channel::PublicationIdentity;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU8, AtomicU64, Ordering};
use std::sync::{Arc, Mutex, OnceLock, RwLock};
use std::time::Instant;
use haematite::{CommitPolicy, Database, DatabaseConfig, EventStore, Retention};
use liminal::channel::{
ChannelConfig, ChannelDelivery, ChannelHandle, ChannelMode, ChannelSupervisor, Schema,
};
use liminal::conversation::{ConversationSupervisor, ParticipantBehaviour};
use liminal::durability::bridge::block_on;
#[cfg(any(test, feature = "test-support"))]
use liminal::durability::open_ephemeral;
use liminal::durability::{
CountingStore, DedupCache, DedupDecision, DurableStore, EphemeralRoot, EphemeralSweepCounts,
HaematiteStore, ProcessingReceipt, StoreCountsHandle,
};
use liminal::protocol::{MessageEnvelope, ProtocolError, SchemaId as ProtocolSchemaId};
use std::num::NonZeroU64;
use super::channel_registry::{
ChannelAccessError, ChannelBuildError, ChannelConfigField, ChannelDescriptor, ChannelOrigin,
ChannelRegistration, ChannelRegistryError, ChannelState, ChannelStatus, MAX_CHANNELS_KEY,
Registered, STATE_ACTIVE, STATE_QUIESCED, UNRECORDED_QUIESCE_REASON,
};
use super::conversation::ConnectionConversation;
use super::services_cluster::build_channel_cluster;
use super::services_schema::{ChannelSchema, resolve_channel_schema, resolve_schema_bytes};
use super::worker_front_door::WorkerFrontDoorServices;
use crate::ServerError;
use crate::config::types::{ClusterConfig, ParticipantConfig, ServerConfig, ServiceProfile};
use crate::config::validation::required_persistence_path;
use crate::health::reissue::OperatorCredentialReissuer;
use crate::health::unloadable::UnloadableConversationRecord;
use crate::server::participant::{
InstalledParticipantService, ParticipantWarm, ProductionParticipantHandler,
};
use crate::server::self_streams::events::PublishOrigin;
use crate::server::self_streams::sink::OriginPublication;
use crate::server::signals::ServerSignals;
pub use super::services_cluster::ChannelCluster;
#[path = "../../agents/host_server.rs"]
mod agent_host;
#[path = "../../agents/server.rs"]
mod agent_services;
#[path = "../../agents/approval_boot.rs"]
mod approval_boot;
#[path = "../../services/site/server.rs"]
mod site_services;
pub use site_services::{
SiteDraftHolder, evaluate_subject as evaluate_site_subject, is_site_route, plan_subject,
};
pub(super) use site_services::{SiteFrame, site_frame_response};
#[path = "services_lifecycle.rs"]
mod lifecycle;
#[path = "services_conversation.rs"]
mod services_conversation;
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
}
fn subscriber_pid(&self) -> Option<u64> {
None
}
fn try_next_positioned(
&mut self,
) -> Result<Option<(liminal::envelope::Envelope, Option<u64>)>, ServerError> {
Ok(self.try_next().map(|envelope| (envelope, None)))
}
fn acknowledge_position(&self, position: u64) -> Result<(), ServerError> {
Err(ServerError::ListenerAccept {
message: format!("subscription cannot acknowledge durable position {position}"),
})
}
fn consumer_position(&self) -> Result<Option<u64>, ServerError> {
Ok(None)
}
}
#[derive(Debug)]
pub struct ConnectionSubscription {
id: u64,
channel: String,
stream_id: u32,
selected_schema: ProtocolSchemaId,
resource: Box<dyn SubscriptionResource>,
issued: super::state::IssuedDeliveries,
}
impl ConnectionSubscription {
#[must_use]
pub fn new(
id: u64,
selected_schema: ProtocolSchemaId,
resource: Box<dyn SubscriptionResource>,
) -> Self {
Self {
id,
channel: String::new(),
stream_id: 0,
selected_schema,
resource,
issued: super::state::IssuedDeliveries::new(0),
}
}
pub(super) fn set_channel(&mut self, channel: String) {
self.channel = channel;
}
#[must_use]
pub(super) fn channel(&self) -> &str {
&self.channel
}
#[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
}
pub(super) fn set_pressure_ledger_depth(&mut self, limit: usize) {
self.issued = super::state::IssuedDeliveries::new(limit);
}
pub(super) fn issued_delivery(&mut self, sequence: u64) {
self.issued.record(sequence);
}
pub(super) fn match_pressure(&mut self, reference: &str, retain: bool) -> bool {
self.issued.matches(reference, retain)
}
#[must_use]
pub const fn selected_schema(&self) -> ProtocolSchemaId {
self.selected_schema
}
#[must_use]
pub fn subscriber_pid(&self) -> Option<u64> {
self.resource.subscriber_pid()
}
pub(crate) fn try_next(&mut self) -> Option<liminal::envelope::Envelope> {
self.resource.try_next()
}
pub(crate) fn has_pending(&self) -> bool {
self.resource.has_pending()
}
pub(crate) fn is_overflowed(&self) -> bool {
self.resource.is_overflowed()
}
pub(super) fn unsubscribe(self) -> Result<(), ServerError> {
self.resource.unsubscribe()
}
pub(crate) fn try_next_positioned(
&mut self,
) -> Result<Option<(liminal::envelope::Envelope, Option<u64>)>, ServerError> {
self.resource.try_next_positioned()
}
pub(crate) fn acknowledge_position(&self, position: u64) -> Result<(), ServerError> {
self.resource.acknowledge_position(position)
}
pub(crate) fn consumer_position(&self) -> Result<Option<u64>, ServerError> {
self.resource.consumer_position()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PublishOutcome {
pub message_id: u64,
pub delivered: bool,
pub durable_position: Option<u64>,
pub reach: Reach,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Reach {
Delivered,
NoSubscriber,
NoneAccepted,
Suppressed,
}
impl Reach {
#[must_use]
pub const fn of(delivery: &ChannelDelivery) -> Self {
if delivery.is_delivered() {
Self::Delivered
} else if delivery.matched_count() == 0 {
Self::NoSubscriber
} else {
Self::NoneAccepted
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ChannelOperation {
Publish,
Subscribe,
}
pub trait ConnectionServices: std::fmt::Debug + Send + Sync {
fn construction_request(
&self,
_: Option<&dyn crate::auth_lys::session::LysSession>,
subject: &str,
_: &[u8],
_: u64,
) -> Option<Result<serde_json::Value, ServerError>> {
is_site_route(subject).then(|| {
Err(site_services::site_error(
crate::services::lifecycle::site::SiteError::Invalid("construction not installed"),
))
})
}
fn prepare_site_trial(
&self,
_: u64,
_: Option<&dyn crate::auth_lys::session::LysSession>,
_: &liminal::protocol::WorkerRegistration,
_: Option<&crate::services::lifecycle::ProcessGrant>,
_: u64,
) -> Result<Option<crate::services::lifecycle::trial::TrialPermit>, ServerError> {
Ok(None)
}
fn admit_site_call(
&self,
_: Option<&dyn crate::auth_lys::session::LysSession>,
_: &str,
_: u64,
) -> Result<bool, ServerError> {
Ok(false)
}
fn site_registration_closed(&self, _: u64) -> Result<(), ServerError> {
Ok(())
}
fn publish_identified(
&self,
connection: u64,
session: Option<&dyn crate::auth_lys::session::LysSession>,
channel: &str,
envelope: &MessageEnvelope,
key: Option<&str>,
) -> Result<PublishOutcome, ServerError> {
crate::auth_membership::check_agent_binding(
session,
crate::auth_lys::HotAction::Publish,
channel,
)?;
self.publish(connection, channel, envelope, key)
}
fn agents(&self) -> Option<&Arc<crate::agents::runtime::Agents>> {
None
}
fn subscribe_identified(
&self,
connection: u64,
session: Option<&dyn crate::auth_lys::session::LysSession>,
channel: &str,
schemas: &[ProtocolSchemaId],
install: Option<liminal::channel::InboxInstall>,
) -> Result<ConnectionSubscription, ServerError> {
if connection == 0 {
return Err(crate::agents::error::AgentError::Invalid("connection id").into());
}
crate::auth_membership::check_agent_binding(
session,
crate::auth_lys::HotAction::Subscribe,
channel,
)?;
self.subscribe(channel, schemas, install)
}
fn participant_service(&self) -> Option<InstalledParticipantService> {
None
}
fn publish(
&self,
connection: u64,
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,
op_id: Option<u64>,
) -> Result<(), ServerError>;
fn close_conversation(&self, conversation: ConnectionConversation) -> Result<(), ServerError>;
fn flush_durable_state(&self) -> Result<(), ServerError>;
fn stop_participant_warm(&self) {}
fn finish_background_store_work(&self) -> Result<(), ServerError> {
Ok(())
}
fn release_store_holders(&self) -> Result<(), ServerError> {
Ok(())
}
fn listening(self: Arc<Self>) -> Option<std::thread::JoinHandle<()>> {
None
}
fn supports_channel_operations(&self) -> bool {
true
}
fn admit_channel(
&self,
_operation: ChannelOperation,
_channel: &str,
) -> Result<(), ChannelAccessError> {
Ok(())
}
}
#[derive(Debug)]
pub struct LiminalConnectionServices {
publication_identity: Arc<publication_identity::ServerPublicationIdentity>,
agents: Option<Arc<crate::agents::runtime::Agents>>,
approval_agents: Option<Arc<crate::agents::waiting::durable::ApprovalAgents>>,
agent_delivery: Option<Arc<crate::agents::waiting::AgentDelivery>>,
site_lifecycle: OnceLock<Arc<crate::services::lifecycle::Lifecycle>>,
site_drafts: OnceLock<Arc<dyn SiteDraftHolder>>,
host_config: OnceLock<(crate::config::types::LysConfig, tokio::runtime::Handle)>,
draft_hook: OnceLock<std::sync::Weak<dyn crate::auth_lys::drafts::DraftDecisionHook>>,
channels: RwLock<HashMap<String, Arc<ConfiguredChannel>>>,
export_subscriptions:
Mutex<Vec<std::sync::Weak<crate::link_bridge::subscriptions::ExportSubscriptions>>>,
max_channels: Option<usize>,
cluster: ChannelCluster,
durable_store: Arc<dyn DurableStore>,
head_checkpoint_stop: Arc<liminal::durability::head_checkpoint_stop::HeadCheckpointStop>,
store_counts: StoreCountsHandle,
ephemeral_sweep: Option<EphemeralSweepCounts>,
signals: ServerSignals,
participant_service: Option<InstalledParticipantService>,
unloadable_conversations: Option<UnloadableConversationRecord>,
credential_reissuer: Option<Arc<dyn OperatorCredentialReissuer>>,
participant_warm: Option<ParticipantWarm>,
dedup: DedupCache,
dedup_ephemeral: DedupCache,
held_keys: unresolved::HeldKeys,
claims_start: Mutex<Option<StoreHistory>>,
conversation_supervisor: Arc<ConversationSupervisor>,
responders: Mutex<ResponderRegistry>,
next_subscription_id: AtomicU64,
console: Arc<crate::console::ConsoleHub>,
}
impl LiminalConnectionServices {
pub fn from_config(config: &ServerConfig) -> Result<Self, ServerError> {
Self::from_config_with_runtime(config, None)
}
pub(in crate::server) fn from_config_with_runtime(
config: &ServerConfig,
runtime: Option<tokio::runtime::Handle>,
) -> Result<Self, ServerError> {
require_full_profile(config)?;
let path = required_persistence_path(config)?;
let history = StoreHistory::of(path);
let (subsystems, store) = crate::server::boot_steps::timed("store_open", || {
let subsystems = crate::server::boot_steps::timed("ephemeral_root_open", || {
RootedProductionSubsystems::new(path)
})?;
let store = subsystems.durable_store(path)?;
Ok::<_, ServerError>((subsystems, store))
})?;
crate::server::boot_steps::timed("channel_construction", || {
Self::from_config_with_store_and_runtime(config, store, history, &subsystems, runtime)
})
}
pub fn from_config_with_store(
config: &ServerConfig,
durable_store: Arc<dyn DurableStore>,
) -> Result<Self, ServerError> {
require_full_profile(config)?;
#[cfg(any(test, feature = "test-support"))]
if config.persistence_path.is_none() {
return Self::from_config_with_store_via(
config,
durable_store,
StoreHistory::Recorded,
&ProductionSubsystems,
);
}
let subsystems = RootedProductionSubsystems::new(required_persistence_path(config)?)?;
Self::from_config_with_store_via(config, durable_store, StoreHistory::Recorded, &subsystems)
}
fn from_config_with_store_via(
config: &ServerConfig,
durable_store: Arc<dyn DurableStore>,
history: StoreHistory,
subsystems: &dyn SubsystemFactory,
) -> Result<Self, ServerError> {
Self::from_config_with_store_and_runtime(config, durable_store, history, subsystems, None)
}
fn from_config_with_store_and_runtime(
config: &ServerConfig,
durable_store: Arc<dyn DurableStore>,
history: StoreHistory,
subsystems: &dyn SubsystemFactory,
runtime: Option<tokio::runtime::Handle>,
) -> Result<Self, ServerError> {
let head_checkpoint_stop = Arc::new(
liminal::durability::head_checkpoint_stop::HeadCheckpointStop::new(durable_store),
);
let durable_store: Arc<dyn DurableStore> = head_checkpoint_stop.clone();
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 configured = build_configured_channel(
&channel.name,
resolved,
channel.durable,
ChannelOrigin::BootConfigured,
&durable_store,
cluster.supervisor(),
)
.map_err(|error| ServerError::ConfigValidation {
message: error.boot_message(&channel.name),
})?;
channels.insert(channel.name.clone(), Arc::new(configured));
}
let conversation_supervisor = subsystems.conversation_supervisor()?;
let dedup = history.delivery_dedup(Arc::clone(&durable_store));
let ephemeral_store = ephemeral_dedup_store(&durable_store, history, subsystems);
let store_counts = combined_store_counts(&durable_store, &ephemeral_store);
let dedup_ephemeral = DedupCache::new_empty(ephemeral_store, ephemeral_dedup_namespace());
let signals = ServerSignals::default();
let installed_participant = config
.participant
.as_ref()
.map(|participant| install_participant(&durable_store, participant, &signals))
.transpose()?;
let device = config
.cluster
.as_ref()
.map_or("local", |cluster| cluster.node_name.as_str());
let console = Arc::new(console_hub(
device,
&channels,
config.persistence_path.is_some(),
config.auth.as_ref().and_then(|auth| auth.lys.as_ref()),
)?);
signals.install_console(Arc::clone(&console))?;
cluster.observer().set_console(Arc::clone(&console))?;
let (participant_service, unloadable_conversations, credential_reissuer, participant_warm) =
installed_participant.map_or((None, None, None, None), |installed| {
(
Some(installed.service),
Some(installed.unloadable_conversations),
Some(installed.credential_reissuer),
Some(installed.warm),
)
});
let (agents, approval_agents, agent_delivery) = Self::configured_agents(
config,
Arc::clone(&durable_store),
cluster.supervisor().clone(),
&console,
runtime,
)?;
let services = Self {
channels: RwLock::new(channels),
export_subscriptions: Mutex::new(Vec::new()),
max_channels: config.limits.max_channels,
cluster,
durable_store,
head_checkpoint_stop,
store_counts,
ephemeral_sweep: subsystems.ephemeral_sweep_counts(),
signals,
participant_service,
unloadable_conversations,
credential_reissuer,
participant_warm,
dedup,
dedup_ephemeral,
held_keys: unresolved::HeldKeys::default(),
claims_start: Mutex::new(Some(history)),
conversation_supervisor,
responders: Mutex::new(HashMap::new()),
publication_identity: publication_identity::ServerPublicationIdentity::new()?,
next_subscription_id: AtomicU64::new(1),
console,
agents,
approval_agents,
agent_delivery,
site_lifecycle: OnceLock::new(),
site_drafts: OnceLock::new(),
host_config: OnceLock::new(),
draft_hook: OnceLock::new(),
};
services.install_roster_identity()?;
Ok(services)
}
#[cfg(any(test, feature = "test-support"))]
pub fn empty() -> Result<Self, ServerError> {
let conversation_supervisor = ProductionSubsystems.conversation_supervisor()?;
let store =
open_ephemeral(DEFAULT_SHARD_COUNT).map_err(|error| ServerError::ConfigValidation {
message: format!("failed to open test durable store: {error}"),
})?;
let head_checkpoint_stop = Arc::new(
liminal::durability::head_checkpoint_stop::HeadCheckpointStop::new(Arc::new(
CountingStore::new(store),
)),
);
let durable_store: Arc<dyn DurableStore> = head_checkpoint_stop.clone();
let store_counts = combined_store_counts(&durable_store, &durable_store);
let dedup = StoreHistory::Fresh.delivery_dedup(Arc::clone(&durable_store));
let dedup_ephemeral =
DedupCache::new_empty(Arc::clone(&durable_store), ephemeral_dedup_namespace());
let signals = ServerSignals::default();
let console = Arc::new(console_hub("local", &HashMap::new(), false, None)?);
signals.install_console(Arc::clone(&console))?;
let cluster = build_channel_cluster(None)?;
cluster.observer().set_console(Arc::clone(&console))?;
Ok(Self {
channels: RwLock::new(HashMap::new()),
export_subscriptions: Mutex::new(Vec::new()),
max_channels: None,
cluster,
durable_store,
head_checkpoint_stop,
store_counts,
ephemeral_sweep: None,
signals,
participant_service: None,
unloadable_conversations: None,
credential_reissuer: None,
participant_warm: None,
dedup,
dedup_ephemeral,
held_keys: unresolved::HeldKeys::default(),
claims_start: Mutex::new(None),
conversation_supervisor,
responders: Mutex::new(HashMap::new()),
publication_identity: publication_identity::ServerPublicationIdentity::new()?,
next_subscription_id: AtomicU64::new(1),
console,
agents: None,
approval_agents: None,
agent_delivery: None,
site_lifecycle: OnceLock::new(),
site_drafts: OnceLock::new(),
host_config: OnceLock::new(),
draft_hook: OnceLock::new(),
})
}
#[must_use]
pub const fn ephemeral_sweep_counts(&self) -> Option<EphemeralSweepCounts> {
self.ephemeral_sweep
}
#[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 console(&self) -> &crate::console::ConsoleHub {
&self.console
}
fn console_changed(&self, change: crate::console::TopologyChange) {
self.signals.topology(change);
}
pub fn responder_subjects(&self) -> Result<Vec<String>, ServerError> {
let responders = self.lock_responders()?;
let mut subjects: Vec<String> = responders.keys().cloned().collect();
drop(responders);
subjects.sort();
Ok(subjects)
}
pub fn finish_participant_warm(&self) -> Result<usize, ServerError> {
self.participant_warm.as_ref().map_or(Ok(0), |warm| {
warm.finish()
.map_err(|error| ServerError::ParticipantStartupRestore {
message: error.to_string(),
})
})
}
#[must_use]
pub fn store_counts(&self) -> StoreCountsHandle {
self.store_counts.clone()
}
#[must_use]
pub fn unloadable_conversation_record(&self) -> Option<UnloadableConversationRecord> {
self.unloadable_conversations.clone()
}
#[must_use]
pub fn credential_reissuer(&self) -> Option<Arc<dyn OperatorCredentialReissuer>> {
self.credential_reissuer.clone()
}
#[cfg(test)]
#[must_use]
pub(crate) fn with_participant_service(
mut self,
participant_service: InstalledParticipantService,
) -> Self {
self.participant_service = Some(participant_service);
self
}
#[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 subject = subject.into();
let mut responders = self.lock_responders()?;
let previous = responders.insert(subject.clone(), behaviour);
drop(responders);
self.console_changed(crate::console::TopologyChange::Service {
subject,
present: true,
});
Ok(previous)
}
pub fn unregister_responder(
&self,
subject: &str,
) -> Result<Option<Arc<dyn ParticipantBehaviour>>, ServerError> {
let mut responders = self.lock_responders()?;
let removed = responders.remove(subject);
drop(responders);
if removed.is_some() {
self.console_changed(crate::console::TopologyChange::Service {
subject: subject.to_owned(),
present: false,
});
}
Ok(removed)
}
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(),
})
}
fn read_channels(
&self,
) -> Result<std::sync::RwLockReadGuard<'_, HashMap<String, Arc<ConfiguredChannel>>>, ServerError>
{
self.channels
.read()
.map_err(|_poisoned| ServerError::ListenerAccept {
message: "channel roster lock poisoned".to_owned(),
})
}
#[cfg(test)]
pub(in crate::server) fn channel_handle_for_test(
&self,
channel: &str,
) -> Result<ChannelHandle, ServerError> {
self.read_channels()?
.get(channel)
.map(|configured| configured.handle.clone())
.ok_or_else(|| ServerError::ListenerAccept {
message: format!("channel '{channel}' is not configured"),
})
}
#[cfg(test)]
pub(crate) fn subscribe_handle_for_test(
&self,
channel: &str,
) -> Result<liminal::channel::SubscriptionHandle, ServerError> {
self.subscribe_handle_for_test_with_install(channel, None)
}
#[cfg(test)]
pub(crate) fn subscribe_handle_for_test_with_install(
&self,
channel: &str,
install: Option<liminal::channel::InboxInstall>,
) -> Result<liminal::channel::SubscriptionHandle, ServerError> {
let channels = self.read_channels()?;
let configured =
channels
.get(channel)
.map(Arc::clone)
.ok_or_else(|| ServerError::ListenerAccept {
message: format!("channel '{channel}' is not configured"),
})?;
drop(channels);
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}"),
})
}
fn claim_delivery(cache: &DedupCache, key: &str) -> Result<bool, ServerError> {
let decision = block_on(cache.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))
}
const fn dedup_for(&self, channel: &ConfiguredChannel) -> &DedupCache {
match channel.handle.config().mode {
ChannelMode::Durable => &self.dedup,
ChannelMode::Ephemeral => &self.dedup_ephemeral,
}
}
fn release_claim(cache: &DedupCache, key: &str) {
match block_on(cache.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 next_message_id(&self) -> Result<u64, ServerError> {
self.publication_identity
.next()
.map(|message| message.id)
.map_err(|error| ServerError::ListenerAccept {
message: error.to_string(),
})
}
fn delivered(delivery: &ChannelDelivery) -> Result<PublishOutcome, ServerError> {
Self::record_delivery_counts(delivery);
let message = delivery
.delivery_id()
.ok_or_else(|| ServerError::ListenerAccept {
message: "publication identity missing after admission".into(),
})?;
Ok(PublishOutcome {
message_id: message.id,
delivered: delivery.is_delivered(),
durable_position: delivery.durable_position(),
reach: Reach::of(delivery),
})
}
}
#[path = "deferred_store.rs"]
mod deferred_store;
#[path = "unresolved.rs"]
mod unresolved;
#[path = "unresolved_start.rs"]
mod unresolved_start;
#[cfg(test)]
#[path = "unresolved_start_tests.rs"]
mod unresolved_start_tests;
use deferred_store::{DeferredStore, ThrowawayStoreOpener};
use unresolved_start::StoreHistory;
impl LiminalConnectionServices {
pub fn register_channel(
&self,
spec: &ChannelRegistration,
) -> Result<Registered, ChannelRegistryError> {
let Some(limit) = self.max_channels else {
return Err(ChannelRegistryError::CapNotConfigured {
cap: MAX_CHANNELS_KEY,
});
};
let resolved = resolve_schema_bytes(spec.schema_bytes.as_deref()).map_err(|error| {
ChannelRegistryError::SchemaRejected {
name: spec.name.clone(),
message: error.to_string(),
}
})?;
let (existing, registered_count) = {
let channels = self.read_roster()?;
let existing = channels.get(&spec.name).map(Arc::clone);
let registered_count = runtime_registered_count(&channels);
drop(channels);
(existing, registered_count)
};
if let Some(existing) = existing {
return compare_registration(&existing, spec, &resolved);
}
if registered_count >= limit {
return Err(ChannelRegistryError::CapReached {
cap: MAX_CHANNELS_KEY,
limit,
});
}
let configured = build_configured_channel(
&spec.name,
resolved,
spec.durable,
ChannelOrigin::RuntimeRegistered,
&self.durable_store,
self.cluster.supervisor(),
)
.map_err(|error| error.into_registry_error(&spec.name))?;
let mut exporters = self.export_subscriptions.lock().map_err(|_| {
ChannelRegistryError::RosterUnavailable {
message: "export subscription registry lock poisoned".to_owned(),
}
})?;
exporters.retain(|exporter| exporter.strong_count() > 0);
let mut channels = self.write_roster()?;
let raced = channels.get(&spec.name).map(Arc::clone);
let registered_count = runtime_registered_count(&channels);
if raced.is_none() && registered_count < limit {
let mut prepared = Vec::new();
for exporter in exporters.iter().filter_map(std::sync::Weak::upgrade) {
if let Some(binding) = exporter.binding(&spec.name) {
let refused = |error: ServerError| ChannelRegistryError::RosterUnavailable {
message: format!(
"export '{}' cannot subscribe to '{}': {error}",
exporter.link(),
spec.name
),
};
let subscription = self
.build_subscription(
&spec.name,
&configured.handle,
configured.protocol_schema,
Some(binding.install),
Some(&binding.consumer),
)
.map_err(refused)?;
let subscription =
crate::link_bridge::subscriptions::PreparedSubscription::new(
binding.prefix,
spec.name.clone(),
subscription,
)
.map_err(refused)?;
prepared.push((exporter, subscription));
}
}
for (exporter, subscription) in prepared {
exporter.attach(subscription);
}
self.identify_registered(&configured)?;
channels.insert(spec.name.clone(), Arc::new(configured));
drop(channels);
drop(exporters);
self.console_changed(crate::console::TopologyChange::Channel {
name: spec.name.clone(),
life: line_life(spec.durable),
state: crate::console::records::LineState::Live,
});
return Ok(Registered::Created);
}
drop(channels);
drop(exporters);
if let Some(raced) = raced {
return compare_registration(&raced, spec, &schema_of(&configured));
}
Err(ChannelRegistryError::CapReached {
cap: MAX_CHANNELS_KEY,
limit,
})
}
pub fn quiesce_channel(
&self,
name: &str,
reason: impl Into<String>,
) -> Result<(), ChannelRegistryError> {
let reason = reason.into();
let configured = {
let channels = self.read_roster()?;
channels.get(name).map(Arc::clone).ok_or_else(|| {
ChannelRegistryError::NotRegistered {
name: name.to_owned(),
}
})?
};
configured.quiesce(name, &reason)?;
self.console_changed(crate::console::TopologyChange::Channel {
name: name.to_owned(),
life: line_life(configured.durable),
state: crate::console::records::LineState::Quiesced,
});
Ok(())
}
pub fn channel_status(&self, name: &str) -> Result<ChannelStatus, ChannelRegistryError> {
let configured = {
let channels = self.read_roster()?;
channels.get(name).map(Arc::clone)
};
Ok(
configured.map_or(ChannelStatus::NotRegistered, |configured| {
status_of(&configured)
}),
)
}
pub fn channel_census(&self) -> Result<Vec<(String, ChannelStatus)>, ChannelRegistryError> {
let mut census: Vec<(String, ChannelStatus)> = self
.read_roster()?
.iter()
.map(|(name, configured)| (name.clone(), status_of(configured)))
.collect();
census.sort_by(|left, right| left.0.cmp(&right.0));
Ok(census)
}
pub fn registered_channels(&self) -> Result<Vec<ChannelDescriptor>, ChannelRegistryError> {
let entries: Vec<(String, Arc<ConfiguredChannel>)> = {
let channels = self.read_roster()?;
channels
.iter()
.map(|(name, configured)| (name.clone(), Arc::clone(configured)))
.collect()
};
let mut descriptors: Vec<ChannelDescriptor> = entries
.into_iter()
.map(|(name, configured)| ChannelDescriptor {
name,
origin: configured.origin,
state: configured.state(),
})
.collect();
descriptors.sort_by(|left, right| left.name.cmp(&right.name));
Ok(descriptors)
}
fn admit_channel(&self, channel: &str) -> Result<Arc<ConfiguredChannel>, ChannelAccessError> {
let configured = {
let channels = self.channels.read().map_err(|_poisoned| {
ChannelAccessError::RosterUnavailable {
message: ROSTER_POISONED.to_owned(),
}
})?;
channels.get(channel).map(Arc::clone)
};
let configured = configured.ok_or_else(|| ChannelAccessError::NotRegistered {
name: channel.to_owned(),
})?;
if configured.state.load(Ordering::Acquire) == STATE_QUIESCED {
return Err(ChannelAccessError::Quiesced {
name: channel.to_owned(),
reason: configured.recorded_quiesce_reason(),
});
}
Ok(configured)
}
fn read_roster(
&self,
) -> Result<
std::sync::RwLockReadGuard<'_, HashMap<String, Arc<ConfiguredChannel>>>,
ChannelRegistryError,
> {
#[cfg(test)]
ROSTER_READS.with(|reads| reads.set(reads.get() + 1));
self.channels
.read()
.map_err(|_poisoned| ChannelRegistryError::RosterUnavailable {
message: ROSTER_POISONED.to_owned(),
})
}
fn write_roster(
&self,
) -> Result<
std::sync::RwLockWriteGuard<'_, HashMap<String, Arc<ConfiguredChannel>>>,
ChannelRegistryError,
> {
self.channels
.write()
.map_err(|_poisoned| ChannelRegistryError::RosterUnavailable {
message: ROSTER_POISONED.to_owned(),
})
}
fn give_back_tap(&self, channel: &str) {
self.console.release_tap(channel, |subject| {
crate::console::ServerPublisher::retire_console_channel(self, subject)
});
}
fn open_subscription(
&self,
channel: &str,
accepted_schemas: &[ProtocolSchemaId],
install: Option<liminal::channel::InboxInstall>,
consumer: Option<&str>,
) -> Result<ConnectionSubscription, ServerError> {
refuse_console_tap_target(channel).map_err(|error| access_to_server_error(&error))?;
let configured = self
.admit_channel(channel)
.map_err(|error| access_to_server_error(&error))?;
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))?
};
self.build_subscription(
channel,
&configured.handle,
selected_schema,
install,
consumer,
)
}
fn build_subscription(
&self,
channel: &str,
handle: &ChannelHandle,
selected_schema: ProtocolSchemaId,
install: Option<liminal::channel::InboxInstall>,
consumer: Option<&str>,
) -> Result<ConnectionSubscription, ServerError> {
let subscription = match (consumer, install) {
(Some(consumer), Some(install)) => handle.subscribe_consumer(consumer, install),
(Some(_), None) => {
return Err(ServerError::ListenerAccept {
message: "a durable consumer requires a bounded inbox install".to_owned(),
});
}
(None, Some(install)) => handle.subscribe_with_install(install),
(None, None) => handle.subscribe(),
}
.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);
let mut connection_subscription = ConnectionSubscription::new(
id,
selected_schema,
Box::new(LiminalSubscriptionResource { subscription }),
);
connection_subscription.set_channel(channel.to_owned());
Ok(connection_subscription)
}
pub(crate) fn subscribe_consumer(
&self,
channel: &str,
consumer: &str,
install: liminal::channel::InboxInstall,
) -> Result<ConnectionSubscription, ServerError> {
self.open_subscription(channel, &[], Some(install), Some(consumer))
}
pub(crate) fn install_exporter(
&self,
exporter: &Arc<crate::link_bridge::subscriptions::ExportSubscriptions>,
) -> Result<(), ServerError> {
let mut exporters =
self.export_subscriptions
.lock()
.map_err(|_| ServerError::ListenerAccept {
message: "export subscription registry lock poisoned".to_owned(),
})?;
exporters.retain(|exporter| exporter.strong_count() > 0);
if exporters
.iter()
.filter_map(std::sync::Weak::upgrade)
.any(|existing| existing.link() == exporter.link())
{
return Err(ServerError::ListenerAccept {
message: format!(
"link '{}' already owns an export subscription",
exporter.link()
),
});
}
let mut prepared = Vec::new();
for channel in self
.registered_channels()
.map_err(|error| ServerError::ListenerAccept {
message: error.to_string(),
})?
{
if matches!(channel.state, ChannelState::Active) {
if let Some(binding) = exporter.binding(&channel.name) {
let subscription =
self.subscribe_consumer(&channel.name, &binding.consumer, binding.install)?;
prepared.push(
crate::link_bridge::subscriptions::PreparedSubscription::new(
binding.prefix,
channel.name,
subscription,
)?,
);
}
}
}
for subscription in prepared {
exporter.attach(subscription);
}
exporters.push(Arc::downgrade(exporter));
drop(exporters);
Ok(())
}
fn refuse_console_tap_limit(&self, channel: &str) -> Result<(), ChannelAccessError> {
match self.console.reserve_tap(channel) {
Ok(false) => Err(ChannelAccessError::ConsoleTapLimit {
name: channel.to_owned(),
limit: crate::console::MAX_LIVE_TAPS,
}),
Ok(true) | Err(_) => Ok(()),
}
}
}
impl crate::console::ServerPublisher for LiminalConnectionServices {
fn publish_console(
&self,
subject: &str,
payload: &[u8],
) -> Result<(), crate::console::ConsoleError> {
let payload = self.agent_topology_payload(subject, payload)?;
let payload = if subject == crate::console::TOPOLOGY_SUBJECT {
if let Some(lifecycle) = self.site_lifecycle.get() {
std::borrow::Cow::Owned(
crate::console::topology::with_sites(
&payload,
lifecycle,
self.console.device(),
)
.map_err(|error| crate::console::ConsoleError::Server {
subject: subject.into(),
source: site_services::site_error(error),
})?,
)
} else {
payload
}
} else {
payload
};
let envelope = MessageEnvelope::new(
ProtocolSchemaId::new([0_u8; ProtocolSchemaId::WIRE_LEN]),
liminal::protocol::CausalContext::independent(),
payload.to_vec(),
);
self.publish_with_origin(
subject,
&envelope,
None,
PublishOrigin::Server {
device: self.console.device(),
},
)
.map(|_published| ())
.map_err(|source| crate::console::ConsoleError::Server {
subject: subject.to_owned(),
source,
})
}
fn retire_console_channel(&self, subject: &str) -> Result<(), crate::console::ConsoleError> {
if !super::channel_registry::is_console_subject(subject) {
return Ok(());
}
let mut channels =
self.write_roster()
.map_err(|error| crate::console::ConsoleError::Server {
subject: subject.to_owned(),
source: ServerError::ListenerAccept {
message: error.to_string(),
},
})?;
channels.remove(subject);
drop(channels);
Ok(())
}
}
struct InstalledParticipant {
service: InstalledParticipantService,
unloadable_conversations: UnloadableConversationRecord,
credential_reissuer: Arc<dyn OperatorCredentialReissuer>,
warm: ParticipantWarm,
}
fn install_participant(
durable_store: &Arc<dyn DurableStore>,
participant: &ParticipantConfig,
signals: &ServerSignals,
) -> Result<InstalledParticipant, ServerError> {
let handler = ProductionParticipantHandler::new(Arc::clone(durable_store), *participant)
.map_err(|error| ServerError::ParticipantStartupRestore {
message: error.to_string(),
})?;
handler.install_signals(signals.clone()).map_err(|error| {
ServerError::ParticipantStartupRestore {
message: error.to_string(),
}
})?;
let unloadable_conversations = handler.unloadable_record();
let handler = Arc::new(handler);
let warm = handler
.install_warm()
.map_err(|error| ServerError::ParticipantStartupRestore {
message: error.to_string(),
})?;
let credential_reissuer: Arc<dyn OperatorCredentialReissuer> =
Arc::<ProductionParticipantHandler>::clone(&handler);
let service = InstalledParticipantService::new(
handler,
Arc::clone(durable_store),
participant.wire_frame_limit,
)
.map_err(|error| ServerError::ConfigValidation {
message: format!(
"participant.wire_frame_limit: {} is below the protocol's minimum \
complete participant frame ({error:?})",
participant.wire_frame_limit
),
})?;
Ok(InstalledParticipant {
service,
unloadable_conversations,
credential_reissuer,
warm,
})
}
fn console_hub(
device: &str,
channels: &HashMap<String, Arc<ConfiguredChannel>>,
depot: bool,
lys: Option<&crate::config::types::LysConfig>,
) -> Result<crate::console::ConsoleHub, ServerError> {
let this = crate::console::ThisDevice {
id: device.to_owned(),
liminal: env!("CARGO_PKG_VERSION").to_owned(),
started: Instant::now(),
depot,
};
let lines = channels.iter().map(|(name, configured)| {
let state = match configured.state() {
ChannelState::Active => crate::console::records::LineState::Live,
ChannelState::Quiesced { .. } => crate::console::records::LineState::Quiesced,
};
(name.clone(), line_life(configured.durable), state)
});
let mut model = crate::console::TopologyModel::new(this, [], lines, [], []);
if let Some(machine) = lys.and_then(|lys| lys.machine.as_ref()) {
model = model.with_machine(machine);
}
crate::console::wall::WallBase::now()
.and_then(|wall| crate::console::ConsoleHub::new(device, model, wall))
.map_err(|error| ServerError::ListenerAccept {
message: format!("console hub: {error}"),
})
}
fn status_of(configured: &ConfiguredChannel) -> ChannelStatus {
let mode = configured.handle.config().mode;
match configured.state() {
ChannelState::Active => ChannelStatus::Active {
origin: configured.origin,
mode,
schema: configured.protocol_schema,
},
ChannelState::Quiesced { reason } => ChannelStatus::Quiesced {
reason,
origin: configured.origin,
mode,
},
}
}
#[cfg(test)]
thread_local! {
static ROSTER_READS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
}
#[cfg(test)]
pub(crate) fn roster_reads() -> u64 {
ROSTER_READS.with(std::cell::Cell::get)
}
const fn line_life(durable: bool) -> crate::console::records::LineLife {
if durable {
crate::console::records::LineLife::Kept
} else {
crate::console::records::LineLife::Passing
}
}
const ROSTER_POISONED: &str = "channel roster lock poisoned";
fn runtime_registered_count(channels: &HashMap<String, Arc<ConfiguredChannel>>) -> usize {
channels
.values()
.filter(|configured| configured.origin == ChannelOrigin::RuntimeRegistered)
.count()
}
fn compare_registration(
existing: &ConfiguredChannel,
spec: &ChannelRegistration,
resolved: &ChannelSchema,
) -> Result<Registered, ChannelRegistryError> {
let requested_mode = if spec.durable {
ChannelMode::Durable
} else {
ChannelMode::Ephemeral
};
let config = existing.handle.config();
let mismatch = if config.mode == requested_mode {
if existing.protocol_schema == resolved.protocol_id {
if *config.schema.definition() == resolved.document {
None
} else {
Some(ChannelConfigField::SchemaDocument)
}
} else {
Some(ChannelConfigField::SchemaId)
}
} else {
Some(ChannelConfigField::Mode)
};
mismatch.map_or(Ok(Registered::AlreadyIdentical), |field| {
Err(ChannelRegistryError::AlreadyRegistered {
name: spec.name.clone(),
field,
})
})
}
fn schema_of(configured: &ConfiguredChannel) -> ChannelSchema {
ChannelSchema {
document: configured.handle.config().schema.definition().clone(),
protocol_id: configured.protocol_schema,
}
}
fn build_configured_channel(
name: &str,
resolved: ChannelSchema,
durable: bool,
origin: ChannelOrigin,
durable_store: &Arc<dyn DurableStore>,
supervisor: &ChannelSupervisor,
) -> Result<ConfiguredChannel, ChannelBuildError> {
let schema =
Schema::new(resolved.document).map_err(|error| ChannelBuildError::SchemaRejected {
message: error.to_string(),
})?;
let channel_config = if durable {
ChannelConfig::new(name.to_owned(), schema, ChannelMode::Durable)
} else {
ChannelConfig::new(name.to_owned(), schema, ChannelMode::Ephemeral)
};
let handle = if durable {
ChannelHandle::new_durable_with_supervisor(
channel_config,
Arc::clone(durable_store),
supervisor.clone(),
)
.map_err(|error| ChannelBuildError::DurableInitFailed {
message: error.to_string(),
})?
} else {
ChannelHandle::with_supervisor(channel_config, supervisor.clone())
};
Ok(ConfiguredChannel {
handle,
protocol_schema: resolved.protocol_id,
durable,
origin,
state: AtomicU8::new(STATE_ACTIVE),
quiesce_reason: OnceLock::new(),
})
}
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 THROWAWAY_SHARD_COUNT: usize = 1;
const DELIVERY_DEDUP_NAMESPACE: &str = "liminal:delivery-dedup";
fn ephemeral_dedup_namespace() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let boot_nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |since| since.as_nanos());
format!(
"{DELIVERY_DEDUP_NAMESPACE}:incarnation:{}-{boot_nanos}",
std::process::id()
)
}
fn combined_store_counts(
durable_store: &Arc<dyn DurableStore>,
ephemeral_store: &Arc<dyn DurableStore>,
) -> StoreCountsHandle {
let durable = durable_store.store_counts().unwrap_or_default();
if Arc::ptr_eq(durable_store, ephemeral_store) {
return durable;
}
durable.with(&ephemeral_store.store_counts().unwrap_or_default())
}
fn ephemeral_dedup_store(
durable_store: &Arc<dyn DurableStore>,
history: StoreHistory,
subsystems: &dyn SubsystemFactory,
) -> Arc<dyn DurableStore> {
match (history, durable_store) {
#[cfg(any(test, feature = "test-support"))]
(StoreHistory::Fresh, store) => Arc::clone(store),
(StoreHistory::Created | StoreHistory::Recorded, _) => Arc::new(CountingStore::new(
DeferredStore::new(subsystems.throwaway_store()),
)),
}
}
#[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: &Path) -> Result<Arc<dyn DurableStore>, ServerError>;
fn throwaway_store(&self) -> ThrowawayStoreOpener;
fn ephemeral_sweep_counts(&self) -> Option<EphemeralSweepCounts> {
None
}
}
#[cfg(any(test, feature = "test-support"))]
pub(super) struct ProductionSubsystems;
#[cfg(any(test, feature = "test-support"))]
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: &Path) -> Result<Arc<dyn DurableStore>, ServerError> {
build_durable_store(persistence_path)
}
fn throwaway_store(&self) -> ThrowawayStoreOpener {
Box::new(|| open_ephemeral(THROWAWAY_SHARD_COUNT))
}
}
struct RootedProductionSubsystems {
root: Arc<EphemeralRoot>,
}
impl RootedProductionSubsystems {
fn new(path: &Path) -> Result<Self, ServerError> {
let root = EphemeralRoot::open(path).map_err(|error| ServerError::ConfigValidation {
message: error.to_string(),
})?;
Ok(Self { root })
}
}
impl SubsystemFactory for RootedProductionSubsystems {
fn channel_cluster(
&self,
config: Option<&ClusterConfig>,
) -> Result<ChannelCluster, ServerError> {
build_channel_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, path: &Path) -> Result<Arc<dyn DurableStore>, ServerError> {
build_durable_store(path)
}
fn throwaway_store(&self) -> ThrowawayStoreOpener {
let root = Arc::clone(&self.root);
Box::new(move || root.open_store(THROWAWAY_SHARD_COUNT))
}
fn ephemeral_sweep_counts(&self) -> Option<EphemeralSweepCounts> {
Some(self.root.sweep_counts())
}
}
#[cfg(test)]
pub(super) mod subsystem_census {
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use liminal::conversation::ConversationSupervisor;
use liminal::durability::{DurableStore, open_ephemeral_rooted};
use super::{
ChannelCluster, ProductionSubsystems, SchedulerSubsystem, SubsystemFactory,
THROWAWAY_SHARD_COUNT, ThrowawayStoreOpener, build_durable_store,
};
use crate::ServerError;
use crate::config::types::ClusterConfig;
pub struct RecordingSubsystems {
census: [AtomicUsize; 3],
ephemeral_root: PathBuf,
}
impl RecordingSubsystems {
pub fn rooted(ephemeral_root: &Path) -> Self {
Self {
census: [
AtomicUsize::new(0),
AtomicUsize::new(0),
AtomicUsize::new(0),
],
ephemeral_root: ephemeral_root.to_path_buf(),
}
}
pub fn recorded(&self) -> Vec<SchedulerSubsystem> {
KINDS
.iter()
.zip(&self.census)
.flat_map(|(kind, count)| std::iter::repeat_n(*kind, count.load(Ordering::SeqCst)))
.collect()
}
fn record(&self, subsystem: SchedulerSubsystem) {
if let Some(count) = KINDS
.iter()
.position(|kind| *kind == subsystem)
.and_then(|index| self.census.get(index))
{
count.fetch_add(1, Ordering::SeqCst);
}
}
}
const KINDS: [SchedulerSubsystem; 3] = [
SchedulerSubsystem::ChannelSupervisor,
SchedulerSubsystem::ConversationSupervisor,
SchedulerSubsystem::HaematiteStore,
];
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: &Path,
) -> Result<Arc<dyn DurableStore>, ServerError> {
let store = build_durable_store(persistence_path)?;
self.record(SchedulerSubsystem::HaematiteStore);
Ok(store)
}
fn throwaway_store(&self) -> ThrowawayStoreOpener {
let root = self.ephemeral_root.clone();
Box::new(move || open_ephemeral_rooted(&root, THROWAWAY_SHARD_COUNT))
}
}
}
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> {
match config.services.profile()? {
ServiceProfile::Full => {
let subsystems = RootedProductionSubsystems::new(required_persistence_path(config)?)?;
build_connection_services_via(config, &subsystems)
}
ServiceProfile::WorkerFrontDoor => build_worker_front_door(config),
}
}
pub(super) fn build_connection_services_via(
config: &ServerConfig,
subsystems: &dyn SubsystemFactory,
) -> Result<Arc<dyn ConnectionServices>, ServerError> {
match config.services.profile()? {
ServiceProfile::Full => {
let path = required_persistence_path(config)?;
let history = StoreHistory::of(path);
let store = subsystems.durable_store(path)?;
Ok(Arc::new(
LiminalConnectionServices::from_config_with_store_via(
config, store, history, subsystems,
)?,
))
}
ServiceProfile::WorkerFrontDoor => build_worker_front_door(config),
}
}
fn build_worker_front_door(
config: &ServerConfig,
) -> Result<Arc<dyn ConnectionServices>, ServerError> {
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()))
}
pub(crate) fn open_persistent_store(
persistence_path: &Path,
) -> Result<Arc<dyn DurableStore>, ServerError> {
build_durable_store(persistence_path)
}
const SPINE_COMMIT_THRESHOLD_BYTES: NonZeroU64 = match NonZeroU64::new(1 << 20) {
Some(threshold) => threshold,
None => unreachable!(),
};
const SPINE_COMMIT_POLICY: CommitPolicy = CommitPolicy::buffered(SPINE_COMMIT_THRESHOLD_BYTES);
fn build_durable_store(path: &Path) -> Result<Arc<dyn DurableStore>, ServerError> {
let data_dir = durable_data_dir(path);
let database = open_or_create_database(&data_dir)?;
Ok(crate::server::boot_steps::timed("event_store_wrap", || {
let event_store = EventStore::with_commit_policy(database, SPINE_COMMIT_POLICY);
let store: Arc<dyn DurableStore> = Arc::new(CountingStore::new(HaematiteStore::new(
Arc::new(event_store),
)));
store
}))
}
pub(super) fn durable_data_dir(persistence_path: &Path) -> PathBuf {
persistence_path.join("durability")
}
pub(super) fn database_exists(data_dir: &Path) -> bool {
database_config_file(data_dir).exists()
}
const UNTOUCHED_DATABASE_ENTRIES: [&str; 2] = ["config.json", "writer.lock"];
pub(super) fn database_untouched(data_dir: &Path) -> bool {
std::fs::read_dir(data_dir).is_ok_and(|entries| {
entries.into_iter().all(|entry| {
entry.is_ok_and(|entry| {
entry
.file_name()
.to_str()
.is_some_and(|name| UNTOUCHED_DATABASE_ENTRIES.contains(&name))
})
})
})
}
fn database_config_file(data_dir: &Path) -> PathBuf {
data_dir.join("config.json")
}
pub(super) const DURABLE_RETENTION: Retention = Retention::reclaim(0);
fn open_or_create_database(data_dir: &Path) -> Result<Database, ServerError> {
let config_file = database_config_file(data_dir);
let exists = config_file.exists();
let step = if exists {
"database_open"
} else {
"database_create"
};
crate::server::boot_steps::timed(step, || open_or_create_arm(data_dir, &config_file, exists))
}
fn open_or_create_arm(
data_dir: &Path,
config_file: &Path,
exists: bool,
) -> Result<Database, ServerError> {
let result = if exists {
adopt_missing_config_keys(data_dir, config_file)?;
Database::open_with_retention(
data_dir,
haematite::RuntimeOptions {
long_wait_interval: std::time::Duration::from_secs(5),
reclaim_start: haematite::ReclaimStart::Held,
},
DURABLE_RETENTION,
)
} else {
Database::create(
DatabaseConfig {
data_dir: data_dir.to_path_buf(),
shard_count: DEFAULT_SHARD_COUNT,
distributed: None,
executor_threads: None,
node_cache_budget: Some(haematite::NodeCacheBudget::Unlimited),
retention: DURABLE_RETENTION,
},
haematite::RuntimeOptions {
long_wait_interval: std::time::Duration::from_secs(5),
reclaim_start: haematite::ReclaimStart::Held,
},
)
};
let database = result.map_err(|error| ServerError::ConfigValidation {
message: format!(
"failed to open durable store at {}: {error}",
data_dir.display()
),
})?;
liminal::durability::record_store_opened();
super::reclaim_placement::arm_watch(&database);
Ok(database)
}
const ADOPTED_CONFIG_KEYS: [(&str, &str); 2] = [
("node_cache_budget", "unlimited"),
("retention", "keep_all"),
];
fn adopt_missing_config_keys(data_dir: &Path, config_file: &Path) -> Result<(), ServerError> {
let Ok(bytes) = std::fs::read(config_file) else {
return Ok(());
};
let Ok(mut config) = serde_json::from_slice::<serde_json::Value>(&bytes) else {
return Ok(());
};
let Some(object) = config.as_object_mut() else {
return Ok(());
};
let mut adopted = Vec::new();
for (key, value) in ADOPTED_CONFIG_KEYS {
if !object.contains_key(key) {
object.insert(key.to_owned(), serde_json::Value::String(value.to_owned()));
adopted.push(key);
}
}
if adopted.is_empty() {
return Ok(());
}
let keys = adopted.join(", ");
let migrated =
serde_json::to_vec_pretty(&config).map_err(|error| ServerError::ConfigValidation {
message: format!(
"durable store config at {} predates `{keys}` and its migration could not be \
serialized: {error}",
config_file.display()
),
})?;
let write_error = |step: &str, error: std::io::Error| ServerError::ConfigValidation {
message: format!(
"durable store config at {} predates `{keys}` and migrating it failed at {step}: \
{error}",
config_file.display()
),
};
let tmp_file = data_dir.join("config.json.migrate-tmp");
let mut tmp = std::fs::File::create(&tmp_file)
.map_err(|error| write_error("creating the temporary file", error))?;
std::io::Write::write_all(&mut tmp, &migrated)
.map_err(|error| write_error("writing the temporary file", error))?;
tmp.sync_all()
.map_err(|error| write_error("syncing the temporary file", error))?;
drop(tmp);
std::fs::rename(&tmp_file, config_file)
.map_err(|error| write_error("renaming over config.json", error))?;
std::fs::File::open(data_dir)
.and_then(|dir| dir.sync_all())
.map_err(|error| write_error("syncing the data directory", error))?;
tracing::warn!(
config = %config_file.display(),
keys = %keys,
"durable store config predates these keys -- wrote the create-arm values (the \
behaviour-preserving ones) so the store opens with no operator action"
);
Ok(())
}
pub(super) fn reserved_namespace_refusal(channel: &str) -> Option<ServerError> {
if channel.starts_with("liminal.") {
crate::metrics::self_stream_reserved_publication();
Some(ServerError::ReservedNamespace {
channel: channel.to_owned(),
})
} else {
None
}
}
impl LiminalConnectionServices {
pub(in crate::server) const fn signals(&self) -> &ServerSignals {
&self.signals
}
pub(in crate::server) fn self_stream_head(&self, name: &str) -> Result<u64, ServerError> {
let channels = self.read_channels()?;
let channel = channels
.get(name)
.ok_or_else(|| ServerError::ListenerAccept {
message: format!("self stream '{name}' is absent after append"),
})?;
let heads =
channel
.handle
.durable_positions()
.map_err(|error| ServerError::ListenerAccept {
message: format!("self stream '{name}' head is unavailable: {error}"),
})?;
drop(channels);
match heads.as_slice() {
[head] => Ok(*head),
_ => Err(ServerError::ListenerAccept {
message: format!("self stream '{name}' must have exactly one durable partition"),
}),
}
}
pub(in crate::server) fn publish_self_stream_batch(
&self,
payloads: &[Vec<u8>],
publisher: &str,
) -> Result<u64, ServerError> {
if publisher.trim().is_empty() || payloads.is_empty() {
return Err(ServerError::ConfigValidation {
message: "self-stream batch requires a publisher and rows".to_owned(),
});
}
let configured = self
.admit_channel("liminal.events")
.map_err(|error| access_to_server_error(&error))?;
let deliveries = configured
.handle
.publish_batch_with_delivery(
payloads,
&liminal::envelope::PublisherId::new(publisher),
None,
)
.map_err(|error| match error {
liminal::error::LiminalError::PublishUnresolved { append } => {
ServerError::PublishUnresolved {
channel: "liminal.events".to_owned(),
idempotency_key: None,
sequence: Some(append.sequence()),
message: append.to_string(),
}
}
error => ServerError::ListenerAccept {
message: format!("self-stream receipt batch failed: {error}"),
},
})?;
if deliveries.len() != payloads.len()
|| deliveries
.iter()
.any(|delivery| delivery.durable_position().is_none())
{
return Err(ServerError::ListenerAccept {
message: "self-stream receipt batch was refused before append".to_owned(),
});
}
for pair in deliveries.windows(2) {
if pair[0]
.durable_position()
.and_then(|position| position.checked_add(1))
!= pair[1].durable_position()
{
return Err(ServerError::ListenerAccept {
message: "self-stream receipt batch positions are not contiguous".to_owned(),
});
}
}
for delivery in &deliveries {
Self::record_delivery_counts(delivery);
}
deliveries
.last()
.and_then(ChannelDelivery::durable_position)
.and_then(|position| position.checked_add(1))
.ok_or_else(|| ServerError::ListenerAccept {
message: "self-stream receipt batch head is unavailable".to_owned(),
})
}
pub(in crate::server) fn checkpoint_self_stream_heads(
&self,
heads: [u64; 2],
) -> Result<(), ServerError> {
for (name, head) in ["liminal.events:0", "liminal.cost:0"]
.into_iter()
.zip(heads)
{
block_on(liminal::durability::head_checkpoint::advance_head(
self.durable_store.as_ref(),
name,
head,
))
.map_err(|error| ServerError::ShutdownFlush {
message: format!("self-stream head checkpoint bridge for '{name}' failed: {error}"),
})?
.map_err(|error| ServerError::ShutdownFlush {
message: format!("self-stream head checkpoint for '{name}' failed: {error}"),
})?;
}
Ok(())
}
pub(in crate::server) fn install_self_stream_channels(&self) -> Result<(), ServerError> {
let names = ["liminal.cost", "liminal.events"];
{
let channels = self.read_channels()?;
for name in names {
if channels.contains_key(name) {
return Err(ServerError::ConfigValidation {
message: format!("self-stream channel '{name}' is already configured"),
});
}
}
}
let mut built = Vec::with_capacity(names.len());
for name in names {
let schema =
resolve_schema_bytes(None).map_err(|error| ServerError::ConfigValidation {
message: format!("self-stream channel '{name}' schema failed: {error}"),
})?;
let channel = build_configured_channel(
name,
schema,
true,
ChannelOrigin::BootConfigured,
&self.durable_store,
self.cluster.supervisor(),
)
.map_err(|error| ServerError::ConfigValidation {
message: error.boot_message(name),
})?;
channel
.handle
.install_publication_identity(self.publication_identity.clone())
.map_err(|error| ServerError::ConfigValidation {
message: error.to_string(),
})?;
built.push((name.to_owned(), Arc::new(channel)));
}
let mut channels =
self.channels
.write()
.map_err(|error| ServerError::ConfigValidation {
message: format!("self-stream channel roster lock poisoned: {error}"),
})?;
for (name, _) in &built {
if channels.contains_key(name) {
return Err(ServerError::ConfigValidation {
message: format!("self-stream channel '{name}' was concurrently configured"),
});
}
}
channels.extend(built);
drop(channels);
for name in names {
self.console_changed(crate::console::TopologyChange::Channel {
name: name.to_owned(),
life: line_life(true),
state: crate::console::records::LineState::Live,
});
}
Ok(())
}
pub(in crate::server) fn install_console_channels(&self) -> Result<(), ServerError> {
self.register_console_channel(crate::console::TOPOLOGY_SUBJECT)
.map_err(|error| ServerError::ConfigValidation {
message: format!("console channel registration failed: {error}"),
})
}
fn register_console_channel(&self, name: &str) -> Result<(), ChannelRegistryError> {
if self.read_roster()?.contains_key(name) {
return Ok(());
}
let schema = resolve_schema_bytes(None).map_err(|error| {
ChannelRegistryError::RosterUnavailable {
message: format!("console channel '{name}' schema failed: {error}"),
}
})?;
let channel = build_configured_channel(
name,
schema,
false,
ChannelOrigin::BootConfigured,
&self.durable_store,
self.cluster.supervisor(),
)
.map_err(|error| error.into_registry_error(name))?;
channel
.handle
.install_publication_identity(self.publication_identity.clone())
.map_err(|error| ChannelRegistryError::RosterUnavailable {
message: error.to_string(),
})?;
self.write_roster()?
.entry(name.to_owned())
.or_insert_with(|| Arc::new(channel));
Ok(())
}
pub fn publish_embedded(
&self,
host: &str,
channel: &str,
envelope: &MessageEnvelope,
idempotency_key: Option<&str>,
) -> Result<PublishOutcome, ServerError> {
self.publish_with_origin(
channel,
envelope,
idempotency_key,
PublishOrigin::Host { name: host },
)
.map(|published| published.outcome)
}
pub(in crate::server) fn publish_with_origin(
&self,
channel: &str,
envelope: &MessageEnvelope,
idempotency_key: Option<&str>,
origin: PublishOrigin<'_>,
) -> Result<OriginPublication, ServerError> {
if !origin.is_server() {
if plan_subject(channel).is_some() {
return Err(site_services::site_error(
crate::services::lifecycle::site::SiteError::Invalid(
"plan subject is server-owned",
),
));
}
crate::agents::binding::refuse_server_publish(channel)?;
}
validate_publication_origin(channel, idempotency_key, origin)?;
let mut recipients = self.signals.lend_recipients();
let received_at = recipients.is_some().then(Instant::now);
let result = (|| {
let configured = self
.admit_channel(channel)
.map_err(|error| access_to_server_error(&error))?;
if let (Some(key), ChannelMode::Durable) =
(idempotency_key, configured.handle.config().mode)
{
return self
.publish_durable_keyed(
&configured,
channel,
(envelope, origin),
key,
recipients.as_mut(),
)
.map(OriginPublication::connection);
}
let dedup = self.dedup_for(&configured);
if let Some(key) = idempotency_key {
if !Self::claim_delivery(dedup, key)? {
crate::metrics::publish_accepted();
return Ok(OriginPublication::connection(PublishOutcome {
message_id: self.next_message_id()?,
delivered: false,
durable_position: None,
reach: Reach::Suppressed,
}));
}
}
let delivery = fan_out(&configured, envelope, origin, recipients.as_mut());
let delivery = match delivery {
Ok(delivery) => delivery,
Err(liminal::error::LiminalError::PublishUnresolved { append }) => {
return Err(ServerError::PublishUnresolved {
channel: channel.to_owned(),
idempotency_key: None,
sequence: Some(append.sequence()),
message: append.to_string(),
});
}
Err(error) => {
if let Some(key) = idempotency_key {
Self::release_claim(dedup, key);
}
return Err(ServerError::ListenerAccept {
message: format!("liminal publish failed for channel '{channel}': {error}"),
});
}
};
if let Some(key) = idempotency_key {
if delivery.is_admitted() {
block_on(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}"),
})?;
} else {
Self::release_claim(dedup, key);
}
}
let self_row_at = delivery.durable_position().filter(|_| origin.is_server());
Ok(OriginPublication {
outcome: Self::delivered(&delivery)?,
self_row_appended: self_row_at.is_some(),
self_row_at,
})
})();
self.signals.published(
channel,
origin,
envelope,
&result,
recipients.as_ref().zip(received_at),
);
if let Some(buffer) = recipients {
self.signals.give_back(buffer);
}
result
}
}
fn fan_out(
configured: &ConfiguredChannel,
envelope: &MessageEnvelope,
origin: PublishOrigin<'_>,
recipients: Option<&mut liminal::channel::PublishRecord>,
) -> Result<liminal::channel::ChannelDelivery, liminal::error::LiminalError> {
let publisher = || publisher_for_origin(origin);
recipients.map_or_else(
|| {
configured
.handle
.publish_with_delivery(&envelope.payload, publisher(), None)
},
|recipients| {
configured.handle.publish_recording(
&envelope.payload,
publisher(),
None,
None,
recipients,
)
},
)
}
fn validate_publication_origin(
channel: &str,
idempotency_key: Option<&str>,
origin: PublishOrigin<'_>,
) -> Result<(), ServerError> {
if !origin.is_server() {
refuse_console_namespace(channel).map_err(|error| access_to_server_error(&error))?;
if let Some(error) = reserved_namespace_refusal(channel) {
return Err(error);
}
}
if let PublishOrigin::Host { name } = origin
&& name.trim().is_empty()
{
return Err(ServerError::ConfigValidation {
message: "embedded publication requires a nonempty host name".to_owned(),
});
}
if let PublishOrigin::Server { device } = origin {
if device.trim().is_empty() || idempotency_key.is_some() {
return Err(ServerError::ConfigValidation {
message: "server-origin publication requires a device and no idempotency key"
.to_owned(),
});
}
}
Ok(())
}
fn publisher_for_origin(origin: PublishOrigin<'_>) -> liminal::envelope::PublisherId {
match origin {
PublishOrigin::Connection { id } => liminal::envelope::PublisherId::new(id.to_string()),
PublishOrigin::Host { name } => {
liminal::envelope::PublisherId::new(format!("liminal.{name}"))
}
PublishOrigin::Server { device } => {
liminal::envelope::PublisherId::new(format!("liminal.{device}"))
}
}
}
impl ConnectionServices for LiminalConnectionServices {
fn construction_request(
&self,
session: Option<&dyn crate::auth_lys::session::LysSession>,
subject: &str,
bytes: &[u8],
at_ms: u64,
) -> Option<Result<serde_json::Value, ServerError>> {
if let Some(answer) = self.agent_host_request(session, subject, bytes, at_ms) {
return Some(answer);
}
is_site_route(subject).then(|| {
self.serve_site_tool(session, subject, bytes, at_ms)
.map_err(site_services::site_error)
})
}
fn prepare_site_trial(
&self,
connection: u64,
session: Option<&dyn crate::auth_lys::session::LysSession>,
registration: &liminal::protocol::WorkerRegistration,
process: Option<&crate::services::lifecycle::ProcessGrant>,
at_ms: u64,
) -> Result<Option<crate::services::lifecycle::trial::TrialPermit>, ServerError> {
self.site_lifecycle.get().map_or(Ok(None), |lifecycle| {
lifecycle
.prepare_trial(connection, session, registration, process, at_ms)
.map_err(site_services::site_error)
})
}
fn admit_site_call(
&self,
session: Option<&dyn crate::auth_lys::session::LysSession>,
subject: &str,
at_ms: u64,
) -> Result<bool, ServerError> {
self.site_lifecycle.get().map_or(Ok(false), |lifecycle| {
lifecycle
.admit_trial_call(session, subject, at_ms)
.map_err(site_services::site_error)
})
}
fn site_registration_closed(&self, connection: u64) -> Result<(), ServerError> {
if let Some(lifecycle) = self.site_lifecycle.get() {
lifecycle
.trial_closed(connection)
.map_err(site_services::site_error)?;
lifecycle
.refresh_sites()
.map_err(site_services::site_error)?;
}
Ok(())
}
fn publish_identified(
&self,
connection: u64,
session: Option<&dyn crate::auth_lys::session::LysSession>,
channel: &str,
envelope: &MessageEnvelope,
key: Option<&str>,
) -> Result<PublishOutcome, ServerError> {
if plan_subject(channel).is_some() {
return Err(site_services::site_error(
crate::services::lifecycle::site::SiteError::Invalid(
"plan subject is server-owned",
),
));
}
self.publish_agent(connection, session, channel, envelope, key)
}
fn agents(&self) -> Option<&Arc<crate::agents::runtime::Agents>> {
self.agents.as_ref()
}
fn subscribe_identified(
&self,
connection: u64,
session: Option<&dyn crate::auth_lys::session::LysSession>,
channel: &str,
schemas: &[ProtocolSchemaId],
install: Option<liminal::channel::InboxInstall>,
) -> Result<ConnectionSubscription, ServerError> {
if plan_subject(channel).is_some() {
return self.subscribe_site_plan(session, channel, schemas, install);
}
self.subscribe_agent(connection, session, channel, schemas, install)
}
fn participant_service(&self) -> Option<InstalledParticipantService> {
self.participant_service.clone()
}
fn admit_channel(
&self,
operation: ChannelOperation,
channel: &str,
) -> Result<(), ChannelAccessError> {
if let Some(name) = plan_subject(channel) {
let refused = |message| ChannelAccessError::RosterUnavailable { message };
if operation == ChannelOperation::Publish {
return Err(refused("plan subject is server-owned".into()));
}
let lifecycle = self
.site_lifecycle
.get()
.ok_or_else(|| refused("construction not installed".into()))?;
lifecycle
.record(name)
.map_err(|error| refused(error.to_string()))?;
return Ok(());
}
if let Some(admitted) = self.agent_channel_admission(channel) {
return admitted;
}
match operation {
ChannelOperation::Publish => refuse_console_namespace(channel)?,
ChannelOperation::Subscribe => {
refuse_console_tap_target(channel)?;
self.refuse_console_tap_limit(channel)?;
let admitted = if self.signals.console_enabled()
&& super::channel_registry::console_tap_target(channel).is_some()
{
self.register_console_channel(channel).map_err(|error| {
ChannelAccessError::RosterUnavailable {
message: error.to_string(),
}
})
} else {
Ok(())
}
.and_then(|()| Self::admit_channel(self, channel).map(|_| ()));
if admitted.is_err() {
self.give_back_tap(channel);
}
return admitted;
}
}
Self::admit_channel(self, channel)?;
Ok(())
}
fn publish(
&self,
connection: u64,
channel: &str,
envelope: &MessageEnvelope,
idempotency_key: Option<&str>,
) -> Result<PublishOutcome, ServerError> {
refuse_console_namespace(channel).map_err(|error| access_to_server_error(&error))?;
if plan_subject(channel).is_some() {
return Err(site_services::site_error(
crate::services::lifecycle::site::SiteError::Invalid(
"plan subject is server-owned",
),
));
}
crate::agents::binding::refuse_server_publish(channel)?;
if crate::agents::binding::subject(channel).is_some() {
return Err(crate::agents::error::AgentError::Invalid(
"agent publish requires the verified identity route",
)
.into());
}
self.publish_with_origin(
channel,
envelope,
idempotency_key,
PublishOrigin::Connection { id: connection },
)
.map(|published| published.outcome)
}
fn subscribe(
&self,
channel: &str,
accepted_schemas: &[ProtocolSchemaId],
install: Option<liminal::channel::InboxInstall>,
) -> Result<ConnectionSubscription, ServerError> {
if plan_subject(channel).is_some() {
return Err(site_services::site_error(
crate::services::lifecycle::site::SiteError::Invalid(
"plan subscription needs an admitted pass",
),
));
}
if crate::agents::binding::subject(channel).is_some() {
return Err(crate::agents::error::AgentError::Invalid(
"agent subscribe requires the verified identity route",
)
.into());
}
let subscribed = self.open_subscription(channel, accepted_schemas, install, None);
if subscribed.is_err() {
self.give_back_tap(channel);
}
subscribed
}
fn unsubscribe(&self, subscription: ConnectionSubscription) -> Result<(), ServerError> {
subscription.unsubscribe()
}
fn open_conversation(
&self,
conversation_id: u64,
subject: &str,
) -> Result<ConnectionConversation, ServerError> {
self.open_routed_conversation(conversation_id, subject)
}
fn conversation_message(
&self,
conversation: &ConnectionConversation,
envelope: &MessageEnvelope,
op_id: Option<u64>,
) -> Result<(), ServerError> {
self.routed_conversation_message(conversation, envelope, op_id)
}
fn close_conversation(&self, conversation: ConnectionConversation) -> Result<(), ServerError> {
conversation.close()
}
fn listening(self: Arc<Self>) -> Option<std::thread::JoinHandle<()>> {
self.resolve_claims_behind_listener()
}
fn stop_participant_warm(&self) {
if let Some(warm) = &self.participant_warm {
warm.stop();
warm.record_clean_stop();
}
}
fn finish_background_store_work(&self) -> Result<(), ServerError> {
self.finish_background_work()
}
fn release_store_holders(&self) -> Result<(), ServerError> {
self.release_holders()
}
fn flush_durable_state(&self) -> Result<(), ServerError> {
self.signals.with_flush_barrier(|| {
block_on(self.head_checkpoint_stop.checkpoint_heads())
.map_err(|error| ServerError::ShutdownFlush {
message: format!("clean-stop head checkpoint bridge failed: {error}"),
})?
.map_err(|error| ServerError::ShutdownFlush {
message: format!("clean-stop head checkpoint failed: {error}"),
})?;
block_on(self.durable_store.flush())
.map_err(|error| ServerError::ShutdownFlush {
message: format!("shared durable store flush bridge failed: {error}"),
})?
.map_err(|error| ServerError::ShutdownFlush {
message: format!("shared durable store flush failed: {error}"),
})?;
Ok(())
})?;
Ok(())
}
}
#[derive(Debug)]
pub(super) struct ConfiguredChannel {
handle: ChannelHandle,
protocol_schema: ProtocolSchemaId,
durable: bool,
origin: ChannelOrigin,
state: AtomicU8,
quiesce_reason: OnceLock<String>,
}
impl ConfiguredChannel {
fn state(&self) -> ChannelState {
if self.state.load(Ordering::Acquire) == STATE_QUIESCED {
ChannelState::Quiesced {
reason: self.recorded_quiesce_reason(),
}
} else {
ChannelState::Active
}
}
fn recorded_quiesce_reason(&self) -> String {
self.quiesce_reason
.get()
.map_or_else(|| UNRECORDED_QUIESCE_REASON.to_owned(), Clone::clone)
}
fn quiesce(&self, name: &str, reason: &str) -> Result<(), ChannelRegistryError> {
let recorded = match self.quiesce_reason.set(reason.to_owned()) {
Ok(()) => reason.to_owned(),
Err(_rejected) => self.recorded_quiesce_reason(),
};
let _flipped_here = self.state.compare_exchange(
STATE_ACTIVE,
STATE_QUIESCED,
Ordering::Release,
Ordering::Acquire,
);
if recorded == reason {
return Ok(());
}
Err(ChannelRegistryError::AlreadyQuiesced {
name: name.to_owned(),
reason: recorded,
})
}
}
#[derive(Debug)]
struct LiminalSubscriptionResource {
subscription: liminal::channel::SubscriptionHandle,
}
impl SubscriptionResource for LiminalSubscriptionResource {
fn try_next_positioned(
&mut self,
) -> Result<Option<(liminal::envelope::Envelope, Option<u64>)>, ServerError> {
self.subscription
.try_next_positioned()
.map_err(|error| subscription_failure(&error))
}
fn acknowledge_position(&self, position: u64) -> Result<(), ServerError> {
self.subscription
.acknowledge_position(position)
.map_err(|error| subscription_failure(&error))
}
fn consumer_position(&self) -> Result<Option<u64>, ServerError> {
self.subscription
.consumer_position()
.map_err(|error| subscription_failure(&error))
}
fn unsubscribe(self: Box<Self>) -> Result<(), ServerError> {
drop(self.subscription);
Ok(())
}
fn is_overflowed(&self) -> bool {
self.subscription.is_overflowed()
}
fn subscriber_pid(&self) -> Option<u64> {
Some(self.subscription.subscriber_pid())
}
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 could not receive its next envelope"
);
None
}
}
}
}
fn subscription_failure(error: &liminal::error::LiminalError) -> ServerError {
ServerError::ListenerAccept {
message: format!("liminal subscription: {error}"),
}
}
pub(super) fn access_to_server_error(error: &ChannelAccessError) -> ServerError {
let message = match error {
ChannelAccessError::NotRegistered { name } => {
format!("channel '{name}' is not configured")
}
ChannelAccessError::RosterUnavailable { message } => message.clone(),
ChannelAccessError::Quiesced { .. }
| ChannelAccessError::ConsoleNamespace { .. }
| ChannelAccessError::ConsoleTapTarget { .. }
| ChannelAccessError::ConsoleTapLimit { .. } => error.to_string(),
};
ServerError::ListenerAccept { message }
}
fn refuse_console_tap_target(channel: &str) -> Result<(), ChannelAccessError> {
use super::channel_registry::{console_tap_target, is_console_subject};
match console_tap_target(channel) {
Some(target) if target.is_empty() || is_console_subject(target) => {
Err(ChannelAccessError::ConsoleTapTarget {
name: channel.to_owned(),
})
}
_ => Ok(()),
}
}
fn refuse_console_namespace(channel: &str) -> Result<(), ChannelAccessError> {
if super::channel_registry::is_console_subject(channel) {
return Err(ChannelAccessError::ConsoleNamespace {
name: channel.to_owned(),
});
}
Ok(())
}
pub(super) fn server_error_from_protocol(error: &ProtocolError) -> ServerError {
ServerError::ListenerAccept {
message: format!("protocol operation failed: {error}"),
}
}
#[cfg(test)]
#[path = "services_registry_tests.rs"]
mod services_registry_tests;
#[cfg(test)]
mod durable_store_tests {
type TestResult = Result<(), Box<dyn std::error::Error>>;
trait Must<T> {
fn must(self, what: &str) -> Result<T, String>;
}
impl<T, E: std::fmt::Debug> Must<T> for Result<T, E> {
fn must(self, what: &str) -> Result<T, String> {
self.map_err(|error| format!("{what}: {error:?}"))
}
}
impl<T> Must<T> for Option<T> {
fn must(self, what: &str) -> Result<T, String> {
self.ok_or_else(|| what.to_owned())
}
}
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,
};
use crate::ServerError;
use crate::config::types::{LimitsConfig, ServerConfig, ServicesConfig};
fn entry_count(root: &std::path::Path) -> Result<usize, String> {
Ok(std::fs::read_dir(root)
.must("ephemeral root is readable")?
.count())
}
fn config_with_profile(profile: &str) -> Result<ServerConfig, String> {
Ok(ServerConfig {
listen_address: "127.0.0.1:0".parse().must("valid socket addr")?,
extra_listen_addresses: Vec::new(),
health_listen_address: "127.0.0.1:1".parse().must("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(),
..ServicesConfig::default()
},
limits: LimitsConfig::default(),
participant: None,
websocket: None,
})
}
#[test]
fn worker_front_door_builds_no_store_and_no_temp_dir() -> TestResult {
let front_door_root = tempfile::tempdir().must("test can create an ephemeral root")?;
let full_root = tempfile::tempdir().must("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,
)
.must("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 mut config = config_with_profile("full")?;
config.persistence_path = Some(full_root.path().to_path_buf());
let full =
build_connection_services_via(&config, &full_subsystems).must("full services build")?;
assert!(
full.supports_channel_operations(),
"full mode serves channel operations"
);
assert_eq!(
entry_count(full_root.path())?,
1,
"full mode builds its persistent database in the explicit directory"
);
drop(front_door);
drop(full);
Ok(())
}
#[test]
fn worker_profile_census_is_empty_and_full_profile_records_all_schedulers() -> TestResult {
let worker_root = tempfile::tempdir().must("test can create an ephemeral root")?;
let full_root = tempfile::tempdir().must("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,
)
.must("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 mut config = config_with_profile("full")?;
config.persistence_path = Some(full_root.path().to_path_buf());
let full =
build_connection_services_via(&config, &full_subsystems).must("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);
Ok(())
}
#[test]
fn full_only_constructors_reject_worker_profile() -> TestResult {
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().must("test can create an ephemeral root")?;
let store = open_ephemeral_rooted(root.path(), DEFAULT_SHARD_COUNT)
.must("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"
);
Ok(())
}
#[test]
fn build_connection_services_rejects_worker_profile_with_full_only_fields() -> TestResult {
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 {
return Err(
"expected ConfigValidation for worker profile with full-only fields".into(),
);
};
assert!(message.contains("builds no channels"), "got: {message}");
assert!(
message.contains("builds no durable store"),
"got: {message}"
);
Ok(())
}
#[test]
fn persistent_store_uses_configured_path_and_creates_no_temp_dir() -> TestResult {
let home = tempfile::tempdir().must("test can create a temp dir")?;
let ephemeral_root = tempfile::tempdir().must("test can create an ephemeral root")?;
let store = build_durable_store(home.path()).must("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);
Ok(())
}
#[test]
fn missing_path_refuses_before_any_store_is_opened() -> Result<(), Box<dyn std::error::Error>> {
let root = tempfile::tempdir()?;
let factory = RecordingSubsystems::rooted(root.path());
let result = build_connection_services_via(&config_with_profile("full")?, &factory);
assert!(
matches!(result, Err(ServerError::ConfigValidation { message }) if message.contains("no store is opened"))
);
assert_eq!(entry_count(root.path())?, 0);
assert!(factory.recorded().is_empty());
Ok(())
}
#[test]
fn a_store_whose_config_predates_the_budget_field_opens_with_no_operator_hand() -> TestResult {
let home = tempfile::tempdir().must("test can create a temp dir")?;
let store = build_durable_store(home.path()).must("persistent store builds")?;
drop(store);
let config_file = home.path().join("durability").join("config.json");
let bytes = std::fs::read(&config_file).must("config.json is readable")?;
let mut config: serde_json::Value =
serde_json::from_slice(&bytes).must("config.json parses")?;
config
.as_object_mut()
.must("config.json is an object")?
.remove("node_cache_budget")
.must("the created config carries the budget to remove")?;
std::fs::write(
&config_file,
serde_json::to_vec_pretty(&config).must("serializes")?,
)
.must("test can age the config")?;
let reopened = build_durable_store(home.path())
.must("a pre-budget store opens with no operator action")?;
drop(reopened);
let after = std::fs::read(&config_file).must("config.json is readable after")?;
let migrated: serde_json::Value =
serde_json::from_slice(&after).must("migrated config parses")?;
assert_eq!(
migrated.get("node_cache_budget"),
Some(&serde_json::Value::String("unlimited".to_owned())),
"the migrated file carries the budget the create arm chooses"
);
Ok(())
}
#[test]
fn a_corrupt_config_still_refuses_and_is_left_untouched() -> TestResult {
let home = tempfile::tempdir().must("test can create a temp dir")?;
let data_dir = home.path().join("durability");
std::fs::create_dir_all(&data_dir).must("test can create the data dir")?;
let config_file = data_dir.join("config.json");
let garbage = b"{ not json at all";
std::fs::write(&config_file, garbage).must("test can write garbage")?;
let refused = build_durable_store(home.path());
assert!(refused.is_err(), "a corrupt config refuses to open");
assert_eq!(
std::fs::read(&config_file)
.must("config.json is still readable")?
.as_slice(),
garbage,
"the refusing file is left byte-identical for the operator to read"
);
Ok(())
}
#[test]
fn a_config_already_carrying_the_budget_is_left_byte_identical() -> TestResult {
let home = tempfile::tempdir().must("test can create a temp dir")?;
let store = build_durable_store(home.path()).must("persistent store builds")?;
drop(store);
let config_file = home.path().join("durability").join("config.json");
let before = std::fs::read(&config_file).must("config.json is readable")?;
let reopened = build_durable_store(home.path()).must("a current store reopens")?;
drop(reopened);
assert_eq!(
std::fs::read(&config_file).must("config.json is readable after")?,
before,
"a config that already carries the budget is left byte-identical"
);
Ok(())
}
#[test]
fn a_keep_all_durable_store_reclaims_after_the_next_open() -> TestResult {
let dir = tempfile::tempdir().must("test can create a store dir")?;
let data_dir = dir.path().join("durability");
let database = haematite::Database::create(
haematite::DatabaseConfig {
data_dir: data_dir.clone(),
shard_count: 1,
distributed: None,
executor_threads: None,
node_cache_budget: Some(haematite::NodeCacheBudget::Unlimited),
retention: haematite::Retention::KeepAll,
},
haematite::RuntimeOptions {
long_wait_interval: std::time::Duration::from_secs(5),
reclaim_start: haematite::ReclaimStart::AtOpen,
},
)
.must("a keep-all store is created")?;
for round in 0_u32..200 {
let mut value = round.to_be_bytes().to_vec();
value.resize(1_024, 0x5a);
database.put(b"key".to_vec(), value).must("put")?;
database.commit().must("commit")?;
}
drop(database);
let grown =
haematite::db::vacuum::stats(&haematite::db::vacuum::Options::new(data_dir.clone()))
.must("stats of the grown store")?
.totals
.enumerated_bytes;
let reopened = super::open_or_create_database(&data_dir).must("the server opens it")?;
reopened
.release_reclaim()
.must("the held reclaim is released")?;
reopened.quiesce_reclaim().must("reclaim runs to rest")?;
drop(reopened);
let config: serde_json::Value = serde_json::from_slice(
&std::fs::read(data_dir.join("config.json")).must("config is readable")?,
)
.must("config is JSON")?;
assert_eq!(
config["retention"],
serde_json::to_value(super::DURABLE_RETENTION).must("the retention serializes")?
);
let report = haematite::db::vacuum::stats(&haematite::db::vacuum::Options::new(data_dir))
.must("stats of the reclaimed store")?;
assert!(
report.totals.enumerated_bytes <= report.totals.marked_bytes.saturating_mul(3),
"the reopened store holds about its live set: enumerated {} B, marked {} B",
report.totals.enumerated_bytes,
report.totals.marked_bytes
);
assert!(
report.totals.enumerated_bytes.saturating_mul(10) < grown,
"the old copies are gone: {} B now against {grown} B grown",
report.totals.enumerated_bytes
);
Ok(())
}
const STORED_RECLAIM_CONFIG: &str = r#"{
"chunking_policy": {
"chunking": "bytes-v2",
"internal_target_bytes": 49152,
"leaf_target_bytes": 65536
},
"data_dir": "DATA_DIR",
"distributed": null,
"executor_threads": null,
"format_version": 2,
"node_cache_budget": "unlimited",
"retention": {
"reclaim": {
"past_roots": 0
}
},
"shard_count": 8
}"#;
#[test]
fn a_stored_reclaim_store_grown_without_reclaim_gives_its_old_copies_back_on_open() -> TestResult
{
let dir = tempfile::tempdir().must("test can create a store dir")?;
let data_dir = dir.path().join("durability");
let database = haematite::Database::create(
haematite::DatabaseConfig {
data_dir: data_dir.clone(),
shard_count: 8,
distributed: None,
executor_threads: None,
node_cache_budget: Some(haematite::NodeCacheBudget::Unlimited),
retention: haematite::Retention::KeepAll,
},
haematite::RuntimeOptions {
long_wait_interval: std::time::Duration::from_secs(5),
reclaim_start: haematite::ReclaimStart::AtOpen,
},
)
.must("a keep-all store is created")?;
for round in 0_u32..200 {
let mut value = round.to_be_bytes().to_vec();
value.resize(1_024, 0x5a);
database.put(b"key".to_vec(), value).must("put")?;
database.commit().must("commit")?;
}
for row in 0_u32..200 {
let key = [b"pruned-".as_slice(), &row.to_be_bytes()].concat();
database.put(key, vec![0xa5; 1_024]).must("put")?;
database.commit().must("commit")?;
}
for row in 0_u32..200 {
let key = [b"pruned-".as_slice(), &row.to_be_bytes()].concat();
database.delete(key).must("delete")?;
}
database.commit().must("commit")?;
drop(database);
let grown_report =
haematite::db::vacuum::stats(&haematite::db::vacuum::Options::new(data_dir.clone()))
.must("stats of the grown store")?;
let grown = grown_report.totals;
assert!(
grown.unmarked_nodes > 0,
"the seed leaves copies no root reaches"
);
let mut owed: std::collections::BTreeSet<usize> = grown_report
.shards
.iter()
.filter(|shard| shard.nodes.unmarked_nodes > 0)
.map(|shard| shard.shard_id)
.collect();
let config_file = data_dir.join("config.json");
let seeded = STORED_RECLAIM_CONFIG
.replace("DATA_DIR", data_dir.to_str().must("the test dir is UTF-8")?);
std::fs::write(&config_file, &seeded).must("config.json is written")?;
let reopened = super::open_or_create_database(&data_dir).must("the server opens it")?;
let watch = reopened
.watch_reclaim_passes(64)
.must("every shard accepts a pass watch")?;
reopened
.release_reclaim()
.must("the held reclaim is released")?;
let mut unmarked_by_shard = std::collections::BTreeMap::new();
while !owed.is_empty() {
let report = watch
.recv_timeout(std::time::Duration::from_secs(30))
.must("each pass the open owes ends without a write or a quiesce")?;
assert_eq!(
report.outcome,
haematite::ReclaimPassOutcome::Passed,
"the pass the open owed on shard {} finished",
report.shard_id
);
if owed.remove(&report.shard_id) {
unmarked_by_shard.insert(report.shard_id, report.unmarked_bytes);
}
}
drop(reopened);
let after = haematite::db::vacuum::stats(&haematite::db::vacuum::Options::new(data_dir))
.must("stats of the reclaimed store")?
.totals;
eprintln!(
"grown {} B, after {} B, {} unreachable nodes seeded, unmarked bytes by shard {unmarked_by_shard:?}",
grown.enumerated_bytes, after.enumerated_bytes, grown.unmarked_nodes
);
assert!(
after.enumerated_bytes.saturating_mul(10) < grown.enumerated_bytes,
"the old copies are gone: {} B now against {} B grown",
after.enumerated_bytes,
grown.enumerated_bytes
);
let config: serde_json::Value = serde_json::from_slice(
&std::fs::read(&config_file).must("config.json is readable after")?,
)
.must("config is JSON")?;
assert_eq!(
config["retention"],
serde_json::to_value(super::DURABLE_RETENTION).must("the retention serializes")?,
"config.json names reclaim after the open"
);
Ok(())
}
}