1#[path = "publication_identity.rs"]
2mod publication_identity;
3use liminal::channel::PublicationIdentity;
4use std::collections::HashMap;
5use std::path::{Path, PathBuf};
6use std::sync::atomic::{AtomicU8, AtomicU64, Ordering};
7use std::sync::{Arc, Mutex, OnceLock, RwLock};
8use std::time::Instant;
9
10use haematite::{CommitPolicy, Database, DatabaseConfig, EventStore, Retention};
11use liminal::channel::{
12 ChannelConfig, ChannelDelivery, ChannelHandle, ChannelMode, ChannelSupervisor, Schema,
13};
14use liminal::conversation::{ConversationSupervisor, ParticipantBehaviour};
15use liminal::durability::bridge::block_on;
16#[cfg(any(test, feature = "test-support"))]
17use liminal::durability::open_ephemeral;
18use liminal::durability::{
19 CountingStore, DedupCache, DedupDecision, DurableStore, EphemeralRoot, EphemeralSweepCounts,
20 HaematiteStore, ProcessingReceipt, StoreCountsHandle,
21};
22use liminal::protocol::{MessageEnvelope, ProtocolError, SchemaId as ProtocolSchemaId};
23use std::num::NonZeroU64;
24
25use super::channel_registry::{
26 ChannelAccessError, ChannelBuildError, ChannelConfigField, ChannelDescriptor, ChannelOrigin,
27 ChannelRegistration, ChannelRegistryError, ChannelState, ChannelStatus, MAX_CHANNELS_KEY,
28 Registered, STATE_ACTIVE, STATE_QUIESCED, UNRECORDED_QUIESCE_REASON,
29};
30use super::conversation::ConnectionConversation;
31use super::services_cluster::build_channel_cluster;
32use super::services_schema::{ChannelSchema, resolve_channel_schema, resolve_schema_bytes};
33use super::worker_front_door::WorkerFrontDoorServices;
34use crate::ServerError;
35use crate::config::types::{ClusterConfig, ParticipantConfig, ServerConfig, ServiceProfile};
36use crate::config::validation::required_persistence_path;
37use crate::health::reissue::OperatorCredentialReissuer;
38use crate::health::unloadable::UnloadableConversationRecord;
39use crate::server::participant::{
40 InstalledParticipantService, ParticipantWarm, ProductionParticipantHandler,
41};
42use crate::server::self_streams::events::PublishOrigin;
43use crate::server::self_streams::sink::OriginPublication;
44use crate::server::signals::ServerSignals;
45
46pub use super::services_cluster::ChannelCluster;
47
48#[path = "../../agents/host_server.rs"]
49mod agent_host;
50#[path = "../../agents/server.rs"]
51mod agent_services;
52#[path = "../../agents/approval_boot.rs"]
53mod approval_boot;
54#[path = "../../services/site/install.rs"]
55mod site_install;
56#[path = "../../services/site/server.rs"]
57mod site_services;
58pub use site_services::{
59 NOT_CONFIGURED, SiteDraftHolder, evaluate_subject as evaluate_site_subject, is_site_route,
60 plan_subject,
61};
62pub(super) use site_services::{SiteFrame, site_frame_response};
63#[path = "services_lifecycle.rs"]
64mod lifecycle;
65#[path = "services_conversation.rs"]
66mod services_conversation;
67
68type ResponderRegistry = HashMap<String, Arc<dyn ParticipantBehaviour>>;
73
74pub trait SubscriptionResource: std::fmt::Debug + Send {
76 fn unsubscribe(self: Box<Self>) -> Result<(), ServerError>;
81
82 fn try_next(&mut self) -> Option<liminal::envelope::Envelope>;
89
90 fn has_pending(&self) -> bool;
92
93 fn is_overflowed(&self) -> bool {
97 false
98 }
99
100 fn subscriber_pid(&self) -> Option<u64> {
104 None
105 }
106
107 fn try_next_positioned(
112 &mut self,
113 ) -> Result<Option<(liminal::envelope::Envelope, Option<u64>)>, ServerError> {
114 Ok(self.try_next().map(|envelope| (envelope, None)))
115 }
116
117 fn acknowledge_position(&self, position: u64) -> Result<(), ServerError> {
122 Err(ServerError::ListenerAccept {
123 message: format!("subscription cannot acknowledge durable position {position}"),
124 })
125 }
126
127 fn consumer_position(&self) -> Result<Option<u64>, ServerError> {
132 Ok(None)
133 }
134}
135
136#[derive(Debug)]
138pub struct ConnectionSubscription {
139 id: u64,
140 channel: String,
145 stream_id: u32,
150 selected_schema: ProtocolSchemaId,
151 resource: Box<dyn SubscriptionResource>,
152 issued: super::state::IssuedDeliveries,
153}
154
155impl ConnectionSubscription {
156 #[must_use]
158 pub fn new(
159 id: u64,
160 selected_schema: ProtocolSchemaId,
161 resource: Box<dyn SubscriptionResource>,
162 ) -> Self {
163 Self {
164 id,
165 channel: String::new(),
166 stream_id: 0,
167 selected_schema,
168 resource,
169 issued: super::state::IssuedDeliveries::new(0),
170 }
171 }
172
173 pub(super) fn set_channel(&mut self, channel: String) {
178 self.channel = channel;
179 }
180
181 #[must_use]
183 pub(super) fn channel(&self) -> &str {
184 &self.channel
185 }
186
187 #[must_use]
189 pub const fn id(&self) -> u64 {
190 self.id
191 }
192
193 pub(super) const fn set_stream_id(&mut self, stream_id: u32) {
195 self.stream_id = stream_id;
196 }
197
198 #[must_use]
200 pub(super) const fn stream_id(&self) -> u32 {
201 self.stream_id
202 }
203
204 pub(super) fn set_pressure_ledger_depth(&mut self, limit: usize) {
205 self.issued = super::state::IssuedDeliveries::new(limit);
206 }
207
208 pub(super) fn issued_delivery(&mut self, sequence: u64) {
209 self.issued.record(sequence);
210 }
211
212 pub(super) fn match_pressure(&mut self, reference: &str, retain: bool) -> bool {
213 self.issued.matches(reference, retain)
214 }
215
216 #[must_use]
218 pub const fn selected_schema(&self) -> ProtocolSchemaId {
219 self.selected_schema
220 }
221
222 #[must_use]
224 pub fn subscriber_pid(&self) -> Option<u64> {
225 self.resource.subscriber_pid()
226 }
227
228 pub(crate) fn try_next(&mut self) -> Option<liminal::envelope::Envelope> {
230 self.resource.try_next()
231 }
232
233 pub(crate) fn has_pending(&self) -> bool {
234 self.resource.has_pending()
235 }
236
237 pub(crate) fn is_overflowed(&self) -> bool {
239 self.resource.is_overflowed()
240 }
241
242 pub(super) fn unsubscribe(self) -> Result<(), ServerError> {
243 self.resource.unsubscribe()
244 }
245
246 pub(crate) fn try_next_positioned(
247 &mut self,
248 ) -> Result<Option<(liminal::envelope::Envelope, Option<u64>)>, ServerError> {
249 self.resource.try_next_positioned()
250 }
251
252 pub(crate) fn acknowledge_position(&self, position: u64) -> Result<(), ServerError> {
253 self.resource.acknowledge_position(position)
254 }
255
256 pub(crate) fn consumer_position(&self) -> Result<Option<u64>, ServerError> {
257 self.resource.consumer_position()
258 }
259}
260
261#[derive(Clone, Copy, Debug, PartialEq, Eq)]
267pub struct PublishOutcome {
268 pub message_id: u64,
282 pub delivered: bool,
286 pub durable_position: Option<u64>,
291 pub reach: Reach,
295}
296
297#[derive(Clone, Copy, Debug, PartialEq, Eq)]
299pub enum Reach {
300 Delivered,
302 NoSubscriber,
304 NoneAccepted,
306 Suppressed,
308}
309
310impl Reach {
311 #[must_use]
313 pub const fn of(delivery: &ChannelDelivery) -> Self {
314 if delivery.is_delivered() {
315 Self::Delivered
316 } else if delivery.matched_count() == 0 {
317 Self::NoSubscriber
318 } else {
319 Self::NoneAccepted
320 }
321 }
322}
323
324#[derive(Clone, Copy, Debug, PartialEq, Eq)]
332pub enum ChannelOperation {
333 Publish,
335 Subscribe,
337}
338
339pub trait ConnectionServices: std::fmt::Debug + Send + Sync {
341 fn construction_request(
343 &self,
344 _: Option<&dyn crate::auth_lys::session::LysSession>,
345 subject: &str,
346 _: &[u8],
347 _: u64,
348 ) -> Option<Result<serde_json::Value, ServerError>> {
349 is_site_route(subject).then(|| {
350 Err(site_services::site_error(
351 crate::services::lifecycle::site::SiteError::Invalid(site_services::NOT_CONFIGURED),
352 ))
353 })
354 }
355 fn prepare_site_trial(
360 &self,
361 _: u64,
362 _: Option<&dyn crate::auth_lys::session::LysSession>,
363 _: &liminal::protocol::WorkerRegistration,
364 _: Option<&crate::services::lifecycle::ProcessGrant>,
365 _: u64,
366 ) -> Result<Option<crate::services::lifecycle::trial::TrialPermit>, ServerError> {
367 Ok(None)
368 }
369 fn admit_site_call(
374 &self,
375 _: Option<&dyn crate::auth_lys::session::LysSession>,
376 _: &str,
377 _: u64,
378 ) -> Result<bool, ServerError> {
379 Ok(false)
380 }
381 fn site_registration_closed(&self, _: u64) -> Result<(), ServerError> {
386 Ok(())
387 }
388 fn publish_identified(
393 &self,
394 connection: u64,
395 session: Option<&dyn crate::auth_lys::session::LysSession>,
396 channel: &str,
397 envelope: &MessageEnvelope,
398 key: Option<&str>,
399 ) -> Result<PublishOutcome, ServerError> {
400 crate::auth_membership::check_agent_binding(
401 session,
402 crate::auth_lys::HotAction::Publish,
403 channel,
404 )?;
405 self.publish(connection, channel, envelope, key)
406 }
407 fn agents(&self) -> Option<&Arc<crate::agents::runtime::Agents>> {
409 None
410 }
411
412 fn subscribe_identified(
417 &self,
418 connection: u64,
419 session: Option<&dyn crate::auth_lys::session::LysSession>,
420 channel: &str,
421 schemas: &[ProtocolSchemaId],
422 install: Option<liminal::channel::InboxInstall>,
423 ) -> Result<ConnectionSubscription, ServerError> {
424 if connection == 0 {
425 return Err(crate::agents::error::AgentError::Invalid("connection id").into());
426 }
427 crate::auth_membership::check_agent_binding(
428 session,
429 crate::auth_lys::HotAction::Subscribe,
430 channel,
431 )?;
432 self.subscribe(channel, schemas, install)
433 }
434 fn participant_service(&self) -> Option<InstalledParticipantService> {
441 None
442 }
443
444 fn publish(
455 &self,
456 connection: u64,
457 channel: &str,
458 envelope: &MessageEnvelope,
459 idempotency_key: Option<&str>,
460 ) -> Result<PublishOutcome, ServerError>;
461
462 fn subscribe(
476 &self,
477 channel: &str,
478 accepted_schemas: &[ProtocolSchemaId],
479 install: Option<liminal::channel::InboxInstall>,
480 ) -> Result<ConnectionSubscription, ServerError>;
481
482 fn unsubscribe(&self, subscription: ConnectionSubscription) -> Result<(), ServerError>;
487
488 fn open_conversation(
493 &self,
494 conversation_id: u64,
495 subject: &str,
496 ) -> Result<ConnectionConversation, ServerError>;
497
498 fn conversation_message(
503 &self,
504 conversation: &ConnectionConversation,
505 envelope: &MessageEnvelope,
506 op_id: Option<u64>,
507 ) -> Result<(), ServerError>;
508
509 fn close_conversation(&self, conversation: ConnectionConversation) -> Result<(), ServerError>;
514
515 fn flush_durable_state(&self) -> Result<(), ServerError>;
520
521 fn stop_participant_warm(&self) {}
525
526 fn finish_background_store_work(&self) -> Result<(), ServerError> {
531 Ok(())
532 }
533
534 fn release_store_holders(&self) -> Result<(), ServerError> {
539 Ok(())
540 }
541
542 fn listening(self: Arc<Self>) -> Option<std::thread::JoinHandle<()>> {
547 None
548 }
549
550 fn supports_channel_operations(&self) -> bool {
563 true
564 }
565
566 fn admit_channel(
589 &self,
590 _operation: ChannelOperation,
591 _channel: &str,
592 ) -> Result<(), ChannelAccessError> {
593 Ok(())
594 }
595}
596
597#[derive(Debug)]
599pub struct LiminalConnectionServices {
600 publication_identity: Arc<publication_identity::ServerPublicationIdentity>,
601 agents: Option<Arc<crate::agents::runtime::Agents>>,
602 approval_agents: Option<Arc<crate::agents::waiting::durable::ApprovalAgents>>,
603 agent_delivery: Option<Arc<crate::agents::waiting::AgentDelivery>>,
604 site_lifecycle: OnceLock<Arc<crate::services::lifecycle::Lifecycle>>,
605 site_drafts: OnceLock<Arc<dyn SiteDraftHolder>>,
606 host_config: OnceLock<(crate::config::types::LysConfig, tokio::runtime::Handle)>,
607 draft_hook: OnceLock<std::sync::Weak<dyn crate::auth_lys::drafts::DraftDecisionHook>>,
608
609 channels: RwLock<HashMap<String, Arc<ConfiguredChannel>>>,
616 export_subscriptions:
617 Mutex<Vec<std::sync::Weak<crate::link_bridge::subscriptions::ExportSubscriptions>>>,
618 max_channels: Option<usize>,
627 cluster: ChannelCluster,
628 durable_store: Arc<dyn DurableStore>,
629 head_checkpoint_stop: Arc<liminal::durability::head_checkpoint_stop::HeadCheckpointStop>,
630 store_counts: StoreCountsHandle,
633 ephemeral_sweep: Option<EphemeralSweepCounts>,
634 signals: ServerSignals,
635 participant_service: Option<InstalledParticipantService>,
638 unloadable_conversations: Option<UnloadableConversationRecord>,
648 credential_reissuer: Option<Arc<dyn OperatorCredentialReissuer>>,
655 participant_warm: Option<ParticipantWarm>,
660 dedup: DedupCache,
668 dedup_ephemeral: DedupCache,
683 held_keys: unresolved::HeldKeys,
687 claims_start: Mutex<Option<StoreHistory>>,
691 conversation_supervisor: Arc<ConversationSupervisor>,
692 responders: Mutex<ResponderRegistry>,
700 next_subscription_id: AtomicU64,
701 console: Arc<crate::console::ConsoleHub>,
703}
704
705impl LiminalConnectionServices {
706 pub fn from_config(config: &ServerConfig) -> Result<Self, ServerError> {
721 Self::from_config_with_runtime(config, None)
722 }
723
724 pub(in crate::server) fn from_config_with_runtime(
725 config: &ServerConfig,
726 runtime: Option<tokio::runtime::Handle>,
727 ) -> Result<Self, ServerError> {
728 require_full_profile(config)?;
729 let path = required_persistence_path(config)?;
730 let history = StoreHistory::of(path);
731 let (subsystems, store) = crate::server::boot_steps::timed("store_open", || {
732 let subsystems = crate::server::boot_steps::timed("ephemeral_root_open", || {
733 RootedProductionSubsystems::new(path)
734 })?;
735 let store = subsystems.durable_store(path)?;
736 Ok::<_, ServerError>((subsystems, store))
737 })?;
738 crate::server::boot_steps::timed("channel_construction", || {
739 Self::from_config_with_store_and_runtime(config, store, history, &subsystems, runtime)
740 })
741 }
742
743 pub fn from_config_with_store(
755 config: &ServerConfig,
756 durable_store: Arc<dyn DurableStore>,
757 ) -> Result<Self, ServerError> {
758 require_full_profile(config)?;
759 #[cfg(any(test, feature = "test-support"))]
760 if config.persistence_path.is_none() {
761 return Self::from_config_with_store_via(
762 config,
763 durable_store,
764 StoreHistory::Recorded,
765 &ProductionSubsystems,
766 );
767 }
768 let subsystems = RootedProductionSubsystems::new(required_persistence_path(config)?)?;
769 Self::from_config_with_store_via(config, durable_store, StoreHistory::Recorded, &subsystems)
770 }
771
772 fn from_config_with_store_via(
783 config: &ServerConfig,
784 durable_store: Arc<dyn DurableStore>,
785 history: StoreHistory,
786 subsystems: &dyn SubsystemFactory,
787 ) -> Result<Self, ServerError> {
788 Self::from_config_with_store_and_runtime(config, durable_store, history, subsystems, None)
789 }
790
791 fn from_config_with_store_and_runtime(
792 config: &ServerConfig,
793 durable_store: Arc<dyn DurableStore>,
794 history: StoreHistory,
795 subsystems: &dyn SubsystemFactory,
796 runtime: Option<tokio::runtime::Handle>,
797 ) -> Result<Self, ServerError> {
798 let head_checkpoint_stop = Arc::new(
799 liminal::durability::head_checkpoint_stop::HeadCheckpointStop::new(durable_store),
800 );
801 let durable_store: Arc<dyn DurableStore> = head_checkpoint_stop.clone();
802 let cluster = subsystems.channel_cluster(config.cluster.as_ref())?;
807 let mut channels = HashMap::new();
808 for channel in &config.channels {
809 let resolved = resolve_channel_schema(channel);
815 let configured = build_configured_channel(
816 &channel.name,
817 resolved,
818 channel.durable,
819 ChannelOrigin::BootConfigured,
820 &durable_store,
821 cluster.supervisor(),
822 )
823 .map_err(|error| ServerError::ConfigValidation {
824 message: error.boot_message(&channel.name),
825 })?;
826 channels.insert(channel.name.clone(), Arc::new(configured));
827 }
828 let conversation_supervisor = subsystems.conversation_supervisor()?;
829 let dedup = history.delivery_dedup(Arc::clone(&durable_store));
830 let ephemeral_store = ephemeral_dedup_store(&durable_store, history, subsystems);
834 let store_counts = combined_store_counts(&durable_store, &ephemeral_store);
835 let dedup_ephemeral = DedupCache::new_empty(ephemeral_store, ephemeral_dedup_namespace());
836 let signals = ServerSignals::default();
843 let installed_participant = config
844 .participant
845 .as_ref()
846 .map(|participant| install_participant(&durable_store, participant, &signals))
847 .transpose()?;
848 let device = config
849 .cluster
850 .as_ref()
851 .map_or("local", |cluster| cluster.node_name.as_str());
852 let console = Arc::new(console_hub(
853 device,
854 &channels,
855 config.persistence_path.is_some(),
856 config.auth.as_ref().and_then(|auth| auth.lys.as_ref()),
857 )?);
858 signals.install_console(Arc::clone(&console))?;
859 cluster.observer().set_console(Arc::clone(&console))?;
860 let (participant_service, unloadable_conversations, credential_reissuer, participant_warm) =
861 installed_participant.map_or((None, None, None, None), |installed| {
862 (
863 Some(installed.service),
864 Some(installed.unloadable_conversations),
865 Some(installed.credential_reissuer),
866 Some(installed.warm),
867 )
868 });
869 let (agents, approval_agents, agent_delivery) = Self::configured_agents(
870 config,
871 Arc::clone(&durable_store),
872 cluster.supervisor().clone(),
873 &console,
874 runtime,
875 )?;
876 let services = Self {
877 channels: RwLock::new(channels),
878 export_subscriptions: Mutex::new(Vec::new()),
879 max_channels: config.limits.max_channels,
880 cluster,
881 durable_store,
882 head_checkpoint_stop,
883 store_counts,
884 ephemeral_sweep: subsystems.ephemeral_sweep_counts(),
885 signals,
886 participant_service,
887 unloadable_conversations,
888 credential_reissuer,
889 participant_warm,
890 dedup,
891 dedup_ephemeral,
892 held_keys: unresolved::HeldKeys::default(),
893 claims_start: Mutex::new(Some(history)),
897 conversation_supervisor,
898 responders: Mutex::new(HashMap::new()),
899 publication_identity: publication_identity::ServerPublicationIdentity::new()?,
900 next_subscription_id: AtomicU64::new(1),
901 console,
902 agents,
903 approval_agents,
904 agent_delivery,
905 site_lifecycle: OnceLock::new(),
906 site_drafts: OnceLock::new(),
907 host_config: OnceLock::new(),
908 draft_hook: OnceLock::new(),
909 };
910 services.install_roster_identity()?;
911 Ok(services)
912 }
913
914 #[cfg(any(test, feature = "test-support"))]
919 pub fn empty() -> Result<Self, ServerError> {
920 let conversation_supervisor = ProductionSubsystems.conversation_supervisor()?;
921 let store =
922 open_ephemeral(DEFAULT_SHARD_COUNT).map_err(|error| ServerError::ConfigValidation {
923 message: format!("failed to open test durable store: {error}"),
924 })?;
925 let head_checkpoint_stop = Arc::new(
926 liminal::durability::head_checkpoint_stop::HeadCheckpointStop::new(Arc::new(
927 CountingStore::new(store),
928 )),
929 );
930 let durable_store: Arc<dyn DurableStore> = head_checkpoint_stop.clone();
931 let store_counts = combined_store_counts(&durable_store, &durable_store);
932 let dedup = StoreHistory::Fresh.delivery_dedup(Arc::clone(&durable_store));
933 let dedup_ephemeral =
936 DedupCache::new_empty(Arc::clone(&durable_store), ephemeral_dedup_namespace());
937 let signals = ServerSignals::default();
938 let console = Arc::new(console_hub("local", &HashMap::new(), false, None)?);
939 signals.install_console(Arc::clone(&console))?;
940 let cluster = build_channel_cluster(None)?;
941 cluster.observer().set_console(Arc::clone(&console))?;
942 Ok(Self {
943 channels: RwLock::new(HashMap::new()),
944 export_subscriptions: Mutex::new(Vec::new()),
945 max_channels: None,
950 cluster,
951 durable_store,
952 head_checkpoint_stop,
953 store_counts,
954 ephemeral_sweep: None,
955 signals,
956 participant_service: None,
957 unloadable_conversations: None,
958 credential_reissuer: None,
959 participant_warm: None,
960 dedup,
961 dedup_ephemeral,
962 held_keys: unresolved::HeldKeys::default(),
963 claims_start: Mutex::new(None),
964 conversation_supervisor,
965 responders: Mutex::new(HashMap::new()),
966 publication_identity: publication_identity::ServerPublicationIdentity::new()?,
967 next_subscription_id: AtomicU64::new(1),
968 console,
969 agents: None,
970 approval_agents: None,
971 agent_delivery: None,
972 site_lifecycle: OnceLock::new(),
973 site_drafts: OnceLock::new(),
974 host_config: OnceLock::new(),
975 draft_hook: OnceLock::new(),
976 })
977 }
978
979 #[must_use]
981 pub const fn ephemeral_sweep_counts(&self) -> Option<EphemeralSweepCounts> {
982 self.ephemeral_sweep
983 }
984
985 #[must_use]
990 pub const fn channel_cluster(&self) -> &ChannelCluster {
991 &self.cluster
992 }
993
994 #[must_use]
996 pub fn durable_store(&self) -> Arc<dyn DurableStore> {
997 Arc::clone(&self.durable_store)
998 }
999
1000 #[must_use]
1002 pub fn console(&self) -> &crate::console::ConsoleHub {
1003 &self.console
1004 }
1005
1006 fn console_changed(&self, change: crate::console::TopologyChange) {
1009 self.signals.topology(change);
1010 }
1011
1012 pub fn responder_subjects(&self) -> Result<Vec<String>, ServerError> {
1018 let responders = self.lock_responders()?;
1019 let mut subjects: Vec<String> = responders.keys().cloned().collect();
1020 drop(responders);
1021 subjects.sort();
1022 Ok(subjects)
1023 }
1024
1025 pub fn finish_participant_warm(&self) -> Result<usize, ServerError> {
1033 self.participant_warm.as_ref().map_or(Ok(0), |warm| {
1034 warm.finish()
1035 .map_err(|error| ServerError::ParticipantStartupRestore {
1036 message: error.to_string(),
1037 })
1038 })
1039 }
1040
1041 #[must_use]
1047 pub fn store_counts(&self) -> StoreCountsHandle {
1048 self.store_counts.clone()
1049 }
1050
1051 #[must_use]
1059 pub fn unloadable_conversation_record(&self) -> Option<UnloadableConversationRecord> {
1060 self.unloadable_conversations.clone()
1061 }
1062
1063 #[must_use]
1072 pub fn credential_reissuer(&self) -> Option<Arc<dyn OperatorCredentialReissuer>> {
1073 self.credential_reissuer.clone()
1074 }
1075
1076 #[cfg(test)]
1083 #[must_use]
1084 pub(crate) fn with_participant_service(
1085 mut self,
1086 participant_service: InstalledParticipantService,
1087 ) -> Self {
1088 self.participant_service = Some(participant_service);
1089 self
1090 }
1091
1092 #[must_use]
1097 pub fn conversation_supervisor(&self) -> Arc<ConversationSupervisor> {
1098 Arc::clone(&self.conversation_supervisor)
1099 }
1100
1101 pub fn register_responder(
1110 &self,
1111 subject: impl Into<String>,
1112 behaviour: Arc<dyn ParticipantBehaviour>,
1113 ) -> Result<Option<Arc<dyn ParticipantBehaviour>>, ServerError> {
1114 let subject = subject.into();
1115 let mut responders = self.lock_responders()?;
1116 let previous = responders.insert(subject.clone(), behaviour);
1117 drop(responders);
1118 self.console_changed(crate::console::TopologyChange::Service {
1119 subject,
1120 present: true,
1121 });
1122 Ok(previous)
1123 }
1124
1125 pub fn unregister_responder(
1133 &self,
1134 subject: &str,
1135 ) -> Result<Option<Arc<dyn ParticipantBehaviour>>, ServerError> {
1136 let mut responders = self.lock_responders()?;
1137 let removed = responders.remove(subject);
1138 drop(responders);
1139 if removed.is_some() {
1140 self.console_changed(crate::console::TopologyChange::Service {
1141 subject: subject.to_owned(),
1142 present: false,
1143 });
1144 }
1145 Ok(removed)
1146 }
1147
1148 fn lock_responders(&self) -> Result<std::sync::MutexGuard<'_, ResponderRegistry>, ServerError> {
1151 self.responders
1152 .lock()
1153 .map_err(|_poisoned| ServerError::ListenerAccept {
1154 message: "responder registry lock poisoned".to_owned(),
1155 })
1156 }
1157
1158 fn read_channels(
1167 &self,
1168 ) -> Result<std::sync::RwLockReadGuard<'_, HashMap<String, Arc<ConfiguredChannel>>>, ServerError>
1169 {
1170 self.channels
1171 .read()
1172 .map_err(|_poisoned| ServerError::ListenerAccept {
1173 message: "channel roster lock poisoned".to_owned(),
1174 })
1175 }
1176
1177 #[cfg(test)]
1181 pub(in crate::server) fn channel_handle_for_test(
1182 &self,
1183 channel: &str,
1184 ) -> Result<ChannelHandle, ServerError> {
1185 self.read_channels()?
1186 .get(channel)
1187 .map(|configured| configured.handle.clone())
1188 .ok_or_else(|| ServerError::ListenerAccept {
1189 message: format!("channel '{channel}' is not configured"),
1190 })
1191 }
1192
1193 #[cfg(test)]
1194 pub(crate) fn subscribe_handle_for_test(
1195 &self,
1196 channel: &str,
1197 ) -> Result<liminal::channel::SubscriptionHandle, ServerError> {
1198 self.subscribe_handle_for_test_with_install(channel, None)
1199 }
1200
1201 #[cfg(test)]
1206 pub(crate) fn subscribe_handle_for_test_with_install(
1207 &self,
1208 channel: &str,
1209 install: Option<liminal::channel::InboxInstall>,
1210 ) -> Result<liminal::channel::SubscriptionHandle, ServerError> {
1211 let channels = self.read_channels()?;
1212 let configured =
1213 channels
1214 .get(channel)
1215 .map(Arc::clone)
1216 .ok_or_else(|| ServerError::ListenerAccept {
1217 message: format!("channel '{channel}' is not configured"),
1218 })?;
1219 drop(channels);
1220 install
1221 .map_or_else(
1222 || configured.handle.subscribe(),
1223 |install| configured.handle.subscribe_with_install(install),
1224 )
1225 .map_err(|error| ServerError::ListenerAccept {
1226 message: format!("liminal subscribe failed for channel '{channel}': {error}"),
1227 })
1228 }
1229
1230 fn claim_delivery(cache: &DedupCache, key: &str) -> Result<bool, ServerError> {
1237 let decision = block_on(cache.claim_or_get(key, dedup_timestamp_millis()))
1238 .map_err(|error| ServerError::ListenerAccept {
1239 message: format!("dedup bridge failed for key '{key}': {error}"),
1240 })?
1241 .map_err(|error| ServerError::ListenerAccept {
1242 message: format!("dedup claim failed for key '{key}': {error}"),
1243 })?;
1244 Ok(matches!(decision, DedupDecision::Claimed))
1245 }
1246
1247 const fn dedup_for(&self, channel: &ConfiguredChannel) -> &DedupCache {
1254 match channel.handle.config().mode {
1255 ChannelMode::Durable => &self.dedup,
1256 ChannelMode::Ephemeral => &self.dedup_ephemeral,
1257 }
1258 }
1259
1260 fn release_claim(cache: &DedupCache, key: &str) {
1268 match block_on(cache.release_claim(key)) {
1269 Ok(Ok(())) => {}
1270 Ok(Err(error)) => {
1271 tracing::error!(
1272 idempotency_key = key,
1273 %error,
1274 "failed to release dedup claim after publish failure; key may stay suppressed"
1275 );
1276 }
1277 Err(error) => {
1278 tracing::error!(
1279 idempotency_key = key,
1280 %error,
1281 "dedup release bridge failed after publish failure; key may stay suppressed"
1282 );
1283 }
1284 }
1285 }
1286
1287 fn next_message_id(&self) -> Result<u64, ServerError> {
1289 self.publication_identity
1290 .next()
1291 .map(|message| message.id)
1292 .map_err(|error| ServerError::ListenerAccept {
1293 message: error.to_string(),
1294 })
1295 }
1296
1297 fn delivered(delivery: &ChannelDelivery) -> Result<PublishOutcome, ServerError> {
1301 Self::record_delivery_counts(delivery);
1302 let message = delivery
1303 .delivery_id()
1304 .ok_or_else(|| ServerError::ListenerAccept {
1305 message: "publication identity missing after admission".into(),
1306 })?;
1307 Ok(PublishOutcome {
1308 message_id: message.id,
1309 delivered: delivery.is_delivered(),
1310 durable_position: delivery.durable_position(),
1311 reach: Reach::of(delivery),
1312 })
1313 }
1314}
1315
1316#[path = "deferred_store.rs"]
1317mod deferred_store;
1318#[path = "unresolved.rs"]
1319mod unresolved;
1320#[path = "unresolved_start.rs"]
1321mod unresolved_start;
1322#[cfg(test)]
1323#[path = "unresolved_start_tests.rs"]
1324mod unresolved_start_tests;
1325
1326use deferred_store::{DeferredStore, ThrowawayStoreOpener};
1327use unresolved_start::StoreHistory;
1328
1329impl LiminalConnectionServices {
1355 pub fn register_channel(
1384 &self,
1385 spec: &ChannelRegistration,
1386 ) -> Result<Registered, ChannelRegistryError> {
1387 let Some(limit) = self.max_channels else {
1391 return Err(ChannelRegistryError::CapNotConfigured {
1392 cap: MAX_CHANNELS_KEY,
1393 });
1394 };
1395 let resolved = resolve_schema_bytes(spec.schema_bytes.as_deref()).map_err(|error| {
1399 ChannelRegistryError::SchemaRejected {
1400 name: spec.name.clone(),
1401 message: error.to_string(),
1402 }
1403 })?;
1404
1405 let (existing, registered_count) = {
1410 let channels = self.read_roster()?;
1411 let existing = channels.get(&spec.name).map(Arc::clone);
1412 let registered_count = runtime_registered_count(&channels);
1413 drop(channels);
1414 (existing, registered_count)
1415 };
1416 if let Some(existing) = existing {
1417 return compare_registration(&existing, spec, &resolved);
1418 }
1419 if registered_count >= limit {
1420 return Err(ChannelRegistryError::CapReached {
1421 cap: MAX_CHANNELS_KEY,
1422 limit,
1423 });
1424 }
1425
1426 let configured = build_configured_channel(
1431 &spec.name,
1432 resolved,
1433 spec.durable,
1434 ChannelOrigin::RuntimeRegistered,
1435 &self.durable_store,
1436 self.cluster.supervisor(),
1437 )
1438 .map_err(|error| error.into_registry_error(&spec.name))?;
1439
1440 let mut exporters = self.export_subscriptions.lock().map_err(|_| {
1444 ChannelRegistryError::RosterUnavailable {
1445 message: "export subscription registry lock poisoned".to_owned(),
1446 }
1447 })?;
1448 exporters.retain(|exporter| exporter.strong_count() > 0);
1449 let mut channels = self.write_roster()?;
1450 let raced = channels.get(&spec.name).map(Arc::clone);
1451 let registered_count = runtime_registered_count(&channels);
1452 if raced.is_none() && registered_count < limit {
1453 let mut prepared = Vec::new();
1454 for exporter in exporters.iter().filter_map(std::sync::Weak::upgrade) {
1455 if let Some(binding) = exporter.binding(&spec.name) {
1456 let refused = |error: ServerError| ChannelRegistryError::RosterUnavailable {
1457 message: format!(
1458 "export '{}' cannot subscribe to '{}': {error}",
1459 exporter.link(),
1460 spec.name
1461 ),
1462 };
1463 let subscription = self
1464 .build_subscription(
1465 &spec.name,
1466 &configured.handle,
1467 configured.protocol_schema,
1468 Some(binding.install),
1469 Some(&binding.consumer),
1470 )
1471 .map_err(refused)?;
1472 let subscription =
1473 crate::link_bridge::subscriptions::PreparedSubscription::new(
1474 binding.prefix,
1475 spec.name.clone(),
1476 subscription,
1477 )
1478 .map_err(refused)?;
1479 prepared.push((exporter, subscription));
1480 }
1481 }
1482 for (exporter, subscription) in prepared {
1484 exporter.attach(subscription);
1485 }
1486 self.identify_registered(&configured)?;
1487 channels.insert(spec.name.clone(), Arc::new(configured));
1488 drop(channels);
1489 drop(exporters);
1490 self.console_changed(crate::console::TopologyChange::Channel {
1491 name: spec.name.clone(),
1492 life: line_life(spec.durable),
1493 state: crate::console::records::LineState::Live,
1494 });
1495 return Ok(Registered::Created);
1496 }
1497 drop(channels);
1498 drop(exporters);
1499 if let Some(raced) = raced {
1500 return compare_registration(&raced, spec, &schema_of(&configured));
1506 }
1507 Err(ChannelRegistryError::CapReached {
1508 cap: MAX_CHANNELS_KEY,
1509 limit,
1510 })
1511 }
1512
1513 pub fn quiesce_channel(
1535 &self,
1536 name: &str,
1537 reason: impl Into<String>,
1538 ) -> Result<(), ChannelRegistryError> {
1539 let reason = reason.into();
1540 let configured = {
1543 let channels = self.read_roster()?;
1544 channels.get(name).map(Arc::clone).ok_or_else(|| {
1545 ChannelRegistryError::NotRegistered {
1546 name: name.to_owned(),
1547 }
1548 })?
1549 };
1550 configured.quiesce(name, &reason)?;
1551 self.console_changed(crate::console::TopologyChange::Channel {
1552 name: name.to_owned(),
1553 life: line_life(configured.durable),
1554 state: crate::console::records::LineState::Quiesced,
1555 });
1556 Ok(())
1557 }
1558
1559 pub fn channel_status(&self, name: &str) -> Result<ChannelStatus, ChannelRegistryError> {
1571 let configured = {
1572 let channels = self.read_roster()?;
1573 channels.get(name).map(Arc::clone)
1574 };
1575 Ok(
1576 configured.map_or(ChannelStatus::NotRegistered, |configured| {
1577 status_of(&configured)
1578 }),
1579 )
1580 }
1581
1582 pub fn channel_census(&self) -> Result<Vec<(String, ChannelStatus)>, ChannelRegistryError> {
1589 let mut census: Vec<(String, ChannelStatus)> = self
1590 .read_roster()?
1591 .iter()
1592 .map(|(name, configured)| (name.clone(), status_of(configured)))
1593 .collect();
1594 census.sort_by(|left, right| left.0.cmp(&right.0));
1595 Ok(census)
1596 }
1597
1598 pub fn registered_channels(&self) -> Result<Vec<ChannelDescriptor>, ChannelRegistryError> {
1609 let entries: Vec<(String, Arc<ConfiguredChannel>)> = {
1610 let channels = self.read_roster()?;
1611 channels
1612 .iter()
1613 .map(|(name, configured)| (name.clone(), Arc::clone(configured)))
1614 .collect()
1615 };
1616 let mut descriptors: Vec<ChannelDescriptor> = entries
1617 .into_iter()
1618 .map(|(name, configured)| ChannelDescriptor {
1619 name,
1620 origin: configured.origin,
1621 state: configured.state(),
1622 })
1623 .collect();
1624 descriptors.sort_by(|left, right| left.name.cmp(&right.name));
1625 Ok(descriptors)
1626 }
1627
1628 fn admit_channel(&self, channel: &str) -> Result<Arc<ConfiguredChannel>, ChannelAccessError> {
1642 let configured = {
1643 let channels = self.channels.read().map_err(|_poisoned| {
1644 ChannelAccessError::RosterUnavailable {
1645 message: ROSTER_POISONED.to_owned(),
1646 }
1647 })?;
1648 channels.get(channel).map(Arc::clone)
1649 };
1650 let configured = configured.ok_or_else(|| ChannelAccessError::NotRegistered {
1651 name: channel.to_owned(),
1652 })?;
1653 if configured.state.load(Ordering::Acquire) == STATE_QUIESCED {
1654 return Err(ChannelAccessError::Quiesced {
1655 name: channel.to_owned(),
1656 reason: configured.recorded_quiesce_reason(),
1657 });
1658 }
1659 Ok(configured)
1660 }
1661
1662 fn read_roster(
1669 &self,
1670 ) -> Result<
1671 std::sync::RwLockReadGuard<'_, HashMap<String, Arc<ConfiguredChannel>>>,
1672 ChannelRegistryError,
1673 > {
1674 #[cfg(test)]
1675 ROSTER_READS.with(|reads| reads.set(reads.get() + 1));
1676 self.channels
1677 .read()
1678 .map_err(|_poisoned| ChannelRegistryError::RosterUnavailable {
1679 message: ROSTER_POISONED.to_owned(),
1680 })
1681 }
1682
1683 fn write_roster(
1686 &self,
1687 ) -> Result<
1688 std::sync::RwLockWriteGuard<'_, HashMap<String, Arc<ConfiguredChannel>>>,
1689 ChannelRegistryError,
1690 > {
1691 self.channels
1692 .write()
1693 .map_err(|_poisoned| ChannelRegistryError::RosterUnavailable {
1694 message: ROSTER_POISONED.to_owned(),
1695 })
1696 }
1697
1698 fn give_back_tap(&self, channel: &str) {
1702 self.console.release_tap(channel, |subject| {
1703 crate::console::ServerPublisher::retire_console_channel(self, subject)
1704 });
1705 }
1706
1707 fn open_subscription(
1711 &self,
1712 channel: &str,
1713 accepted_schemas: &[ProtocolSchemaId],
1714 install: Option<liminal::channel::InboxInstall>,
1715 consumer: Option<&str>,
1716 ) -> Result<ConnectionSubscription, ServerError> {
1717 refuse_console_tap_target(channel).map_err(|error| access_to_server_error(&error))?;
1720 let configured = self
1726 .admit_channel(channel)
1727 .map_err(|error| access_to_server_error(&error))?;
1728 let selected_schema = if accepted_schemas.is_empty() {
1729 configured.protocol_schema
1730 } else {
1731 liminal::protocol::negotiate_schema(configured.protocol_schema, accepted_schemas)
1732 .map_err(|error| server_error_from_protocol(&error))?
1733 };
1734 self.build_subscription(
1735 channel,
1736 &configured.handle,
1737 selected_schema,
1738 install,
1739 consumer,
1740 )
1741 }
1742
1743 fn build_subscription(
1744 &self,
1745 channel: &str,
1746 handle: &ChannelHandle,
1747 selected_schema: ProtocolSchemaId,
1748 install: Option<liminal::channel::InboxInstall>,
1749 consumer: Option<&str>,
1750 ) -> Result<ConnectionSubscription, ServerError> {
1751 let subscription = match (consumer, install) {
1756 (Some(consumer), Some(install)) => handle.subscribe_consumer(consumer, install),
1757 (Some(_), None) => {
1758 return Err(ServerError::ListenerAccept {
1759 message: "a durable consumer requires a bounded inbox install".to_owned(),
1760 });
1761 }
1762 (None, Some(install)) => handle.subscribe_with_install(install),
1763 (None, None) => handle.subscribe(),
1764 }
1765 .map_err(|error| ServerError::ListenerAccept {
1766 message: format!("liminal subscribe failed for channel '{channel}': {error}"),
1767 })?;
1768 let id = self.next_subscription_id.fetch_add(1, Ordering::Relaxed);
1769 let mut connection_subscription = ConnectionSubscription::new(
1770 id,
1771 selected_schema,
1772 Box::new(LiminalSubscriptionResource { subscription }),
1773 );
1774 connection_subscription.set_channel(channel.to_owned());
1775 Ok(connection_subscription)
1779 }
1780
1781 pub(crate) fn subscribe_consumer(
1782 &self,
1783 channel: &str,
1784 consumer: &str,
1785 install: liminal::channel::InboxInstall,
1786 ) -> Result<ConnectionSubscription, ServerError> {
1787 self.open_subscription(channel, &[], Some(install), Some(consumer))
1788 }
1789
1790 pub(crate) fn install_exporter(
1791 &self,
1792 exporter: &Arc<crate::link_bridge::subscriptions::ExportSubscriptions>,
1793 ) -> Result<(), ServerError> {
1794 let mut exporters =
1795 self.export_subscriptions
1796 .lock()
1797 .map_err(|_| ServerError::ListenerAccept {
1798 message: "export subscription registry lock poisoned".to_owned(),
1799 })?;
1800 exporters.retain(|exporter| exporter.strong_count() > 0);
1801 if exporters
1802 .iter()
1803 .filter_map(std::sync::Weak::upgrade)
1804 .any(|existing| existing.link() == exporter.link())
1805 {
1806 return Err(ServerError::ListenerAccept {
1807 message: format!(
1808 "link '{}' already owns an export subscription",
1809 exporter.link()
1810 ),
1811 });
1812 }
1813 let mut prepared = Vec::new();
1814 for channel in self
1815 .registered_channels()
1816 .map_err(|error| ServerError::ListenerAccept {
1817 message: error.to_string(),
1818 })?
1819 {
1820 if matches!(channel.state, ChannelState::Active) {
1821 if let Some(binding) = exporter.binding(&channel.name) {
1822 let subscription =
1823 self.subscribe_consumer(&channel.name, &binding.consumer, binding.install)?;
1824 prepared.push(
1825 crate::link_bridge::subscriptions::PreparedSubscription::new(
1826 binding.prefix,
1827 channel.name,
1828 subscription,
1829 )?,
1830 );
1831 }
1832 }
1833 }
1834 for subscription in prepared {
1835 exporter.attach(subscription);
1836 }
1837 exporters.push(Arc::downgrade(exporter));
1838 drop(exporters);
1839 Ok(())
1840 }
1841
1842 fn refuse_console_tap_limit(&self, channel: &str) -> Result<(), ChannelAccessError> {
1853 match self.console.reserve_tap(channel) {
1854 Ok(false) => Err(ChannelAccessError::ConsoleTapLimit {
1855 name: channel.to_owned(),
1856 limit: crate::console::MAX_LIVE_TAPS,
1857 }),
1858 Ok(true) | Err(_) => Ok(()),
1859 }
1860 }
1861}
1862
1863impl crate::console::ServerPublisher for LiminalConnectionServices {
1864 fn publish_console(
1868 &self,
1869 subject: &str,
1870 payload: &[u8],
1871 ) -> Result<(), crate::console::ConsoleError> {
1872 let payload = self.agent_topology_payload(subject, payload)?;
1873 let payload = if subject == crate::console::TOPOLOGY_SUBJECT {
1874 if let Some(lifecycle) = self.site_lifecycle.get() {
1875 std::borrow::Cow::Owned(
1876 crate::console::topology::with_sites(
1877 &payload,
1878 lifecycle,
1879 self.console.device(),
1880 )
1881 .map_err(|error| crate::console::ConsoleError::Server {
1882 subject: subject.into(),
1883 source: site_services::site_error(error),
1884 })?,
1885 )
1886 } else {
1887 payload
1888 }
1889 } else {
1890 payload
1891 };
1892 let envelope = MessageEnvelope::new(
1893 ProtocolSchemaId::new([0_u8; ProtocolSchemaId::WIRE_LEN]),
1894 liminal::protocol::CausalContext::independent(),
1895 payload.to_vec(),
1896 );
1897 self.publish_with_origin(
1898 subject,
1899 &envelope,
1900 None,
1901 PublishOrigin::Server {
1902 device: self.console.device(),
1903 },
1904 )
1905 .map(|_published| ())
1906 .map_err(|source| crate::console::ConsoleError::Server {
1907 subject: subject.to_owned(),
1908 source,
1909 })
1910 }
1911
1912 fn retire_console_channel(&self, subject: &str) -> Result<(), crate::console::ConsoleError> {
1915 if !super::channel_registry::is_console_subject(subject) {
1916 return Ok(());
1917 }
1918 let mut channels =
1919 self.write_roster()
1920 .map_err(|error| crate::console::ConsoleError::Server {
1921 subject: subject.to_owned(),
1922 source: ServerError::ListenerAccept {
1923 message: error.to_string(),
1924 },
1925 })?;
1926 channels.remove(subject);
1927 drop(channels);
1928 Ok(())
1929 }
1930}
1931
1932struct InstalledParticipant {
1936 service: InstalledParticipantService,
1937 unloadable_conversations: UnloadableConversationRecord,
1938 credential_reissuer: Arc<dyn OperatorCredentialReissuer>,
1939 warm: ParticipantWarm,
1940}
1941
1942fn install_participant(
1952 durable_store: &Arc<dyn DurableStore>,
1953 participant: &ParticipantConfig,
1954 signals: &ServerSignals,
1955) -> Result<InstalledParticipant, ServerError> {
1956 let handler = ProductionParticipantHandler::new(Arc::clone(durable_store), *participant)
1957 .map_err(|error| ServerError::ParticipantStartupRestore {
1958 message: error.to_string(),
1959 })?;
1960 handler.install_signals(signals.clone()).map_err(|error| {
1966 ServerError::ParticipantStartupRestore {
1967 message: error.to_string(),
1968 }
1969 })?;
1970 let unloadable_conversations = handler.unloadable_record();
1971 let handler = Arc::new(handler);
1972 let warm = handler
1973 .install_warm()
1974 .map_err(|error| ServerError::ParticipantStartupRestore {
1975 message: error.to_string(),
1976 })?;
1977 let credential_reissuer: Arc<dyn OperatorCredentialReissuer> =
1981 Arc::<ProductionParticipantHandler>::clone(&handler);
1982 let service = InstalledParticipantService::new(
1983 handler,
1984 Arc::clone(durable_store),
1985 participant.wire_frame_limit,
1986 )
1987 .map_err(|error| ServerError::ConfigValidation {
1988 message: format!(
1989 "participant.wire_frame_limit: {} is below the protocol's minimum \
1990 complete participant frame ({error:?})",
1991 participant.wire_frame_limit
1992 ),
1993 })?;
1994 Ok(InstalledParticipant {
1995 service,
1996 unloadable_conversations,
1997 credential_reissuer,
1998 warm,
1999 })
2000}
2001
2002fn console_hub(
2009 device: &str,
2010 channels: &HashMap<String, Arc<ConfiguredChannel>>,
2011 depot: bool,
2012 lys: Option<&crate::config::types::LysConfig>,
2013) -> Result<crate::console::ConsoleHub, ServerError> {
2014 let this = crate::console::ThisDevice {
2015 id: device.to_owned(),
2016 liminal: env!("CARGO_PKG_VERSION").to_owned(),
2017 started: Instant::now(),
2018 depot,
2019 };
2020 let lines = channels.iter().map(|(name, configured)| {
2021 let state = match configured.state() {
2022 ChannelState::Active => crate::console::records::LineState::Live,
2023 ChannelState::Quiesced { .. } => crate::console::records::LineState::Quiesced,
2024 };
2025 (name.clone(), line_life(configured.durable), state)
2026 });
2027 let mut model = crate::console::TopologyModel::new(this, [], lines, [], []);
2028 if let Some(machine) = lys.and_then(|lys| lys.machine.as_ref()) {
2029 model = model.with_machine(machine);
2030 }
2031 crate::console::wall::WallBase::now()
2032 .and_then(|wall| crate::console::ConsoleHub::new(device, model, wall))
2033 .map_err(|error| ServerError::ListenerAccept {
2034 message: format!("console hub: {error}"),
2035 })
2036}
2037
2038fn status_of(configured: &ConfiguredChannel) -> ChannelStatus {
2041 let mode = configured.handle.config().mode;
2042 match configured.state() {
2043 ChannelState::Active => ChannelStatus::Active {
2044 origin: configured.origin,
2045 mode,
2046 schema: configured.protocol_schema,
2047 },
2048 ChannelState::Quiesced { reason } => ChannelStatus::Quiesced {
2049 reason,
2050 origin: configured.origin,
2051 mode,
2052 },
2053 }
2054}
2055
2056#[cfg(test)]
2057thread_local! {
2058 static ROSTER_READS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
2060}
2061
2062#[cfg(test)]
2064pub(crate) fn roster_reads() -> u64 {
2065 ROSTER_READS.with(std::cell::Cell::get)
2066}
2067
2068const fn line_life(durable: bool) -> crate::console::records::LineLife {
2070 if durable {
2071 crate::console::records::LineLife::Kept
2072 } else {
2073 crate::console::records::LineLife::Passing
2074 }
2075}
2076
2077const ROSTER_POISONED: &str = "channel roster lock poisoned";
2080
2081fn runtime_registered_count(channels: &HashMap<String, Arc<ConfiguredChannel>>) -> usize {
2089 channels
2090 .values()
2091 .filter(|configured| configured.origin == ChannelOrigin::RuntimeRegistered)
2092 .count()
2093}
2094
2095fn compare_registration(
2115 existing: &ConfiguredChannel,
2116 spec: &ChannelRegistration,
2117 resolved: &ChannelSchema,
2118) -> Result<Registered, ChannelRegistryError> {
2119 let requested_mode = if spec.durable {
2120 ChannelMode::Durable
2121 } else {
2122 ChannelMode::Ephemeral
2123 };
2124 let config = existing.handle.config();
2125 let mismatch = if config.mode == requested_mode {
2126 if existing.protocol_schema == resolved.protocol_id {
2127 if *config.schema.definition() == resolved.document {
2128 None
2129 } else {
2130 Some(ChannelConfigField::SchemaDocument)
2131 }
2132 } else {
2133 Some(ChannelConfigField::SchemaId)
2134 }
2135 } else {
2136 Some(ChannelConfigField::Mode)
2137 };
2138 mismatch.map_or(Ok(Registered::AlreadyIdentical), |field| {
2139 Err(ChannelRegistryError::AlreadyRegistered {
2140 name: spec.name.clone(),
2141 field,
2142 })
2143 })
2144}
2145
2146fn schema_of(configured: &ConfiguredChannel) -> ChannelSchema {
2150 ChannelSchema {
2151 document: configured.handle.config().schema.definition().clone(),
2152 protocol_id: configured.protocol_schema,
2153 }
2154}
2155
2156fn build_configured_channel(
2180 name: &str,
2181 resolved: ChannelSchema,
2182 durable: bool,
2183 origin: ChannelOrigin,
2184 durable_store: &Arc<dyn DurableStore>,
2185 supervisor: &ChannelSupervisor,
2186) -> Result<ConfiguredChannel, ChannelBuildError> {
2187 let schema =
2188 Schema::new(resolved.document).map_err(|error| ChannelBuildError::SchemaRejected {
2189 message: error.to_string(),
2190 })?;
2191 let channel_config = if durable {
2192 ChannelConfig::new(name.to_owned(), schema, ChannelMode::Durable)
2193 } else {
2194 ChannelConfig::new(name.to_owned(), schema, ChannelMode::Ephemeral)
2195 };
2196 let handle = if durable {
2197 ChannelHandle::new_durable_with_supervisor(
2198 channel_config,
2199 Arc::clone(durable_store),
2200 supervisor.clone(),
2201 )
2202 .map_err(|error| ChannelBuildError::DurableInitFailed {
2203 message: error.to_string(),
2204 })?
2205 } else {
2206 ChannelHandle::with_supervisor(channel_config, supervisor.clone())
2207 };
2208 Ok(ConfiguredChannel {
2209 handle,
2210 protocol_schema: resolved.protocol_id,
2211 durable,
2212 origin,
2213 state: AtomicU8::new(STATE_ACTIVE),
2214 quiesce_reason: OnceLock::new(),
2215 })
2216}
2217
2218fn dedup_timestamp_millis() -> u64 {
2224 use std::time::{SystemTime, UNIX_EPOCH};
2225 SystemTime::now()
2226 .duration_since(UNIX_EPOCH)
2227 .ok()
2228 .and_then(|duration| u64::try_from(duration.as_millis()).ok())
2229 .unwrap_or(0)
2230}
2231
2232const DEFAULT_SHARD_COUNT: usize = 8;
2239
2240const THROWAWAY_SHARD_COUNT: usize = 1;
2243
2244const DELIVERY_DEDUP_NAMESPACE: &str = "liminal:delivery-dedup";
2250
2251fn ephemeral_dedup_namespace() -> String {
2264 use std::time::{SystemTime, UNIX_EPOCH};
2265 let boot_nanos = SystemTime::now()
2266 .duration_since(UNIX_EPOCH)
2267 .map_or(0, |since| since.as_nanos());
2268 format!(
2269 "{DELIVERY_DEDUP_NAMESPACE}:incarnation:{}-{boot_nanos}",
2270 std::process::id()
2271 )
2272}
2273
2274fn combined_store_counts(
2277 durable_store: &Arc<dyn DurableStore>,
2278 ephemeral_store: &Arc<dyn DurableStore>,
2279) -> StoreCountsHandle {
2280 let durable = durable_store.store_counts().unwrap_or_default();
2281 if Arc::ptr_eq(durable_store, ephemeral_store) {
2282 return durable;
2283 }
2284 durable.with(&ephemeral_store.store_counts().unwrap_or_default())
2285}
2286
2287fn ephemeral_dedup_store(
2296 durable_store: &Arc<dyn DurableStore>,
2297 history: StoreHistory,
2298 subsystems: &dyn SubsystemFactory,
2299) -> Arc<dyn DurableStore> {
2300 match (history, durable_store) {
2301 #[cfg(any(test, feature = "test-support"))]
2302 (StoreHistory::Fresh, store) => Arc::clone(store),
2303 (StoreHistory::Created | StoreHistory::Recorded, _) => Arc::new(CountingStore::new(
2304 DeferredStore::new(subsystems.throwaway_store()),
2305 )),
2306 }
2307}
2308
2309#[cfg(test)]
2314#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
2315pub(super) enum SchedulerSubsystem {
2316 ChannelSupervisor,
2318 ConversationSupervisor,
2320 HaematiteStore,
2322}
2323
2324pub(super) trait SubsystemFactory {
2337 fn channel_cluster(
2343 &self,
2344 cluster_config: Option<&ClusterConfig>,
2345 ) -> Result<ChannelCluster, ServerError>;
2346
2347 fn conversation_supervisor(&self) -> Result<Arc<ConversationSupervisor>, ServerError>;
2353
2354 fn durable_store(&self, persistence_path: &Path) -> Result<Arc<dyn DurableStore>, ServerError>;
2359
2360 fn throwaway_store(&self) -> ThrowawayStoreOpener;
2364
2365 fn ephemeral_sweep_counts(&self) -> Option<EphemeralSweepCounts> {
2366 None
2367 }
2368}
2369
2370#[cfg(any(test, feature = "test-support"))]
2372pub(super) struct ProductionSubsystems;
2373
2374#[cfg(any(test, feature = "test-support"))]
2375impl SubsystemFactory for ProductionSubsystems {
2376 fn channel_cluster(
2377 &self,
2378 cluster_config: Option<&ClusterConfig>,
2379 ) -> Result<ChannelCluster, ServerError> {
2380 build_channel_cluster(cluster_config)
2381 }
2382
2383 fn conversation_supervisor(&self) -> Result<Arc<ConversationSupervisor>, ServerError> {
2384 Ok(Arc::new(ConversationSupervisor::new().map_err(
2385 |error| ServerError::ConfigValidation {
2386 message: format!("failed to start conversation supervisor: {error}"),
2387 },
2388 )?))
2389 }
2390
2391 fn durable_store(&self, persistence_path: &Path) -> Result<Arc<dyn DurableStore>, ServerError> {
2392 build_durable_store(persistence_path)
2393 }
2394
2395 fn throwaway_store(&self) -> ThrowawayStoreOpener {
2396 Box::new(|| open_ephemeral(THROWAWAY_SHARD_COUNT))
2397 }
2398}
2399
2400struct RootedProductionSubsystems {
2401 root: Arc<EphemeralRoot>,
2402}
2403
2404impl RootedProductionSubsystems {
2405 fn new(path: &Path) -> Result<Self, ServerError> {
2406 let root = EphemeralRoot::open(path).map_err(|error| ServerError::ConfigValidation {
2407 message: error.to_string(),
2408 })?;
2409 Ok(Self { root })
2410 }
2411}
2412
2413impl SubsystemFactory for RootedProductionSubsystems {
2414 fn channel_cluster(
2415 &self,
2416 config: Option<&ClusterConfig>,
2417 ) -> Result<ChannelCluster, ServerError> {
2418 build_channel_cluster(config)
2419 }
2420
2421 fn conversation_supervisor(&self) -> Result<Arc<ConversationSupervisor>, ServerError> {
2422 Ok(Arc::new(ConversationSupervisor::new().map_err(
2423 |error| ServerError::ConfigValidation {
2424 message: format!("failed to start conversation supervisor: {error}"),
2425 },
2426 )?))
2427 }
2428
2429 fn durable_store(&self, path: &Path) -> Result<Arc<dyn DurableStore>, ServerError> {
2430 build_durable_store(path)
2431 }
2432
2433 fn throwaway_store(&self) -> ThrowawayStoreOpener {
2434 let root = Arc::clone(&self.root);
2435 Box::new(move || root.open_store(THROWAWAY_SHARD_COUNT))
2436 }
2437
2438 fn ephemeral_sweep_counts(&self) -> Option<EphemeralSweepCounts> {
2439 Some(self.root.sweep_counts())
2440 }
2441}
2442
2443#[cfg(test)]
2447pub(super) mod subsystem_census {
2448 use std::path::{Path, PathBuf};
2449 use std::sync::Arc;
2450 use std::sync::atomic::{AtomicUsize, Ordering};
2451
2452 use liminal::conversation::ConversationSupervisor;
2453 use liminal::durability::{DurableStore, open_ephemeral_rooted};
2454
2455 use super::{
2456 ChannelCluster, ProductionSubsystems, SchedulerSubsystem, SubsystemFactory,
2457 THROWAWAY_SHARD_COUNT, ThrowawayStoreOpener, build_durable_store,
2458 };
2459 use crate::ServerError;
2460 use crate::config::types::ClusterConfig;
2461
2462 pub struct RecordingSubsystems {
2470 census: [AtomicUsize; 3],
2472 ephemeral_root: PathBuf,
2473 }
2474
2475 impl RecordingSubsystems {
2476 pub fn rooted(ephemeral_root: &Path) -> Self {
2478 Self {
2479 census: [
2480 AtomicUsize::new(0),
2481 AtomicUsize::new(0),
2482 AtomicUsize::new(0),
2483 ],
2484 ephemeral_root: ephemeral_root.to_path_buf(),
2485 }
2486 }
2487
2488 pub fn recorded(&self) -> Vec<SchedulerSubsystem> {
2491 KINDS
2492 .iter()
2493 .zip(&self.census)
2494 .flat_map(|(kind, count)| std::iter::repeat_n(*kind, count.load(Ordering::SeqCst)))
2495 .collect()
2496 }
2497
2498 fn record(&self, subsystem: SchedulerSubsystem) {
2499 if let Some(count) = KINDS
2500 .iter()
2501 .position(|kind| *kind == subsystem)
2502 .and_then(|index| self.census.get(index))
2503 {
2504 count.fetch_add(1, Ordering::SeqCst);
2505 }
2506 }
2507 }
2508
2509 const KINDS: [SchedulerSubsystem; 3] = [
2511 SchedulerSubsystem::ChannelSupervisor,
2512 SchedulerSubsystem::ConversationSupervisor,
2513 SchedulerSubsystem::HaematiteStore,
2514 ];
2515
2516 impl SubsystemFactory for RecordingSubsystems {
2517 fn channel_cluster(
2518 &self,
2519 cluster_config: Option<&ClusterConfig>,
2520 ) -> Result<ChannelCluster, ServerError> {
2521 let cluster = ProductionSubsystems.channel_cluster(cluster_config)?;
2522 self.record(SchedulerSubsystem::ChannelSupervisor);
2523 Ok(cluster)
2524 }
2525
2526 fn conversation_supervisor(&self) -> Result<Arc<ConversationSupervisor>, ServerError> {
2527 let supervisor = ProductionSubsystems.conversation_supervisor()?;
2528 self.record(SchedulerSubsystem::ConversationSupervisor);
2529 Ok(supervisor)
2530 }
2531
2532 fn durable_store(
2533 &self,
2534 persistence_path: &Path,
2535 ) -> Result<Arc<dyn DurableStore>, ServerError> {
2536 let store = build_durable_store(persistence_path)?;
2537 self.record(SchedulerSubsystem::HaematiteStore);
2538 Ok(store)
2539 }
2540
2541 fn throwaway_store(&self) -> ThrowawayStoreOpener {
2542 let root = self.ephemeral_root.clone();
2543 Box::new(move || open_ephemeral_rooted(&root, THROWAWAY_SHARD_COUNT))
2544 }
2545 }
2546}
2547
2548fn require_full_profile(config: &ServerConfig) -> Result<(), ServerError> {
2555 match config.services.profile()? {
2556 ServiceProfile::Full => Ok(()),
2557 ServiceProfile::WorkerFrontDoor => Err(ServerError::ConfigValidation {
2558 message: format!(
2559 "services.profile: \"{}\" cannot construct the full LiminalConnectionServices; \
2560 build profile-selected services via build_connection_services",
2561 ServiceProfile::WORKER_FRONT_DOOR
2562 ),
2563 }),
2564 }
2565}
2566
2567pub fn build_connection_services(
2584 config: &ServerConfig,
2585) -> Result<Arc<dyn ConnectionServices>, ServerError> {
2586 match config.services.profile()? {
2587 ServiceProfile::Full => {
2588 let subsystems = RootedProductionSubsystems::new(required_persistence_path(config)?)?;
2589 build_connection_services_via(config, &subsystems)
2590 }
2591 ServiceProfile::WorkerFrontDoor => build_worker_front_door(config),
2592 }
2593}
2594
2595pub(super) fn build_connection_services_via(
2611 config: &ServerConfig,
2612 subsystems: &dyn SubsystemFactory,
2613) -> Result<Arc<dyn ConnectionServices>, ServerError> {
2614 match config.services.profile()? {
2615 ServiceProfile::Full => {
2616 let path = required_persistence_path(config)?;
2617 let history = StoreHistory::of(path);
2618 let store = subsystems.durable_store(path)?;
2619 Ok(Arc::new(
2620 LiminalConnectionServices::from_config_with_store_via(
2621 config, store, history, subsystems,
2622 )?,
2623 ))
2624 }
2625 ServiceProfile::WorkerFrontDoor => build_worker_front_door(config),
2626 }
2627}
2628
2629fn build_worker_front_door(
2630 config: &ServerConfig,
2631) -> Result<Arc<dyn ConnectionServices>, ServerError> {
2632 let errors = crate::config::validation::worker_front_door_field_errors(config);
2633 if !errors.is_empty() {
2634 return Err(ServerError::ConfigValidation {
2635 message: errors.join("; "),
2636 });
2637 }
2638 Ok(Arc::new(WorkerFrontDoorServices::new()))
2639}
2640
2641pub(crate) fn open_persistent_store(
2643 persistence_path: &Path,
2644) -> Result<Arc<dyn DurableStore>, ServerError> {
2645 build_durable_store(persistence_path)
2646}
2647
2648const SPINE_COMMIT_THRESHOLD_BYTES: NonZeroU64 = match NonZeroU64::new(1 << 20) {
2661 Some(threshold) => threshold,
2662 None => unreachable!(),
2663};
2664
2665const SPINE_COMMIT_POLICY: CommitPolicy = CommitPolicy::buffered(SPINE_COMMIT_THRESHOLD_BYTES);
2667
2668fn build_durable_store(path: &Path) -> Result<Arc<dyn DurableStore>, ServerError> {
2669 let data_dir = durable_data_dir(path);
2670 let database = open_or_create_database(&data_dir)?;
2671 Ok(crate::server::boot_steps::timed("event_store_wrap", || {
2672 let event_store = EventStore::with_commit_policy(database, SPINE_COMMIT_POLICY);
2673 let store: Arc<dyn DurableStore> = Arc::new(CountingStore::new(HaematiteStore::new(
2674 Arc::new(event_store),
2675 )));
2676 store
2677 }))
2678}
2679
2680pub(super) fn durable_data_dir(persistence_path: &Path) -> PathBuf {
2682 persistence_path.join("durability")
2683}
2684
2685pub(super) fn database_exists(data_dir: &Path) -> bool {
2688 database_config_file(data_dir).exists()
2689}
2690
2691const UNTOUCHED_DATABASE_ENTRIES: [&str; 2] = ["config.json", "writer.lock"];
2695
2696pub(super) fn database_untouched(data_dir: &Path) -> bool {
2700 std::fs::read_dir(data_dir).is_ok_and(|entries| {
2701 entries.into_iter().all(|entry| {
2702 entry.is_ok_and(|entry| {
2703 entry
2704 .file_name()
2705 .to_str()
2706 .is_some_and(|name| UNTOUCHED_DATABASE_ENTRIES.contains(&name))
2707 })
2708 })
2709 })
2710}
2711
2712fn database_config_file(data_dir: &Path) -> PathBuf {
2713 data_dir.join("config.json")
2714}
2715
2716pub(super) const DURABLE_RETENTION: Retention = Retention::reclaim(0);
2721
2722fn open_or_create_database(data_dir: &Path) -> Result<Database, ServerError> {
2725 let config_file = database_config_file(data_dir);
2726 let exists = config_file.exists();
2727 let step = if exists {
2728 "database_open"
2729 } else {
2730 "database_create"
2731 };
2732 crate::server::boot_steps::timed(step, || open_or_create_arm(data_dir, &config_file, exists))
2733}
2734
2735fn open_or_create_arm(
2738 data_dir: &Path,
2739 config_file: &Path,
2740 exists: bool,
2741) -> Result<Database, ServerError> {
2742 let result = if exists {
2743 adopt_missing_config_keys(data_dir, config_file)?;
2744 Database::open_with_retention(
2745 data_dir,
2746 haematite::RuntimeOptions {
2747 long_wait_interval: std::time::Duration::from_secs(5),
2748 reclaim_start: haematite::ReclaimStart::Held,
2751 },
2752 DURABLE_RETENTION,
2753 )
2754 } else {
2755 Database::create(
2756 DatabaseConfig {
2757 data_dir: data_dir.to_path_buf(),
2758 shard_count: DEFAULT_SHARD_COUNT,
2759 distributed: None,
2760 executor_threads: None,
2761 node_cache_budget: Some(haematite::NodeCacheBudget::Unlimited),
2770 retention: DURABLE_RETENTION,
2771 },
2772 haematite::RuntimeOptions {
2773 long_wait_interval: std::time::Duration::from_secs(5),
2774 reclaim_start: haematite::ReclaimStart::Held,
2777 },
2778 )
2779 };
2780 let database = result.map_err(|error| ServerError::ConfigValidation {
2781 message: format!(
2782 "failed to open durable store at {}: {error}",
2783 data_dir.display()
2784 ),
2785 })?;
2786 liminal::durability::record_store_opened();
2787 super::reclaim_placement::arm_watch(&database);
2788 Ok(database)
2789}
2790
2791const ADOPTED_CONFIG_KEYS: [(&str, &str); 2] = [
2803 ("node_cache_budget", "unlimited"),
2804 ("retention", "keep_all"),
2805];
2806
2807fn adopt_missing_config_keys(data_dir: &Path, config_file: &Path) -> Result<(), ServerError> {
2825 let Ok(bytes) = std::fs::read(config_file) else {
2826 return Ok(());
2827 };
2828 let Ok(mut config) = serde_json::from_slice::<serde_json::Value>(&bytes) else {
2829 return Ok(());
2830 };
2831 let Some(object) = config.as_object_mut() else {
2832 return Ok(());
2833 };
2834 let mut adopted = Vec::new();
2835 for (key, value) in ADOPTED_CONFIG_KEYS {
2836 if !object.contains_key(key) {
2837 object.insert(key.to_owned(), serde_json::Value::String(value.to_owned()));
2838 adopted.push(key);
2839 }
2840 }
2841 if adopted.is_empty() {
2842 return Ok(());
2843 }
2844 let keys = adopted.join(", ");
2845 let migrated =
2846 serde_json::to_vec_pretty(&config).map_err(|error| ServerError::ConfigValidation {
2847 message: format!(
2848 "durable store config at {} predates `{keys}` and its migration could not be \
2849 serialized: {error}",
2850 config_file.display()
2851 ),
2852 })?;
2853 let write_error = |step: &str, error: std::io::Error| ServerError::ConfigValidation {
2854 message: format!(
2855 "durable store config at {} predates `{keys}` and migrating it failed at {step}: \
2856 {error}",
2857 config_file.display()
2858 ),
2859 };
2860 let tmp_file = data_dir.join("config.json.migrate-tmp");
2861 let mut tmp = std::fs::File::create(&tmp_file)
2862 .map_err(|error| write_error("creating the temporary file", error))?;
2863 std::io::Write::write_all(&mut tmp, &migrated)
2864 .map_err(|error| write_error("writing the temporary file", error))?;
2865 tmp.sync_all()
2866 .map_err(|error| write_error("syncing the temporary file", error))?;
2867 drop(tmp);
2868 std::fs::rename(&tmp_file, config_file)
2869 .map_err(|error| write_error("renaming over config.json", error))?;
2870 std::fs::File::open(data_dir)
2871 .and_then(|dir| dir.sync_all())
2872 .map_err(|error| write_error("syncing the data directory", error))?;
2873 tracing::warn!(
2874 config = %config_file.display(),
2875 keys = %keys,
2876 "durable store config predates these keys -- wrote the create-arm values (the \
2877 behaviour-preserving ones) so the store opens with no operator action"
2878 );
2879 Ok(())
2880}
2881
2882pub(super) fn reserved_namespace_refusal(channel: &str) -> Option<ServerError> {
2883 if channel.starts_with("liminal.") {
2884 crate::metrics::self_stream_reserved_publication();
2885 Some(ServerError::ReservedNamespace {
2886 channel: channel.to_owned(),
2887 })
2888 } else {
2889 None
2890 }
2891}
2892
2893impl LiminalConnectionServices {
2894 pub(in crate::server) const fn signals(&self) -> &ServerSignals {
2895 &self.signals
2896 }
2897
2898 pub(in crate::server) fn self_stream_head(&self, name: &str) -> Result<u64, ServerError> {
2899 let channels = self.read_channels()?;
2900 let channel = channels
2901 .get(name)
2902 .ok_or_else(|| ServerError::ListenerAccept {
2903 message: format!("self stream '{name}' is absent after append"),
2904 })?;
2905 let heads =
2906 channel
2907 .handle
2908 .durable_positions()
2909 .map_err(|error| ServerError::ListenerAccept {
2910 message: format!("self stream '{name}' head is unavailable: {error}"),
2911 })?;
2912 drop(channels);
2913 match heads.as_slice() {
2914 [head] => Ok(*head),
2915 _ => Err(ServerError::ListenerAccept {
2916 message: format!("self stream '{name}' must have exactly one durable partition"),
2917 }),
2918 }
2919 }
2920
2921 pub(in crate::server) fn publish_self_stream_batch(
2922 &self,
2923 payloads: &[Vec<u8>],
2924 publisher: &str,
2925 ) -> Result<u64, ServerError> {
2926 if publisher.trim().is_empty() || payloads.is_empty() {
2927 return Err(ServerError::ConfigValidation {
2928 message: "self-stream batch requires a publisher and rows".to_owned(),
2929 });
2930 }
2931 let configured = self
2932 .admit_channel("liminal.events")
2933 .map_err(|error| access_to_server_error(&error))?;
2934 let deliveries = configured
2935 .handle
2936 .publish_batch_with_delivery(
2937 payloads,
2938 &liminal::envelope::PublisherId::new(publisher),
2939 None,
2940 )
2941 .map_err(|error| match error {
2942 liminal::error::LiminalError::PublishUnresolved { append } => {
2943 ServerError::PublishUnresolved {
2944 channel: "liminal.events".to_owned(),
2945 idempotency_key: None,
2946 sequence: Some(append.sequence()),
2947 message: append.to_string(),
2948 }
2949 }
2950 error => ServerError::ListenerAccept {
2951 message: format!("self-stream receipt batch failed: {error}"),
2952 },
2953 })?;
2954 if deliveries.len() != payloads.len()
2955 || deliveries
2956 .iter()
2957 .any(|delivery| delivery.durable_position().is_none())
2958 {
2959 return Err(ServerError::ListenerAccept {
2960 message: "self-stream receipt batch was refused before append".to_owned(),
2961 });
2962 }
2963 for pair in deliveries.windows(2) {
2964 if pair[0]
2965 .durable_position()
2966 .and_then(|position| position.checked_add(1))
2967 != pair[1].durable_position()
2968 {
2969 return Err(ServerError::ListenerAccept {
2970 message: "self-stream receipt batch positions are not contiguous".to_owned(),
2971 });
2972 }
2973 }
2974 for delivery in &deliveries {
2975 Self::record_delivery_counts(delivery);
2976 }
2977 deliveries
2978 .last()
2979 .and_then(ChannelDelivery::durable_position)
2980 .and_then(|position| position.checked_add(1))
2981 .ok_or_else(|| ServerError::ListenerAccept {
2982 message: "self-stream receipt batch head is unavailable".to_owned(),
2983 })
2984 }
2985
2986 pub(in crate::server) fn checkpoint_self_stream_heads(
2987 &self,
2988 heads: [u64; 2],
2989 ) -> Result<(), ServerError> {
2990 for (name, head) in ["liminal.events:0", "liminal.cost:0"]
2991 .into_iter()
2992 .zip(heads)
2993 {
2994 block_on(liminal::durability::head_checkpoint::advance_head(
2995 self.durable_store.as_ref(),
2996 name,
2997 head,
2998 ))
2999 .map_err(|error| ServerError::ShutdownFlush {
3000 message: format!("self-stream head checkpoint bridge for '{name}' failed: {error}"),
3001 })?
3002 .map_err(|error| ServerError::ShutdownFlush {
3003 message: format!("self-stream head checkpoint for '{name}' failed: {error}"),
3004 })?;
3005 }
3006 Ok(())
3007 }
3008
3009 pub(in crate::server) fn install_self_stream_channels(&self) -> Result<(), ServerError> {
3010 let names = ["liminal.cost", "liminal.events"];
3011 {
3012 let channels = self.read_channels()?;
3013 for name in names {
3014 if channels.contains_key(name) {
3015 return Err(ServerError::ConfigValidation {
3016 message: format!("self-stream channel '{name}' is already configured"),
3017 });
3018 }
3019 }
3020 }
3021 let mut built = Vec::with_capacity(names.len());
3022 for name in names {
3023 let schema =
3024 resolve_schema_bytes(None).map_err(|error| ServerError::ConfigValidation {
3025 message: format!("self-stream channel '{name}' schema failed: {error}"),
3026 })?;
3027 let channel = build_configured_channel(
3028 name,
3029 schema,
3030 true,
3031 ChannelOrigin::BootConfigured,
3032 &self.durable_store,
3033 self.cluster.supervisor(),
3034 )
3035 .map_err(|error| ServerError::ConfigValidation {
3036 message: error.boot_message(name),
3037 })?;
3038 channel
3039 .handle
3040 .install_publication_identity(self.publication_identity.clone())
3041 .map_err(|error| ServerError::ConfigValidation {
3042 message: error.to_string(),
3043 })?;
3044 built.push((name.to_owned(), Arc::new(channel)));
3045 }
3046 let mut channels =
3047 self.channels
3048 .write()
3049 .map_err(|error| ServerError::ConfigValidation {
3050 message: format!("self-stream channel roster lock poisoned: {error}"),
3051 })?;
3052 for (name, _) in &built {
3053 if channels.contains_key(name) {
3054 return Err(ServerError::ConfigValidation {
3055 message: format!("self-stream channel '{name}' was concurrently configured"),
3056 });
3057 }
3058 }
3059 channels.extend(built);
3060 drop(channels);
3061 for name in names {
3064 self.console_changed(crate::console::TopologyChange::Channel {
3065 name: name.to_owned(),
3066 life: line_life(true),
3067 state: crate::console::records::LineState::Live,
3068 });
3069 }
3070 Ok(())
3071 }
3072
3073 pub(in crate::server) fn install_console_channels(&self) -> Result<(), ServerError> {
3078 self.register_console_channel(crate::console::TOPOLOGY_SUBJECT)
3079 .map_err(|error| ServerError::ConfigValidation {
3080 message: format!("console channel registration failed: {error}"),
3081 })
3082 }
3083
3084 fn register_console_channel(&self, name: &str) -> Result<(), ChannelRegistryError> {
3087 if self.read_roster()?.contains_key(name) {
3088 return Ok(());
3089 }
3090 let schema = resolve_schema_bytes(None).map_err(|error| {
3091 ChannelRegistryError::RosterUnavailable {
3092 message: format!("console channel '{name}' schema failed: {error}"),
3093 }
3094 })?;
3095 let channel = build_configured_channel(
3096 name,
3097 schema,
3098 false,
3099 ChannelOrigin::BootConfigured,
3100 &self.durable_store,
3101 self.cluster.supervisor(),
3102 )
3103 .map_err(|error| error.into_registry_error(name))?;
3104 channel
3105 .handle
3106 .install_publication_identity(self.publication_identity.clone())
3107 .map_err(|error| ChannelRegistryError::RosterUnavailable {
3108 message: error.to_string(),
3109 })?;
3110 self.write_roster()?
3111 .entry(name.to_owned())
3112 .or_insert_with(|| Arc::new(channel));
3113 Ok(())
3114 }
3115
3116 pub fn publish_embedded(
3128 &self,
3129 host: &str,
3130 channel: &str,
3131 envelope: &MessageEnvelope,
3132 idempotency_key: Option<&str>,
3133 ) -> Result<PublishOutcome, ServerError> {
3134 self.publish_with_origin(
3135 channel,
3136 envelope,
3137 idempotency_key,
3138 PublishOrigin::Host { name: host },
3139 )
3140 .map(|published| published.outcome)
3141 }
3142
3143 pub(in crate::server) fn publish_with_origin(
3144 &self,
3145 channel: &str,
3146 envelope: &MessageEnvelope,
3147 idempotency_key: Option<&str>,
3148 origin: PublishOrigin<'_>,
3149 ) -> Result<OriginPublication, ServerError> {
3150 if !origin.is_server() {
3151 if plan_subject(channel).is_some() {
3152 return Err(site_services::site_error(
3153 crate::services::lifecycle::site::SiteError::Invalid(
3154 "plan subject is server-owned",
3155 ),
3156 ));
3157 }
3158 crate::agents::binding::refuse_server_publish(channel)?;
3159 }
3160 validate_publication_origin(channel, idempotency_key, origin)?;
3161 let mut recipients = self.signals.lend_recipients();
3165 let received_at = recipients.is_some().then(Instant::now);
3169 let result = (|| {
3170 let configured = self
3179 .admit_channel(channel)
3180 .map_err(|error| access_to_server_error(&error))?;
3181
3182 if let (Some(key), ChannelMode::Durable) =
3193 (idempotency_key, configured.handle.config().mode)
3194 {
3195 return self
3196 .publish_durable_keyed(
3197 &configured,
3198 channel,
3199 (envelope, origin),
3200 key,
3201 recipients.as_mut(),
3202 )
3203 .map(OriginPublication::connection);
3204 }
3205 let dedup = self.dedup_for(&configured);
3206 if let Some(key) = idempotency_key {
3207 if !Self::claim_delivery(dedup, key)? {
3208 crate::metrics::publish_accepted();
3212 return Ok(OriginPublication::connection(PublishOutcome {
3213 message_id: self.next_message_id()?,
3214 delivered: false,
3215 durable_position: None,
3216 reach: Reach::Suppressed,
3217 }));
3218 }
3219 }
3220
3221 let delivery = fan_out(&configured, envelope, origin, recipients.as_mut());
3222 let delivery = match delivery {
3223 Ok(delivery) => delivery,
3224 Err(liminal::error::LiminalError::PublishUnresolved { append }) => {
3228 return Err(ServerError::PublishUnresolved {
3229 channel: channel.to_owned(),
3230 idempotency_key: None,
3231 sequence: Some(append.sequence()),
3232 message: append.to_string(),
3233 });
3234 }
3235 Err(error) => {
3236 if let Some(key) = idempotency_key {
3243 Self::release_claim(dedup, key);
3244 }
3245 return Err(ServerError::ListenerAccept {
3246 message: format!("liminal publish failed for channel '{channel}': {error}"),
3247 });
3248 }
3249 };
3250
3251 if let Some(key) = idempotency_key {
3268 if delivery.is_admitted() {
3269 block_on(dedup.complete_receipt(key, ProcessingReceipt::new(Vec::new())))
3270 .map_err(|error| ServerError::ListenerAccept {
3271 message: format!(
3272 "dedup receipt bridge failed for key '{key}': {error}"
3273 ),
3274 })?
3275 .map_err(|error| ServerError::ListenerAccept {
3276 message: format!("dedup receipt write failed for key '{key}': {error}"),
3277 })?;
3278 } else {
3279 Self::release_claim(dedup, key);
3280 }
3281 }
3282
3283 let self_row_at = delivery.durable_position().filter(|_| origin.is_server());
3284 Ok(OriginPublication {
3285 outcome: Self::delivered(&delivery)?,
3286 self_row_appended: self_row_at.is_some(),
3287 self_row_at,
3288 })
3289 })();
3290 self.signals.published(
3291 channel,
3292 origin,
3293 envelope,
3294 &result,
3295 recipients.as_ref().zip(received_at),
3296 );
3297 if let Some(buffer) = recipients {
3298 self.signals.give_back(buffer);
3299 }
3300 result
3301 }
3302}
3303
3304fn fan_out(
3307 configured: &ConfiguredChannel,
3308 envelope: &MessageEnvelope,
3309 origin: PublishOrigin<'_>,
3310 recipients: Option<&mut liminal::channel::PublishRecord>,
3311) -> Result<liminal::channel::ChannelDelivery, liminal::error::LiminalError> {
3312 let publisher = || publisher_for_origin(origin);
3313 recipients.map_or_else(
3314 || {
3315 configured
3316 .handle
3317 .publish_with_delivery(&envelope.payload, publisher(), None)
3318 },
3319 |recipients| {
3320 configured.handle.publish_recording(
3321 &envelope.payload,
3322 publisher(),
3323 None,
3324 None,
3325 recipients,
3326 )
3327 },
3328 )
3329}
3330
3331fn validate_publication_origin(
3332 channel: &str,
3333 idempotency_key: Option<&str>,
3334 origin: PublishOrigin<'_>,
3335) -> Result<(), ServerError> {
3336 if !origin.is_server() {
3337 refuse_console_namespace(channel).map_err(|error| access_to_server_error(&error))?;
3338 if let Some(error) = reserved_namespace_refusal(channel) {
3339 return Err(error);
3340 }
3341 }
3342 if let PublishOrigin::Host { name } = origin
3343 && name.trim().is_empty()
3344 {
3345 return Err(ServerError::ConfigValidation {
3346 message: "embedded publication requires a nonempty host name".to_owned(),
3347 });
3348 }
3349 if let PublishOrigin::Server { device } = origin {
3350 if device.trim().is_empty() || idempotency_key.is_some() {
3351 return Err(ServerError::ConfigValidation {
3352 message: "server-origin publication requires a device and no idempotency key"
3353 .to_owned(),
3354 });
3355 }
3356 }
3357 Ok(())
3358}
3359
3360fn publisher_for_origin(origin: PublishOrigin<'_>) -> liminal::envelope::PublisherId {
3361 match origin {
3362 PublishOrigin::Connection { id } => liminal::envelope::PublisherId::new(id.to_string()),
3363 PublishOrigin::Host { name } => {
3364 liminal::envelope::PublisherId::new(format!("liminal.{name}"))
3365 }
3366 PublishOrigin::Server { device } => {
3367 liminal::envelope::PublisherId::new(format!("liminal.{device}"))
3368 }
3369 }
3370}
3371
3372impl ConnectionServices for LiminalConnectionServices {
3373 fn construction_request(
3374 &self,
3375 session: Option<&dyn crate::auth_lys::session::LysSession>,
3376 subject: &str,
3377 bytes: &[u8],
3378 at_ms: u64,
3379 ) -> Option<Result<serde_json::Value, ServerError>> {
3380 if let Some(answer) = self.agent_host_request(session, subject, bytes, at_ms) {
3381 return Some(answer);
3382 }
3383 is_site_route(subject).then(|| {
3384 self.serve_site_tool(session, subject, bytes, at_ms)
3385 .map_err(site_services::site_error)
3386 })
3387 }
3388 fn prepare_site_trial(
3389 &self,
3390 connection: u64,
3391 session: Option<&dyn crate::auth_lys::session::LysSession>,
3392 registration: &liminal::protocol::WorkerRegistration,
3393 process: Option<&crate::services::lifecycle::ProcessGrant>,
3394 at_ms: u64,
3395 ) -> Result<Option<crate::services::lifecycle::trial::TrialPermit>, ServerError> {
3396 self.site_lifecycle.get().map_or(Ok(None), |lifecycle| {
3397 lifecycle
3398 .prepare_trial(connection, session, registration, process, at_ms)
3399 .map_err(site_services::site_error)
3400 })
3401 }
3402 fn admit_site_call(
3403 &self,
3404 session: Option<&dyn crate::auth_lys::session::LysSession>,
3405 subject: &str,
3406 at_ms: u64,
3407 ) -> Result<bool, ServerError> {
3408 self.site_lifecycle.get().map_or(Ok(false), |lifecycle| {
3409 lifecycle
3410 .admit_trial_call(session, subject, at_ms)
3411 .map_err(site_services::site_error)
3412 })
3413 }
3414 fn site_registration_closed(&self, connection: u64) -> Result<(), ServerError> {
3415 if let Some(lifecycle) = self.site_lifecycle.get() {
3416 lifecycle
3417 .trial_closed(connection)
3418 .map_err(site_services::site_error)?;
3419 lifecycle
3420 .refresh_sites()
3421 .map_err(site_services::site_error)?;
3422 }
3423 Ok(())
3424 }
3425 fn publish_identified(
3426 &self,
3427 connection: u64,
3428 session: Option<&dyn crate::auth_lys::session::LysSession>,
3429 channel: &str,
3430 envelope: &MessageEnvelope,
3431 key: Option<&str>,
3432 ) -> Result<PublishOutcome, ServerError> {
3433 if plan_subject(channel).is_some() {
3434 return Err(site_services::site_error(
3435 crate::services::lifecycle::site::SiteError::Invalid(
3436 "plan subject is server-owned",
3437 ),
3438 ));
3439 }
3440 self.publish_agent(connection, session, channel, envelope, key)
3441 }
3442 fn agents(&self) -> Option<&Arc<crate::agents::runtime::Agents>> {
3443 self.agents.as_ref()
3444 }
3445
3446 fn subscribe_identified(
3447 &self,
3448 connection: u64,
3449 session: Option<&dyn crate::auth_lys::session::LysSession>,
3450 channel: &str,
3451 schemas: &[ProtocolSchemaId],
3452 install: Option<liminal::channel::InboxInstall>,
3453 ) -> Result<ConnectionSubscription, ServerError> {
3454 if plan_subject(channel).is_some() {
3455 return self.subscribe_site_plan(session, channel, schemas, install);
3456 }
3457 self.subscribe_agent(connection, session, channel, schemas, install)
3458 }
3459 fn participant_service(&self) -> Option<InstalledParticipantService> {
3460 self.participant_service.clone()
3461 }
3462
3463 fn admit_channel(
3490 &self,
3491 operation: ChannelOperation,
3492 channel: &str,
3493 ) -> Result<(), ChannelAccessError> {
3494 if let Some(name) = plan_subject(channel) {
3495 let refused = |message| ChannelAccessError::RosterUnavailable { message };
3496 if operation == ChannelOperation::Publish {
3497 return Err(refused("plan subject is server-owned".into()));
3498 }
3499 let lifecycle = self
3500 .site_lifecycle
3501 .get()
3502 .ok_or_else(|| refused("construction not installed".into()))?;
3503 lifecycle
3504 .record(name)
3505 .map_err(|error| refused(error.to_string()))?;
3506 return Ok(());
3507 }
3508 if let Some(admitted) = self.agent_channel_admission(operation, channel) {
3509 return admitted;
3510 }
3511 match operation {
3512 ChannelOperation::Publish => refuse_console_namespace(channel)?,
3513 ChannelOperation::Subscribe => {
3514 refuse_console_tap_target(channel)?;
3515 self.refuse_console_tap_limit(channel)?;
3516 let admitted = if self.signals.console_enabled()
3519 && super::channel_registry::console_tap_target(channel).is_some()
3520 {
3521 self.register_console_channel(channel).map_err(|error| {
3522 ChannelAccessError::RosterUnavailable {
3523 message: error.to_string(),
3524 }
3525 })
3526 } else {
3527 Ok(())
3528 }
3529 .and_then(|()| Self::admit_channel(self, channel).map(|_| ()));
3530 if admitted.is_err() {
3531 self.give_back_tap(channel);
3532 }
3533 return admitted;
3534 }
3535 }
3536 Self::admit_channel(self, channel)?;
3537 Ok(())
3538 }
3539
3540 fn publish(
3541 &self,
3542 connection: u64,
3543 channel: &str,
3544 envelope: &MessageEnvelope,
3545 idempotency_key: Option<&str>,
3546 ) -> Result<PublishOutcome, ServerError> {
3547 refuse_console_namespace(channel).map_err(|error| access_to_server_error(&error))?;
3551 if plan_subject(channel).is_some() {
3552 return Err(site_services::site_error(
3553 crate::services::lifecycle::site::SiteError::Invalid(
3554 "plan subject is server-owned",
3555 ),
3556 ));
3557 }
3558 crate::agents::binding::refuse_server_publish(channel)?;
3559 if crate::agents::binding::subject(channel).is_some() {
3560 return Err(crate::agents::error::AgentError::Invalid(
3561 "agent publish requires the verified identity route",
3562 )
3563 .into());
3564 }
3565 self.publish_with_origin(
3566 channel,
3567 envelope,
3568 idempotency_key,
3569 PublishOrigin::Connection { id: connection },
3570 )
3571 .map(|published| published.outcome)
3572 }
3573
3574 fn subscribe(
3575 &self,
3576 channel: &str,
3577 accepted_schemas: &[ProtocolSchemaId],
3578 install: Option<liminal::channel::InboxInstall>,
3579 ) -> Result<ConnectionSubscription, ServerError> {
3580 if plan_subject(channel).is_some() {
3581 return Err(site_services::site_error(
3582 crate::services::lifecycle::site::SiteError::Invalid(
3583 "plan subscription needs an admitted pass",
3584 ),
3585 ));
3586 }
3587 if crate::agents::binding::subject(channel).is_some() {
3588 return Err(crate::agents::error::AgentError::Invalid(
3589 "agent subscribe requires the verified identity route",
3590 )
3591 .into());
3592 }
3593 let subscribed = self.open_subscription(channel, accepted_schemas, install, None);
3594 if subscribed.is_err() {
3595 self.give_back_tap(channel);
3599 }
3600 subscribed
3601 }
3602
3603 fn unsubscribe(&self, subscription: ConnectionSubscription) -> Result<(), ServerError> {
3604 subscription.unsubscribe()
3605 }
3606
3607 fn open_conversation(
3608 &self,
3609 conversation_id: u64,
3610 subject: &str,
3611 ) -> Result<ConnectionConversation, ServerError> {
3612 self.open_routed_conversation(conversation_id, subject)
3613 }
3614
3615 fn conversation_message(
3616 &self,
3617 conversation: &ConnectionConversation,
3618 envelope: &MessageEnvelope,
3619 op_id: Option<u64>,
3620 ) -> Result<(), ServerError> {
3621 self.routed_conversation_message(conversation, envelope, op_id)
3622 }
3623
3624 fn close_conversation(&self, conversation: ConnectionConversation) -> Result<(), ServerError> {
3625 conversation.close()
3626 }
3627
3628 fn listening(self: Arc<Self>) -> Option<std::thread::JoinHandle<()>> {
3629 self.resolve_claims_behind_listener()
3630 }
3631
3632 fn stop_participant_warm(&self) {
3633 if let Some(warm) = &self.participant_warm {
3638 warm.stop();
3639 warm.record_clean_stop();
3640 }
3641 }
3642
3643 fn finish_background_store_work(&self) -> Result<(), ServerError> {
3644 self.finish_background_work()
3645 }
3646
3647 fn release_store_holders(&self) -> Result<(), ServerError> {
3648 self.release_holders()
3649 }
3650
3651 fn flush_durable_state(&self) -> Result<(), ServerError> {
3652 self.signals.with_flush_barrier(|| {
3655 block_on(self.head_checkpoint_stop.checkpoint_heads())
3656 .map_err(|error| ServerError::ShutdownFlush {
3657 message: format!("clean-stop head checkpoint bridge failed: {error}"),
3658 })?
3659 .map_err(|error| ServerError::ShutdownFlush {
3660 message: format!("clean-stop head checkpoint failed: {error}"),
3661 })?;
3662 block_on(self.durable_store.flush())
3663 .map_err(|error| ServerError::ShutdownFlush {
3664 message: format!("shared durable store flush bridge failed: {error}"),
3665 })?
3666 .map_err(|error| ServerError::ShutdownFlush {
3667 message: format!("shared durable store flush failed: {error}"),
3668 })?;
3669 Ok(())
3670 })?;
3671 Ok(())
3672 }
3673}
3674
3675#[derive(Debug)]
3678pub(super) struct ConfiguredChannel {
3679 handle: ChannelHandle,
3680 protocol_schema: ProtocolSchemaId,
3681 durable: bool,
3683 origin: ChannelOrigin,
3687 state: AtomicU8,
3691 quiesce_reason: OnceLock<String>,
3696}
3697
3698impl ConfiguredChannel {
3699 fn state(&self) -> ChannelState {
3705 if self.state.load(Ordering::Acquire) == STATE_QUIESCED {
3706 ChannelState::Quiesced {
3707 reason: self.recorded_quiesce_reason(),
3708 }
3709 } else {
3710 ChannelState::Active
3711 }
3712 }
3713
3714 fn recorded_quiesce_reason(&self) -> String {
3717 self.quiesce_reason
3718 .get()
3719 .map_or_else(|| UNRECORDED_QUIESCE_REASON.to_owned(), Clone::clone)
3720 }
3721
3722 fn quiesce(&self, name: &str, reason: &str) -> Result<(), ChannelRegistryError> {
3731 let recorded = match self.quiesce_reason.set(reason.to_owned()) {
3737 Ok(()) => reason.to_owned(),
3738 Err(_rejected) => self.recorded_quiesce_reason(),
3739 };
3740 let _flipped_here = self.state.compare_exchange(
3745 STATE_ACTIVE,
3746 STATE_QUIESCED,
3747 Ordering::Release,
3748 Ordering::Acquire,
3749 );
3750 if recorded == reason {
3751 return Ok(());
3752 }
3753 Err(ChannelRegistryError::AlreadyQuiesced {
3754 name: name.to_owned(),
3755 reason: recorded,
3756 })
3757 }
3758}
3759
3760#[derive(Debug)]
3761struct LiminalSubscriptionResource {
3762 subscription: liminal::channel::SubscriptionHandle,
3763}
3764
3765impl SubscriptionResource for LiminalSubscriptionResource {
3766 fn try_next_positioned(
3767 &mut self,
3768 ) -> Result<Option<(liminal::envelope::Envelope, Option<u64>)>, ServerError> {
3769 self.subscription
3770 .try_next_positioned()
3771 .map_err(|error| subscription_failure(&error))
3772 }
3773
3774 fn acknowledge_position(&self, position: u64) -> Result<(), ServerError> {
3775 self.subscription
3776 .acknowledge_position(position)
3777 .map_err(|error| subscription_failure(&error))
3778 }
3779
3780 fn consumer_position(&self) -> Result<Option<u64>, ServerError> {
3781 self.subscription
3782 .consumer_position()
3783 .map_err(|error| subscription_failure(&error))
3784 }
3785 fn unsubscribe(self: Box<Self>) -> Result<(), ServerError> {
3786 drop(self.subscription);
3787 Ok(())
3788 }
3789
3790 fn is_overflowed(&self) -> bool {
3791 self.subscription.is_overflowed()
3792 }
3793
3794 fn subscriber_pid(&self) -> Option<u64> {
3795 Some(self.subscription.subscriber_pid())
3796 }
3797
3798 fn has_pending(&self) -> bool {
3799 self.subscription.has_pending()
3800 }
3801
3802 fn try_next(&mut self) -> Option<liminal::envelope::Envelope> {
3803 match self.subscription.try_next() {
3804 Ok(envelope) => envelope,
3805 Err(error) => {
3806 tracing::error!(
3807 %error,
3808 "subscription could not receive its next envelope"
3809 );
3810 None
3811 }
3812 }
3813 }
3814}
3815
3816fn subscription_failure(error: &liminal::error::LiminalError) -> ServerError {
3818 ServerError::ListenerAccept {
3819 message: format!("liminal subscription: {error}"),
3820 }
3821}
3822
3823pub(super) fn access_to_server_error(error: &ChannelAccessError) -> ServerError {
3845 let message = match error {
3846 ChannelAccessError::NotRegistered { name } => {
3847 format!("channel '{name}' is not configured")
3848 }
3849 ChannelAccessError::RosterUnavailable { message } => message.clone(),
3850 ChannelAccessError::Quiesced { .. }
3851 | ChannelAccessError::ConsoleNamespace { .. }
3852 | ChannelAccessError::ConsoleTapTarget { .. }
3853 | ChannelAccessError::ConsoleTapLimit { .. } => error.to_string(),
3854 };
3855 ServerError::ListenerAccept { message }
3856}
3857
3858fn refuse_console_tap_target(channel: &str) -> Result<(), ChannelAccessError> {
3864 use super::channel_registry::{console_tap_target, is_console_subject};
3865 match console_tap_target(channel) {
3866 Some(target) if target.is_empty() || is_console_subject(target) => {
3867 Err(ChannelAccessError::ConsoleTapTarget {
3868 name: channel.to_owned(),
3869 })
3870 }
3871 _ => Ok(()),
3872 }
3873}
3874
3875fn refuse_console_namespace(channel: &str) -> Result<(), ChannelAccessError> {
3880 if super::channel_registry::is_console_subject(channel) {
3881 return Err(ChannelAccessError::ConsoleNamespace {
3882 name: channel.to_owned(),
3883 });
3884 }
3885 Ok(())
3886}
3887
3888pub(super) fn server_error_from_protocol(error: &ProtocolError) -> ServerError {
3889 ServerError::ListenerAccept {
3890 message: format!("protocol operation failed: {error}"),
3891 }
3892}
3893
3894#[cfg(test)]
3897#[path = "services_registry_tests.rs"]
3898mod services_registry_tests;
3899
3900#[cfg(test)]
3901mod durable_store_tests {
3902 type TestResult = Result<(), Box<dyn std::error::Error>>;
3904
3905 trait Must<T> {
3907 fn must(self, what: &str) -> Result<T, String>;
3908 }
3909
3910 impl<T, E: std::fmt::Debug> Must<T> for Result<T, E> {
3911 fn must(self, what: &str) -> Result<T, String> {
3912 self.map_err(|error| format!("{what}: {error:?}"))
3913 }
3914 }
3915
3916 impl<T> Must<T> for Option<T> {
3917 fn must(self, what: &str) -> Result<T, String> {
3918 self.ok_or_else(|| what.to_owned())
3919 }
3920 }
3921
3922 use liminal::durability::open_ephemeral_rooted;
3923
3924 use super::subsystem_census::RecordingSubsystems;
3925 use super::{
3926 ConnectionServices, DEFAULT_SHARD_COUNT, LiminalConnectionServices, SchedulerSubsystem,
3927 build_connection_services, build_connection_services_via, build_durable_store,
3928 };
3929 use crate::ServerError;
3930 use crate::config::types::{LimitsConfig, ServerConfig, ServicesConfig};
3931
3932 fn entry_count(root: &std::path::Path) -> Result<usize, String> {
3935 Ok(std::fs::read_dir(root)
3936 .must("ephemeral root is readable")?
3937 .count())
3938 }
3939
3940 fn config_with_profile(profile: &str) -> Result<ServerConfig, String> {
3945 Ok(ServerConfig {
3946 listen_address: "127.0.0.1:0".parse().must("valid socket addr")?,
3947 extra_listen_addresses: Vec::new(),
3948 health_listen_address: "127.0.0.1:1".parse().must("valid socket addr")?,
3949 drain_timeout_ms: 30_000,
3950 channels: Vec::new(),
3951 routing_rules: Vec::new(),
3952 persistence_path: None,
3953 cluster: None,
3954 auth: None,
3955 services: ServicesConfig {
3956 profile: profile.to_owned(),
3957 ..ServicesConfig::default()
3958 },
3959 limits: LimitsConfig::default(),
3960 participant: None,
3961 websocket: None,
3962 open: None,
3963 templates: crate::services::lifecycle::templates::TemplateSources::default(),
3964 agents: None,
3965 })
3966 }
3967
3968 #[test]
3979 fn worker_front_door_builds_no_store_and_no_temp_dir() -> TestResult {
3980 let front_door_root = tempfile::tempdir().must("test can create an ephemeral root")?;
3981 let full_root = tempfile::tempdir().must("test can create an ephemeral root")?;
3982
3983 let front_door_subsystems = RecordingSubsystems::rooted(front_door_root.path());
3984 let front_door: std::sync::Arc<dyn ConnectionServices> = build_connection_services_via(
3985 &config_with_profile("worker-front-door")?,
3986 &front_door_subsystems,
3987 )
3988 .must("worker-front-door services build")?;
3989 assert!(
3990 !front_door.supports_channel_operations(),
3991 "the worker front door serves no channel operations"
3992 );
3993 assert_eq!(
3994 entry_count(front_door_root.path())?,
3995 0,
3996 "the worker front door creates no ephemeral store directory (no haematite, no temp dir)"
3997 );
3998
3999 let full_subsystems = RecordingSubsystems::rooted(full_root.path());
4000 let mut config = config_with_profile("full")?;
4001 config.persistence_path = Some(full_root.path().to_path_buf());
4002 let full =
4003 build_connection_services_via(&config, &full_subsystems).must("full services build")?;
4004 assert!(
4005 full.supports_channel_operations(),
4006 "full mode serves channel operations"
4007 );
4008 assert_eq!(
4009 entry_count(full_root.path())?,
4010 1,
4011 "full mode builds its persistent database in the explicit directory"
4012 );
4013
4014 drop(front_door);
4015 drop(full);
4016 Ok(())
4017 }
4018
4019 #[test]
4037 fn worker_profile_census_is_empty_and_full_profile_records_all_schedulers() -> TestResult {
4038 let worker_root = tempfile::tempdir().must("test can create an ephemeral root")?;
4039 let full_root = tempfile::tempdir().must("test can create an ephemeral root")?;
4040
4041 let worker_subsystems = RecordingSubsystems::rooted(worker_root.path());
4042 let front_door = build_connection_services_via(
4043 &config_with_profile("worker-front-door")?,
4044 &worker_subsystems,
4045 )
4046 .must("worker-front-door services build")?;
4047 assert_eq!(
4048 worker_subsystems.recorded(),
4049 Vec::<SchedulerSubsystem>::new(),
4050 "the worker front door constructs no scheduler-owning subsystem"
4051 );
4052
4053 let full_subsystems = RecordingSubsystems::rooted(full_root.path());
4054 let mut config = config_with_profile("full")?;
4055 config.persistence_path = Some(full_root.path().to_path_buf());
4056 let full =
4057 build_connection_services_via(&config, &full_subsystems).must("full services build")?;
4058 assert_eq!(
4059 full_subsystems.recorded(),
4060 vec![
4061 SchedulerSubsystem::ChannelSupervisor,
4062 SchedulerSubsystem::ConversationSupervisor,
4063 SchedulerSubsystem::HaematiteStore,
4064 ],
4065 "the full profile constructs every scheduler-owning subsystem, once each — \
4066 the positive control proving the census instrument detects them"
4067 );
4068
4069 drop(front_door);
4070 drop(full);
4071 Ok(())
4072 }
4073
4074 #[test]
4078 fn full_only_constructors_reject_worker_profile() -> TestResult {
4079 let config = config_with_profile("worker-front-door")?;
4080
4081 let from_config = LiminalConnectionServices::from_config(&config);
4082 assert!(
4083 matches!(from_config, Err(ServerError::ConfigValidation { .. })),
4084 "from_config must reject a worker-front-door profile with ConfigValidation, got {from_config:?}"
4085 );
4086
4087 let root = tempfile::tempdir().must("test can create an ephemeral root")?;
4088 let store = open_ephemeral_rooted(root.path(), DEFAULT_SHARD_COUNT)
4089 .must("test store for the rejection check builds")?;
4090 let from_config_with_store =
4091 LiminalConnectionServices::from_config_with_store(&config, std::sync::Arc::new(store));
4092 assert!(
4093 matches!(
4094 from_config_with_store,
4095 Err(ServerError::ConfigValidation { .. })
4096 ),
4097 "from_config_with_store must reject a worker-front-door profile with ConfigValidation"
4098 );
4099 Ok(())
4100 }
4101
4102 #[test]
4107 fn build_connection_services_rejects_worker_profile_with_full_only_fields() -> TestResult {
4108 let mut config = config_with_profile("worker-front-door")?;
4109 config.channels = vec![crate::config::types::ChannelDef {
4110 name: "orders".to_owned(),
4111 schema_ref: None,
4112 durable: false,
4113 loaded_schema: None,
4114 }];
4115 config.persistence_path = Some(std::path::PathBuf::from("/tmp"));
4116
4117 let result = build_connection_services(&config);
4118 let Err(ServerError::ConfigValidation { message }) = result else {
4119 return Err(
4120 "expected ConfigValidation for worker profile with full-only fields".into(),
4121 );
4122 };
4123 assert!(message.contains("builds no channels"), "got: {message}");
4124 assert!(
4125 message.contains("builds no durable store"),
4126 "got: {message}"
4127 );
4128 Ok(())
4129 }
4130
4131 #[test]
4134 fn persistent_store_uses_configured_path_and_creates_no_temp_dir() -> TestResult {
4135 let home = tempfile::tempdir().must("test can create a temp dir")?;
4136 let ephemeral_root = tempfile::tempdir().must("test can create an ephemeral root")?;
4137
4138 let store = build_durable_store(home.path()).must("persistent store builds")?;
4139
4140 assert!(
4141 home.path().join("durability").join("config.json").exists(),
4142 "the persistent database is created under the configured path"
4143 );
4144 assert_eq!(
4145 entry_count(ephemeral_root.path())?,
4146 0,
4147 "the persistent branch creates no ephemeral guard directory"
4148 );
4149
4150 drop(store);
4151 Ok(())
4152 }
4153
4154 #[test]
4155 fn missing_path_refuses_before_any_store_is_opened() -> Result<(), Box<dyn std::error::Error>> {
4156 let root = tempfile::tempdir()?;
4157 let factory = RecordingSubsystems::rooted(root.path());
4158 let result = build_connection_services_via(&config_with_profile("full")?, &factory);
4159 assert!(
4160 matches!(result, Err(ServerError::ConfigValidation { message }) if message.contains("no store is opened"))
4161 );
4162 assert_eq!(entry_count(root.path())?, 0);
4163 assert!(factory.recorded().is_empty());
4164 Ok(())
4165 }
4166
4167 #[test]
4175 fn a_store_whose_config_predates_the_budget_field_opens_with_no_operator_hand() -> TestResult {
4176 let home = tempfile::tempdir().must("test can create a temp dir")?;
4177 let store = build_durable_store(home.path()).must("persistent store builds")?;
4178 drop(store);
4179
4180 let config_file = home.path().join("durability").join("config.json");
4181 let bytes = std::fs::read(&config_file).must("config.json is readable")?;
4182 let mut config: serde_json::Value =
4183 serde_json::from_slice(&bytes).must("config.json parses")?;
4184 config
4185 .as_object_mut()
4186 .must("config.json is an object")?
4187 .remove("node_cache_budget")
4188 .must("the created config carries the budget to remove")?;
4189 std::fs::write(
4190 &config_file,
4191 serde_json::to_vec_pretty(&config).must("serializes")?,
4192 )
4193 .must("test can age the config")?;
4194
4195 let reopened = build_durable_store(home.path())
4196 .must("a pre-budget store opens with no operator action")?;
4197 drop(reopened);
4198
4199 let after = std::fs::read(&config_file).must("config.json is readable after")?;
4200 let migrated: serde_json::Value =
4201 serde_json::from_slice(&after).must("migrated config parses")?;
4202 assert_eq!(
4203 migrated.get("node_cache_budget"),
4204 Some(&serde_json::Value::String("unlimited".to_owned())),
4205 "the migrated file carries the budget the create arm chooses"
4206 );
4207 Ok(())
4208 }
4209
4210 #[test]
4216 fn a_corrupt_config_still_refuses_and_is_left_untouched() -> TestResult {
4217 let home = tempfile::tempdir().must("test can create a temp dir")?;
4218 let data_dir = home.path().join("durability");
4219 std::fs::create_dir_all(&data_dir).must("test can create the data dir")?;
4220 let config_file = data_dir.join("config.json");
4221 let garbage = b"{ not json at all";
4222 std::fs::write(&config_file, garbage).must("test can write garbage")?;
4223
4224 let refused = build_durable_store(home.path());
4225 assert!(refused.is_err(), "a corrupt config refuses to open");
4226 assert_eq!(
4227 std::fs::read(&config_file)
4228 .must("config.json is still readable")?
4229 .as_slice(),
4230 garbage,
4231 "the refusing file is left byte-identical for the operator to read"
4232 );
4233 Ok(())
4234 }
4235
4236 #[test]
4240 fn a_config_already_carrying_the_budget_is_left_byte_identical() -> TestResult {
4241 let home = tempfile::tempdir().must("test can create a temp dir")?;
4242 let store = build_durable_store(home.path()).must("persistent store builds")?;
4243 drop(store);
4244
4245 let config_file = home.path().join("durability").join("config.json");
4246 let before = std::fs::read(&config_file).must("config.json is readable")?;
4247 let reopened = build_durable_store(home.path()).must("a current store reopens")?;
4248 drop(reopened);
4249 assert_eq!(
4250 std::fs::read(&config_file).must("config.json is readable after")?,
4251 before,
4252 "a config that already carries the budget is left byte-identical"
4253 );
4254 Ok(())
4255 }
4256
4257 #[test]
4261 fn a_keep_all_durable_store_reclaims_after_the_next_open() -> TestResult {
4262 let dir = tempfile::tempdir().must("test can create a store dir")?;
4263 let data_dir = dir.path().join("durability");
4264 let database = haematite::Database::create(
4265 haematite::DatabaseConfig {
4266 data_dir: data_dir.clone(),
4267 shard_count: 1,
4268 distributed: None,
4269 executor_threads: None,
4270 node_cache_budget: Some(haematite::NodeCacheBudget::Unlimited),
4271 retention: haematite::Retention::KeepAll,
4272 },
4273 haematite::RuntimeOptions {
4274 long_wait_interval: std::time::Duration::from_secs(5),
4275 reclaim_start: haematite::ReclaimStart::AtOpen,
4276 },
4277 )
4278 .must("a keep-all store is created")?;
4279 for round in 0_u32..200 {
4280 let mut value = round.to_be_bytes().to_vec();
4281 value.resize(1_024, 0x5a);
4282 database.put(b"key".to_vec(), value).must("put")?;
4283 database.commit().must("commit")?;
4284 }
4285 drop(database);
4286 let grown =
4287 haematite::db::vacuum::stats(&haematite::db::vacuum::Options::new(data_dir.clone()))
4288 .must("stats of the grown store")?
4289 .totals
4290 .enumerated_bytes;
4291
4292 let reopened = super::open_or_create_database(&data_dir).must("the server opens it")?;
4293 reopened
4296 .release_reclaim()
4297 .must("the held reclaim is released")?;
4298 reopened.quiesce_reclaim().must("reclaim runs to rest")?;
4299 drop(reopened);
4300
4301 let config: serde_json::Value = serde_json::from_slice(
4302 &std::fs::read(data_dir.join("config.json")).must("config is readable")?,
4303 )
4304 .must("config is JSON")?;
4305 assert_eq!(
4306 config["retention"],
4307 serde_json::to_value(super::DURABLE_RETENTION).must("the retention serializes")?
4308 );
4309 let report = haematite::db::vacuum::stats(&haematite::db::vacuum::Options::new(data_dir))
4310 .must("stats of the reclaimed store")?;
4311 assert!(
4312 report.totals.enumerated_bytes <= report.totals.marked_bytes.saturating_mul(3),
4313 "the reopened store holds about its live set: enumerated {} B, marked {} B",
4314 report.totals.enumerated_bytes,
4315 report.totals.marked_bytes
4316 );
4317 assert!(
4318 report.totals.enumerated_bytes.saturating_mul(10) < grown,
4319 "the old copies are gone: {} B now against {grown} B grown",
4320 report.totals.enumerated_bytes
4321 );
4322 Ok(())
4323 }
4324
4325 const STORED_RECLAIM_CONFIG: &str = r#"{
4331 "chunking_policy": {
4332 "chunking": "bytes-v2",
4333 "internal_target_bytes": 49152,
4334 "leaf_target_bytes": 65536
4335 },
4336 "data_dir": "DATA_DIR",
4337 "distributed": null,
4338 "executor_threads": null,
4339 "format_version": 2,
4340 "node_cache_budget": "unlimited",
4341 "retention": {
4342 "reclaim": {
4343 "past_roots": 0
4344 }
4345 },
4346 "shard_count": 8
4347}"#;
4348
4349 #[test]
4356 fn a_stored_reclaim_store_grown_without_reclaim_gives_its_old_copies_back_on_open() -> TestResult
4357 {
4358 let dir = tempfile::tempdir().must("test can create a store dir")?;
4359 let data_dir = dir.path().join("durability");
4360 let database = haematite::Database::create(
4361 haematite::DatabaseConfig {
4362 data_dir: data_dir.clone(),
4363 shard_count: 8,
4364 distributed: None,
4365 executor_threads: None,
4366 node_cache_budget: Some(haematite::NodeCacheBudget::Unlimited),
4367 retention: haematite::Retention::KeepAll,
4368 },
4369 haematite::RuntimeOptions {
4370 long_wait_interval: std::time::Duration::from_secs(5),
4371 reclaim_start: haematite::ReclaimStart::AtOpen,
4372 },
4373 )
4374 .must("a keep-all store is created")?;
4375 for round in 0_u32..200 {
4376 let mut value = round.to_be_bytes().to_vec();
4377 value.resize(1_024, 0x5a);
4378 database.put(b"key".to_vec(), value).must("put")?;
4379 database.commit().must("commit")?;
4380 }
4381 for row in 0_u32..200 {
4382 let key = [b"pruned-".as_slice(), &row.to_be_bytes()].concat();
4383 database.put(key, vec![0xa5; 1_024]).must("put")?;
4384 database.commit().must("commit")?;
4385 }
4386 for row in 0_u32..200 {
4387 let key = [b"pruned-".as_slice(), &row.to_be_bytes()].concat();
4388 database.delete(key).must("delete")?;
4389 }
4390 database.commit().must("commit")?;
4391 drop(database);
4392 let grown_report =
4393 haematite::db::vacuum::stats(&haematite::db::vacuum::Options::new(data_dir.clone()))
4394 .must("stats of the grown store")?;
4395 let grown = grown_report.totals;
4396 assert!(
4397 grown.unmarked_nodes > 0,
4398 "the seed leaves copies no root reaches"
4399 );
4400 let mut owed: std::collections::BTreeSet<usize> = grown_report
4401 .shards
4402 .iter()
4403 .filter(|shard| shard.nodes.unmarked_nodes > 0)
4404 .map(|shard| shard.shard_id)
4405 .collect();
4406
4407 let config_file = data_dir.join("config.json");
4408 let seeded = STORED_RECLAIM_CONFIG
4409 .replace("DATA_DIR", data_dir.to_str().must("the test dir is UTF-8")?);
4410 std::fs::write(&config_file, &seeded).must("config.json is written")?;
4411
4412 let reopened = super::open_or_create_database(&data_dir).must("the server opens it")?;
4413 let watch = reopened
4414 .watch_reclaim_passes(64)
4415 .must("every shard accepts a pass watch")?;
4416 reopened
4418 .release_reclaim()
4419 .must("the held reclaim is released")?;
4420 let mut unmarked_by_shard = std::collections::BTreeMap::new();
4421 while !owed.is_empty() {
4422 let report = watch
4423 .recv_timeout(std::time::Duration::from_secs(30))
4424 .must("each pass the open owes ends without a write or a quiesce")?;
4425 assert_eq!(
4426 report.outcome,
4427 haematite::ReclaimPassOutcome::Passed,
4428 "the pass the open owed on shard {} finished",
4429 report.shard_id
4430 );
4431 if owed.remove(&report.shard_id) {
4432 unmarked_by_shard.insert(report.shard_id, report.unmarked_bytes);
4433 }
4434 }
4435 drop(reopened);
4436
4437 let after = haematite::db::vacuum::stats(&haematite::db::vacuum::Options::new(data_dir))
4438 .must("stats of the reclaimed store")?
4439 .totals;
4440 eprintln!(
4441 "grown {} B, after {} B, {} unreachable nodes seeded, unmarked bytes by shard {unmarked_by_shard:?}",
4442 grown.enumerated_bytes, after.enumerated_bytes, grown.unmarked_nodes
4443 );
4444 assert!(
4445 after.enumerated_bytes.saturating_mul(10) < grown.enumerated_bytes,
4446 "the old copies are gone: {} B now against {} B grown",
4447 after.enumerated_bytes,
4448 grown.enumerated_bytes
4449 );
4450 let config: serde_json::Value = serde_json::from_slice(
4451 &std::fs::read(&config_file).must("config.json is readable after")?,
4452 )
4453 .must("config is JSON")?;
4454 assert_eq!(
4455 config["retention"],
4456 serde_json::to_value(super::DURABLE_RETENTION).must("the retention serializes")?,
4457 "config.json names reclaim after the open"
4458 );
4459 Ok(())
4460 }
4461}