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/server.rs"]
55mod site_services;
56pub use site_services::{
57 SiteDraftHolder, evaluate_subject as evaluate_site_subject, is_site_route, plan_subject,
58};
59pub(super) use site_services::{SiteFrame, site_frame_response};
60#[path = "services_lifecycle.rs"]
61mod lifecycle;
62#[path = "services_conversation.rs"]
63mod services_conversation;
64
65type ResponderRegistry = HashMap<String, Arc<dyn ParticipantBehaviour>>;
70
71pub trait SubscriptionResource: std::fmt::Debug + Send {
73 fn unsubscribe(self: Box<Self>) -> Result<(), ServerError>;
78
79 fn try_next(&mut self) -> Option<liminal::envelope::Envelope>;
86
87 fn has_pending(&self) -> bool;
89
90 fn is_overflowed(&self) -> bool {
94 false
95 }
96
97 fn subscriber_pid(&self) -> Option<u64> {
101 None
102 }
103
104 fn try_next_positioned(
109 &mut self,
110 ) -> Result<Option<(liminal::envelope::Envelope, Option<u64>)>, ServerError> {
111 Ok(self.try_next().map(|envelope| (envelope, None)))
112 }
113
114 fn acknowledge_position(&self, position: u64) -> Result<(), ServerError> {
119 Err(ServerError::ListenerAccept {
120 message: format!("subscription cannot acknowledge durable position {position}"),
121 })
122 }
123
124 fn consumer_position(&self) -> Result<Option<u64>, ServerError> {
129 Ok(None)
130 }
131}
132
133#[derive(Debug)]
135pub struct ConnectionSubscription {
136 id: u64,
137 channel: String,
142 stream_id: u32,
147 selected_schema: ProtocolSchemaId,
148 resource: Box<dyn SubscriptionResource>,
149 issued: super::state::IssuedDeliveries,
150}
151
152impl ConnectionSubscription {
153 #[must_use]
155 pub fn new(
156 id: u64,
157 selected_schema: ProtocolSchemaId,
158 resource: Box<dyn SubscriptionResource>,
159 ) -> Self {
160 Self {
161 id,
162 channel: String::new(),
163 stream_id: 0,
164 selected_schema,
165 resource,
166 issued: super::state::IssuedDeliveries::new(0),
167 }
168 }
169
170 pub(super) fn set_channel(&mut self, channel: String) {
175 self.channel = channel;
176 }
177
178 #[must_use]
180 pub(super) fn channel(&self) -> &str {
181 &self.channel
182 }
183
184 #[must_use]
186 pub const fn id(&self) -> u64 {
187 self.id
188 }
189
190 pub(super) const fn set_stream_id(&mut self, stream_id: u32) {
192 self.stream_id = stream_id;
193 }
194
195 #[must_use]
197 pub(super) const fn stream_id(&self) -> u32 {
198 self.stream_id
199 }
200
201 pub(super) fn set_pressure_ledger_depth(&mut self, limit: usize) {
202 self.issued = super::state::IssuedDeliveries::new(limit);
203 }
204
205 pub(super) fn issued_delivery(&mut self, sequence: u64) {
206 self.issued.record(sequence);
207 }
208
209 pub(super) fn match_pressure(&mut self, reference: &str, retain: bool) -> bool {
210 self.issued.matches(reference, retain)
211 }
212
213 #[must_use]
215 pub const fn selected_schema(&self) -> ProtocolSchemaId {
216 self.selected_schema
217 }
218
219 #[must_use]
221 pub fn subscriber_pid(&self) -> Option<u64> {
222 self.resource.subscriber_pid()
223 }
224
225 pub(crate) fn try_next(&mut self) -> Option<liminal::envelope::Envelope> {
227 self.resource.try_next()
228 }
229
230 pub(crate) fn has_pending(&self) -> bool {
231 self.resource.has_pending()
232 }
233
234 pub(crate) fn is_overflowed(&self) -> bool {
236 self.resource.is_overflowed()
237 }
238
239 pub(super) fn unsubscribe(self) -> Result<(), ServerError> {
240 self.resource.unsubscribe()
241 }
242
243 pub(crate) fn try_next_positioned(
244 &mut self,
245 ) -> Result<Option<(liminal::envelope::Envelope, Option<u64>)>, ServerError> {
246 self.resource.try_next_positioned()
247 }
248
249 pub(crate) fn acknowledge_position(&self, position: u64) -> Result<(), ServerError> {
250 self.resource.acknowledge_position(position)
251 }
252
253 pub(crate) fn consumer_position(&self) -> Result<Option<u64>, ServerError> {
254 self.resource.consumer_position()
255 }
256}
257
258#[derive(Clone, Copy, Debug, PartialEq, Eq)]
264pub struct PublishOutcome {
265 pub message_id: u64,
279 pub delivered: bool,
283 pub durable_position: Option<u64>,
288 pub reach: Reach,
292}
293
294#[derive(Clone, Copy, Debug, PartialEq, Eq)]
296pub enum Reach {
297 Delivered,
299 NoSubscriber,
301 NoneAccepted,
303 Suppressed,
305}
306
307impl Reach {
308 #[must_use]
310 pub const fn of(delivery: &ChannelDelivery) -> Self {
311 if delivery.is_delivered() {
312 Self::Delivered
313 } else if delivery.matched_count() == 0 {
314 Self::NoSubscriber
315 } else {
316 Self::NoneAccepted
317 }
318 }
319}
320
321#[derive(Clone, Copy, Debug, PartialEq, Eq)]
329pub enum ChannelOperation {
330 Publish,
332 Subscribe,
334}
335
336pub trait ConnectionServices: std::fmt::Debug + Send + Sync {
338 fn construction_request(
340 &self,
341 _: Option<&dyn crate::auth_lys::session::LysSession>,
342 subject: &str,
343 _: &[u8],
344 _: u64,
345 ) -> Option<Result<serde_json::Value, ServerError>> {
346 is_site_route(subject).then(|| {
347 Err(site_services::site_error(
348 crate::services::lifecycle::site::SiteError::Invalid("construction not installed"),
349 ))
350 })
351 }
352 fn prepare_site_trial(
357 &self,
358 _: u64,
359 _: Option<&dyn crate::auth_lys::session::LysSession>,
360 _: &liminal::protocol::WorkerRegistration,
361 _: Option<&crate::services::lifecycle::ProcessGrant>,
362 _: u64,
363 ) -> Result<Option<crate::services::lifecycle::trial::TrialPermit>, ServerError> {
364 Ok(None)
365 }
366 fn admit_site_call(
371 &self,
372 _: Option<&dyn crate::auth_lys::session::LysSession>,
373 _: &str,
374 _: u64,
375 ) -> Result<bool, ServerError> {
376 Ok(false)
377 }
378 fn site_registration_closed(&self, _: u64) -> Result<(), ServerError> {
383 Ok(())
384 }
385 fn publish_identified(
390 &self,
391 connection: u64,
392 session: Option<&dyn crate::auth_lys::session::LysSession>,
393 channel: &str,
394 envelope: &MessageEnvelope,
395 key: Option<&str>,
396 ) -> Result<PublishOutcome, ServerError> {
397 crate::auth_membership::check_agent_binding(
398 session,
399 crate::auth_lys::HotAction::Publish,
400 channel,
401 )?;
402 self.publish(connection, channel, envelope, key)
403 }
404 fn agents(&self) -> Option<&Arc<crate::agents::runtime::Agents>> {
406 None
407 }
408
409 fn subscribe_identified(
414 &self,
415 connection: u64,
416 session: Option<&dyn crate::auth_lys::session::LysSession>,
417 channel: &str,
418 schemas: &[ProtocolSchemaId],
419 install: Option<liminal::channel::InboxInstall>,
420 ) -> Result<ConnectionSubscription, ServerError> {
421 if connection == 0 {
422 return Err(crate::agents::error::AgentError::Invalid("connection id").into());
423 }
424 crate::auth_membership::check_agent_binding(
425 session,
426 crate::auth_lys::HotAction::Subscribe,
427 channel,
428 )?;
429 self.subscribe(channel, schemas, install)
430 }
431 fn participant_service(&self) -> Option<InstalledParticipantService> {
438 None
439 }
440
441 fn publish(
452 &self,
453 connection: u64,
454 channel: &str,
455 envelope: &MessageEnvelope,
456 idempotency_key: Option<&str>,
457 ) -> Result<PublishOutcome, ServerError>;
458
459 fn subscribe(
473 &self,
474 channel: &str,
475 accepted_schemas: &[ProtocolSchemaId],
476 install: Option<liminal::channel::InboxInstall>,
477 ) -> Result<ConnectionSubscription, ServerError>;
478
479 fn unsubscribe(&self, subscription: ConnectionSubscription) -> Result<(), ServerError>;
484
485 fn open_conversation(
490 &self,
491 conversation_id: u64,
492 subject: &str,
493 ) -> Result<ConnectionConversation, ServerError>;
494
495 fn conversation_message(
500 &self,
501 conversation: &ConnectionConversation,
502 envelope: &MessageEnvelope,
503 op_id: Option<u64>,
504 ) -> Result<(), ServerError>;
505
506 fn close_conversation(&self, conversation: ConnectionConversation) -> Result<(), ServerError>;
511
512 fn flush_durable_state(&self) -> Result<(), ServerError>;
517
518 fn stop_participant_warm(&self) {}
522
523 fn finish_background_store_work(&self) -> Result<(), ServerError> {
528 Ok(())
529 }
530
531 fn release_store_holders(&self) -> Result<(), ServerError> {
536 Ok(())
537 }
538
539 fn listening(self: Arc<Self>) -> Option<std::thread::JoinHandle<()>> {
544 None
545 }
546
547 fn supports_channel_operations(&self) -> bool {
560 true
561 }
562
563 fn admit_channel(
586 &self,
587 _operation: ChannelOperation,
588 _channel: &str,
589 ) -> Result<(), ChannelAccessError> {
590 Ok(())
591 }
592}
593
594#[derive(Debug)]
596pub struct LiminalConnectionServices {
597 publication_identity: Arc<publication_identity::ServerPublicationIdentity>,
598 agents: Option<Arc<crate::agents::runtime::Agents>>,
599 approval_agents: Option<Arc<crate::agents::waiting::durable::ApprovalAgents>>,
600 agent_delivery: Option<Arc<crate::agents::waiting::AgentDelivery>>,
601 site_lifecycle: OnceLock<Arc<crate::services::lifecycle::Lifecycle>>,
602 site_drafts: OnceLock<Arc<dyn SiteDraftHolder>>,
603 host_config: OnceLock<(crate::config::types::LysConfig, tokio::runtime::Handle)>,
604 draft_hook: OnceLock<std::sync::Weak<dyn crate::auth_lys::drafts::DraftDecisionHook>>,
605
606 channels: RwLock<HashMap<String, Arc<ConfiguredChannel>>>,
613 export_subscriptions:
614 Mutex<Vec<std::sync::Weak<crate::link_bridge::subscriptions::ExportSubscriptions>>>,
615 max_channels: Option<usize>,
624 cluster: ChannelCluster,
625 durable_store: Arc<dyn DurableStore>,
626 head_checkpoint_stop: Arc<liminal::durability::head_checkpoint_stop::HeadCheckpointStop>,
627 store_counts: StoreCountsHandle,
630 ephemeral_sweep: Option<EphemeralSweepCounts>,
631 signals: ServerSignals,
632 participant_service: Option<InstalledParticipantService>,
635 unloadable_conversations: Option<UnloadableConversationRecord>,
645 credential_reissuer: Option<Arc<dyn OperatorCredentialReissuer>>,
652 participant_warm: Option<ParticipantWarm>,
657 dedup: DedupCache,
665 dedup_ephemeral: DedupCache,
680 held_keys: unresolved::HeldKeys,
684 claims_start: Mutex<Option<StoreHistory>>,
688 conversation_supervisor: Arc<ConversationSupervisor>,
689 responders: Mutex<ResponderRegistry>,
697 next_subscription_id: AtomicU64,
698 console: Arc<crate::console::ConsoleHub>,
700}
701
702impl LiminalConnectionServices {
703 pub fn from_config(config: &ServerConfig) -> Result<Self, ServerError> {
718 Self::from_config_with_runtime(config, None)
719 }
720
721 pub(in crate::server) fn from_config_with_runtime(
722 config: &ServerConfig,
723 runtime: Option<tokio::runtime::Handle>,
724 ) -> Result<Self, ServerError> {
725 require_full_profile(config)?;
726 let path = required_persistence_path(config)?;
727 let history = StoreHistory::of(path);
728 let (subsystems, store) = crate::server::boot_steps::timed("store_open", || {
729 let subsystems = RootedProductionSubsystems::new(path)?;
730 let store = subsystems.durable_store(path)?;
731 Ok::<_, ServerError>((subsystems, store))
732 })?;
733 crate::server::boot_steps::timed("channel_construction", || {
734 Self::from_config_with_store_and_runtime(config, store, history, &subsystems, runtime)
735 })
736 }
737
738 pub fn from_config_with_store(
750 config: &ServerConfig,
751 durable_store: Arc<dyn DurableStore>,
752 ) -> Result<Self, ServerError> {
753 require_full_profile(config)?;
754 #[cfg(any(test, feature = "test-support"))]
755 if config.persistence_path.is_none() {
756 return Self::from_config_with_store_via(
757 config,
758 durable_store,
759 StoreHistory::Recorded,
760 &ProductionSubsystems,
761 );
762 }
763 let subsystems = RootedProductionSubsystems::new(required_persistence_path(config)?)?;
764 Self::from_config_with_store_via(config, durable_store, StoreHistory::Recorded, &subsystems)
765 }
766
767 fn from_config_with_store_via(
778 config: &ServerConfig,
779 durable_store: Arc<dyn DurableStore>,
780 history: StoreHistory,
781 subsystems: &dyn SubsystemFactory,
782 ) -> Result<Self, ServerError> {
783 Self::from_config_with_store_and_runtime(config, durable_store, history, subsystems, None)
784 }
785
786 fn from_config_with_store_and_runtime(
787 config: &ServerConfig,
788 durable_store: Arc<dyn DurableStore>,
789 history: StoreHistory,
790 subsystems: &dyn SubsystemFactory,
791 runtime: Option<tokio::runtime::Handle>,
792 ) -> Result<Self, ServerError> {
793 let head_checkpoint_stop = Arc::new(
794 liminal::durability::head_checkpoint_stop::HeadCheckpointStop::new(durable_store),
795 );
796 let durable_store: Arc<dyn DurableStore> = head_checkpoint_stop.clone();
797 let cluster = subsystems.channel_cluster(config.cluster.as_ref())?;
802 let mut channels = HashMap::new();
803 for channel in &config.channels {
804 let resolved = resolve_channel_schema(channel);
810 let configured = build_configured_channel(
811 &channel.name,
812 resolved,
813 channel.durable,
814 ChannelOrigin::BootConfigured,
815 &durable_store,
816 cluster.supervisor(),
817 )
818 .map_err(|error| ServerError::ConfigValidation {
819 message: error.boot_message(&channel.name),
820 })?;
821 channels.insert(channel.name.clone(), Arc::new(configured));
822 }
823 let conversation_supervisor = subsystems.conversation_supervisor()?;
824 let dedup = history.delivery_dedup(Arc::clone(&durable_store));
825 let ephemeral_store = ephemeral_dedup_store(&durable_store, history, subsystems);
829 let store_counts = combined_store_counts(&durable_store, &ephemeral_store);
830 let dedup_ephemeral = DedupCache::new_empty(ephemeral_store, ephemeral_dedup_namespace());
831 let signals = ServerSignals::default();
838 let installed_participant = config
839 .participant
840 .as_ref()
841 .map(|participant| install_participant(&durable_store, participant, &signals))
842 .transpose()?;
843 let device = config
844 .cluster
845 .as_ref()
846 .map_or("local", |cluster| cluster.node_name.as_str());
847 let console = Arc::new(console_hub(
848 device,
849 &channels,
850 config.persistence_path.is_some(),
851 config.auth.as_ref().and_then(|auth| auth.lys.as_ref()),
852 )?);
853 signals.install_console(Arc::clone(&console))?;
854 cluster.observer().set_console(Arc::clone(&console))?;
855 let (participant_service, unloadable_conversations, credential_reissuer, participant_warm) =
856 installed_participant.map_or((None, None, None, None), |installed| {
857 (
858 Some(installed.service),
859 Some(installed.unloadable_conversations),
860 Some(installed.credential_reissuer),
861 Some(installed.warm),
862 )
863 });
864 let (agents, approval_agents, agent_delivery) = Self::configured_agents(
865 config,
866 Arc::clone(&durable_store),
867 cluster.supervisor().clone(),
868 &console,
869 runtime,
870 )?;
871 let services = Self {
872 channels: RwLock::new(channels),
873 export_subscriptions: Mutex::new(Vec::new()),
874 max_channels: config.limits.max_channels,
875 cluster,
876 durable_store,
877 head_checkpoint_stop,
878 store_counts,
879 ephemeral_sweep: subsystems.ephemeral_sweep_counts(),
880 signals,
881 participant_service,
882 unloadable_conversations,
883 credential_reissuer,
884 participant_warm,
885 dedup,
886 dedup_ephemeral,
887 held_keys: unresolved::HeldKeys::default(),
888 claims_start: Mutex::new(Some(history)),
892 conversation_supervisor,
893 responders: Mutex::new(HashMap::new()),
894 publication_identity: publication_identity::ServerPublicationIdentity::new()?,
895 next_subscription_id: AtomicU64::new(1),
896 console,
897 agents,
898 approval_agents,
899 agent_delivery,
900 site_lifecycle: OnceLock::new(),
901 site_drafts: OnceLock::new(),
902 host_config: OnceLock::new(),
903 draft_hook: OnceLock::new(),
904 };
905 services.install_roster_identity()?;
906 Ok(services)
907 }
908
909 #[cfg(any(test, feature = "test-support"))]
914 pub fn empty() -> Result<Self, ServerError> {
915 let conversation_supervisor = ProductionSubsystems.conversation_supervisor()?;
916 let store =
917 open_ephemeral(DEFAULT_SHARD_COUNT).map_err(|error| ServerError::ConfigValidation {
918 message: format!("failed to open test durable store: {error}"),
919 })?;
920 let head_checkpoint_stop = Arc::new(
921 liminal::durability::head_checkpoint_stop::HeadCheckpointStop::new(Arc::new(
922 CountingStore::new(store),
923 )),
924 );
925 let durable_store: Arc<dyn DurableStore> = head_checkpoint_stop.clone();
926 let store_counts = combined_store_counts(&durable_store, &durable_store);
927 let dedup = StoreHistory::Fresh.delivery_dedup(Arc::clone(&durable_store));
928 let dedup_ephemeral =
931 DedupCache::new_empty(Arc::clone(&durable_store), ephemeral_dedup_namespace());
932 let signals = ServerSignals::default();
933 let console = Arc::new(console_hub("local", &HashMap::new(), false, None)?);
934 signals.install_console(Arc::clone(&console))?;
935 let cluster = build_channel_cluster(None)?;
936 cluster.observer().set_console(Arc::clone(&console))?;
937 Ok(Self {
938 channels: RwLock::new(HashMap::new()),
939 export_subscriptions: Mutex::new(Vec::new()),
940 max_channels: None,
945 cluster,
946 durable_store,
947 head_checkpoint_stop,
948 store_counts,
949 ephemeral_sweep: None,
950 signals,
951 participant_service: None,
952 unloadable_conversations: None,
953 credential_reissuer: None,
954 participant_warm: None,
955 dedup,
956 dedup_ephemeral,
957 held_keys: unresolved::HeldKeys::default(),
958 claims_start: Mutex::new(None),
959 conversation_supervisor,
960 responders: Mutex::new(HashMap::new()),
961 publication_identity: publication_identity::ServerPublicationIdentity::new()?,
962 next_subscription_id: AtomicU64::new(1),
963 console,
964 agents: None,
965 approval_agents: None,
966 agent_delivery: None,
967 site_lifecycle: OnceLock::new(),
968 site_drafts: OnceLock::new(),
969 host_config: OnceLock::new(),
970 draft_hook: OnceLock::new(),
971 })
972 }
973
974 #[must_use]
976 pub const fn ephemeral_sweep_counts(&self) -> Option<EphemeralSweepCounts> {
977 self.ephemeral_sweep
978 }
979
980 #[must_use]
985 pub const fn channel_cluster(&self) -> &ChannelCluster {
986 &self.cluster
987 }
988
989 #[must_use]
991 pub fn durable_store(&self) -> Arc<dyn DurableStore> {
992 Arc::clone(&self.durable_store)
993 }
994
995 #[must_use]
997 pub fn console(&self) -> &crate::console::ConsoleHub {
998 &self.console
999 }
1000
1001 fn console_changed(&self, change: crate::console::TopologyChange) {
1004 self.signals.topology(change);
1005 }
1006
1007 pub fn responder_subjects(&self) -> Result<Vec<String>, ServerError> {
1013 let responders = self.lock_responders()?;
1014 let mut subjects: Vec<String> = responders.keys().cloned().collect();
1015 drop(responders);
1016 subjects.sort();
1017 Ok(subjects)
1018 }
1019
1020 pub fn finish_participant_warm(&self) -> Result<usize, ServerError> {
1028 self.participant_warm.as_ref().map_or(Ok(0), |warm| {
1029 warm.finish()
1030 .map_err(|error| ServerError::ParticipantStartupRestore {
1031 message: error.to_string(),
1032 })
1033 })
1034 }
1035
1036 #[must_use]
1042 pub fn store_counts(&self) -> StoreCountsHandle {
1043 self.store_counts.clone()
1044 }
1045
1046 #[must_use]
1054 pub fn unloadable_conversation_record(&self) -> Option<UnloadableConversationRecord> {
1055 self.unloadable_conversations.clone()
1056 }
1057
1058 #[must_use]
1067 pub fn credential_reissuer(&self) -> Option<Arc<dyn OperatorCredentialReissuer>> {
1068 self.credential_reissuer.clone()
1069 }
1070
1071 #[cfg(test)]
1078 #[must_use]
1079 pub(crate) fn with_participant_service(
1080 mut self,
1081 participant_service: InstalledParticipantService,
1082 ) -> Self {
1083 self.participant_service = Some(participant_service);
1084 self
1085 }
1086
1087 #[must_use]
1092 pub fn conversation_supervisor(&self) -> Arc<ConversationSupervisor> {
1093 Arc::clone(&self.conversation_supervisor)
1094 }
1095
1096 pub fn register_responder(
1105 &self,
1106 subject: impl Into<String>,
1107 behaviour: Arc<dyn ParticipantBehaviour>,
1108 ) -> Result<Option<Arc<dyn ParticipantBehaviour>>, ServerError> {
1109 let subject = subject.into();
1110 let mut responders = self.lock_responders()?;
1111 let previous = responders.insert(subject.clone(), behaviour);
1112 drop(responders);
1113 self.console_changed(crate::console::TopologyChange::Service {
1114 subject,
1115 present: true,
1116 });
1117 Ok(previous)
1118 }
1119
1120 pub fn unregister_responder(
1128 &self,
1129 subject: &str,
1130 ) -> Result<Option<Arc<dyn ParticipantBehaviour>>, ServerError> {
1131 let mut responders = self.lock_responders()?;
1132 let removed = responders.remove(subject);
1133 drop(responders);
1134 if removed.is_some() {
1135 self.console_changed(crate::console::TopologyChange::Service {
1136 subject: subject.to_owned(),
1137 present: false,
1138 });
1139 }
1140 Ok(removed)
1141 }
1142
1143 fn lock_responders(&self) -> Result<std::sync::MutexGuard<'_, ResponderRegistry>, ServerError> {
1146 self.responders
1147 .lock()
1148 .map_err(|_poisoned| ServerError::ListenerAccept {
1149 message: "responder registry lock poisoned".to_owned(),
1150 })
1151 }
1152
1153 fn read_channels(
1162 &self,
1163 ) -> Result<std::sync::RwLockReadGuard<'_, HashMap<String, Arc<ConfiguredChannel>>>, ServerError>
1164 {
1165 self.channels
1166 .read()
1167 .map_err(|_poisoned| ServerError::ListenerAccept {
1168 message: "channel roster lock poisoned".to_owned(),
1169 })
1170 }
1171
1172 #[cfg(test)]
1176 pub(in crate::server) fn channel_handle_for_test(
1177 &self,
1178 channel: &str,
1179 ) -> Result<ChannelHandle, ServerError> {
1180 self.read_channels()?
1181 .get(channel)
1182 .map(|configured| configured.handle.clone())
1183 .ok_or_else(|| ServerError::ListenerAccept {
1184 message: format!("channel '{channel}' is not configured"),
1185 })
1186 }
1187
1188 #[cfg(test)]
1189 pub(crate) fn subscribe_handle_for_test(
1190 &self,
1191 channel: &str,
1192 ) -> Result<liminal::channel::SubscriptionHandle, ServerError> {
1193 self.subscribe_handle_for_test_with_install(channel, None)
1194 }
1195
1196 #[cfg(test)]
1201 pub(crate) fn subscribe_handle_for_test_with_install(
1202 &self,
1203 channel: &str,
1204 install: Option<liminal::channel::InboxInstall>,
1205 ) -> Result<liminal::channel::SubscriptionHandle, ServerError> {
1206 let channels = self.read_channels()?;
1207 let configured =
1208 channels
1209 .get(channel)
1210 .map(Arc::clone)
1211 .ok_or_else(|| ServerError::ListenerAccept {
1212 message: format!("channel '{channel}' is not configured"),
1213 })?;
1214 drop(channels);
1215 install
1216 .map_or_else(
1217 || configured.handle.subscribe(),
1218 |install| configured.handle.subscribe_with_install(install),
1219 )
1220 .map_err(|error| ServerError::ListenerAccept {
1221 message: format!("liminal subscribe failed for channel '{channel}': {error}"),
1222 })
1223 }
1224
1225 fn claim_delivery(cache: &DedupCache, key: &str) -> Result<bool, ServerError> {
1232 let decision = block_on(cache.claim_or_get(key, dedup_timestamp_millis()))
1233 .map_err(|error| ServerError::ListenerAccept {
1234 message: format!("dedup bridge failed for key '{key}': {error}"),
1235 })?
1236 .map_err(|error| ServerError::ListenerAccept {
1237 message: format!("dedup claim failed for key '{key}': {error}"),
1238 })?;
1239 Ok(matches!(decision, DedupDecision::Claimed))
1240 }
1241
1242 const fn dedup_for(&self, channel: &ConfiguredChannel) -> &DedupCache {
1249 match channel.handle.config().mode {
1250 ChannelMode::Durable => &self.dedup,
1251 ChannelMode::Ephemeral => &self.dedup_ephemeral,
1252 }
1253 }
1254
1255 fn release_claim(cache: &DedupCache, key: &str) {
1263 match block_on(cache.release_claim(key)) {
1264 Ok(Ok(())) => {}
1265 Ok(Err(error)) => {
1266 tracing::error!(
1267 idempotency_key = key,
1268 %error,
1269 "failed to release dedup claim after publish failure; key may stay suppressed"
1270 );
1271 }
1272 Err(error) => {
1273 tracing::error!(
1274 idempotency_key = key,
1275 %error,
1276 "dedup release bridge failed after publish failure; key may stay suppressed"
1277 );
1278 }
1279 }
1280 }
1281
1282 fn next_message_id(&self) -> Result<u64, ServerError> {
1284 self.publication_identity
1285 .next()
1286 .map(|message| message.id)
1287 .map_err(|error| ServerError::ListenerAccept {
1288 message: error.to_string(),
1289 })
1290 }
1291
1292 fn delivered(delivery: &ChannelDelivery) -> Result<PublishOutcome, ServerError> {
1296 Self::record_delivery_counts(delivery);
1297 let message = delivery
1298 .delivery_id()
1299 .ok_or_else(|| ServerError::ListenerAccept {
1300 message: "publication identity missing after admission".into(),
1301 })?;
1302 Ok(PublishOutcome {
1303 message_id: message.id,
1304 delivered: delivery.is_delivered(),
1305 durable_position: delivery.durable_position(),
1306 reach: Reach::of(delivery),
1307 })
1308 }
1309}
1310
1311#[path = "deferred_store.rs"]
1312mod deferred_store;
1313#[path = "unresolved.rs"]
1314mod unresolved;
1315#[path = "unresolved_start.rs"]
1316mod unresolved_start;
1317#[cfg(test)]
1318#[path = "unresolved_start_tests.rs"]
1319mod unresolved_start_tests;
1320
1321use deferred_store::{DeferredStore, ThrowawayStoreOpener};
1322use unresolved_start::StoreHistory;
1323
1324impl LiminalConnectionServices {
1350 pub fn register_channel(
1379 &self,
1380 spec: &ChannelRegistration,
1381 ) -> Result<Registered, ChannelRegistryError> {
1382 let Some(limit) = self.max_channels else {
1386 return Err(ChannelRegistryError::CapNotConfigured {
1387 cap: MAX_CHANNELS_KEY,
1388 });
1389 };
1390 let resolved = resolve_schema_bytes(spec.schema_bytes.as_deref()).map_err(|error| {
1394 ChannelRegistryError::SchemaRejected {
1395 name: spec.name.clone(),
1396 message: error.to_string(),
1397 }
1398 })?;
1399
1400 let (existing, registered_count) = {
1405 let channels = self.read_roster()?;
1406 let existing = channels.get(&spec.name).map(Arc::clone);
1407 let registered_count = runtime_registered_count(&channels);
1408 drop(channels);
1409 (existing, registered_count)
1410 };
1411 if let Some(existing) = existing {
1412 return compare_registration(&existing, spec, &resolved);
1413 }
1414 if registered_count >= limit {
1415 return Err(ChannelRegistryError::CapReached {
1416 cap: MAX_CHANNELS_KEY,
1417 limit,
1418 });
1419 }
1420
1421 let configured = build_configured_channel(
1426 &spec.name,
1427 resolved,
1428 spec.durable,
1429 ChannelOrigin::RuntimeRegistered,
1430 &self.durable_store,
1431 self.cluster.supervisor(),
1432 )
1433 .map_err(|error| error.into_registry_error(&spec.name))?;
1434
1435 let mut exporters = self.export_subscriptions.lock().map_err(|_| {
1439 ChannelRegistryError::RosterUnavailable {
1440 message: "export subscription registry lock poisoned".to_owned(),
1441 }
1442 })?;
1443 exporters.retain(|exporter| exporter.strong_count() > 0);
1444 let mut channels = self.write_roster()?;
1445 let raced = channels.get(&spec.name).map(Arc::clone);
1446 let registered_count = runtime_registered_count(&channels);
1447 if raced.is_none() && registered_count < limit {
1448 let mut prepared = Vec::new();
1449 for exporter in exporters.iter().filter_map(std::sync::Weak::upgrade) {
1450 if let Some(binding) = exporter.binding(&spec.name) {
1451 let refused = |error: ServerError| ChannelRegistryError::RosterUnavailable {
1452 message: format!(
1453 "export '{}' cannot subscribe to '{}': {error}",
1454 exporter.link(),
1455 spec.name
1456 ),
1457 };
1458 let subscription = self
1459 .build_subscription(
1460 &spec.name,
1461 &configured.handle,
1462 configured.protocol_schema,
1463 Some(binding.install),
1464 Some(&binding.consumer),
1465 )
1466 .map_err(refused)?;
1467 let subscription =
1468 crate::link_bridge::subscriptions::PreparedSubscription::new(
1469 binding.prefix,
1470 spec.name.clone(),
1471 subscription,
1472 )
1473 .map_err(refused)?;
1474 prepared.push((exporter, subscription));
1475 }
1476 }
1477 for (exporter, subscription) in prepared {
1479 exporter.attach(subscription);
1480 }
1481 self.identify_registered(&configured)?;
1482 channels.insert(spec.name.clone(), Arc::new(configured));
1483 drop(channels);
1484 drop(exporters);
1485 self.console_changed(crate::console::TopologyChange::Channel {
1486 name: spec.name.clone(),
1487 life: line_life(spec.durable),
1488 state: crate::console::records::LineState::Live,
1489 });
1490 return Ok(Registered::Created);
1491 }
1492 drop(channels);
1493 drop(exporters);
1494 if let Some(raced) = raced {
1495 return compare_registration(&raced, spec, &schema_of(&configured));
1501 }
1502 Err(ChannelRegistryError::CapReached {
1503 cap: MAX_CHANNELS_KEY,
1504 limit,
1505 })
1506 }
1507
1508 pub fn quiesce_channel(
1530 &self,
1531 name: &str,
1532 reason: impl Into<String>,
1533 ) -> Result<(), ChannelRegistryError> {
1534 let reason = reason.into();
1535 let configured = {
1538 let channels = self.read_roster()?;
1539 channels.get(name).map(Arc::clone).ok_or_else(|| {
1540 ChannelRegistryError::NotRegistered {
1541 name: name.to_owned(),
1542 }
1543 })?
1544 };
1545 configured.quiesce(name, &reason)?;
1546 self.console_changed(crate::console::TopologyChange::Channel {
1547 name: name.to_owned(),
1548 life: line_life(configured.durable),
1549 state: crate::console::records::LineState::Quiesced,
1550 });
1551 Ok(())
1552 }
1553
1554 pub fn channel_status(&self, name: &str) -> Result<ChannelStatus, ChannelRegistryError> {
1566 let configured = {
1567 let channels = self.read_roster()?;
1568 channels.get(name).map(Arc::clone)
1569 };
1570 Ok(
1571 configured.map_or(ChannelStatus::NotRegistered, |configured| {
1572 status_of(&configured)
1573 }),
1574 )
1575 }
1576
1577 pub fn channel_census(&self) -> Result<Vec<(String, ChannelStatus)>, ChannelRegistryError> {
1584 let mut census: Vec<(String, ChannelStatus)> = self
1585 .read_roster()?
1586 .iter()
1587 .map(|(name, configured)| (name.clone(), status_of(configured)))
1588 .collect();
1589 census.sort_by(|left, right| left.0.cmp(&right.0));
1590 Ok(census)
1591 }
1592
1593 pub fn registered_channels(&self) -> Result<Vec<ChannelDescriptor>, ChannelRegistryError> {
1604 let entries: Vec<(String, Arc<ConfiguredChannel>)> = {
1605 let channels = self.read_roster()?;
1606 channels
1607 .iter()
1608 .map(|(name, configured)| (name.clone(), Arc::clone(configured)))
1609 .collect()
1610 };
1611 let mut descriptors: Vec<ChannelDescriptor> = entries
1612 .into_iter()
1613 .map(|(name, configured)| ChannelDescriptor {
1614 name,
1615 origin: configured.origin,
1616 state: configured.state(),
1617 })
1618 .collect();
1619 descriptors.sort_by(|left, right| left.name.cmp(&right.name));
1620 Ok(descriptors)
1621 }
1622
1623 fn admit_channel(&self, channel: &str) -> Result<Arc<ConfiguredChannel>, ChannelAccessError> {
1637 let configured = {
1638 let channels = self.channels.read().map_err(|_poisoned| {
1639 ChannelAccessError::RosterUnavailable {
1640 message: ROSTER_POISONED.to_owned(),
1641 }
1642 })?;
1643 channels.get(channel).map(Arc::clone)
1644 };
1645 let configured = configured.ok_or_else(|| ChannelAccessError::NotRegistered {
1646 name: channel.to_owned(),
1647 })?;
1648 if configured.state.load(Ordering::Acquire) == STATE_QUIESCED {
1649 return Err(ChannelAccessError::Quiesced {
1650 name: channel.to_owned(),
1651 reason: configured.recorded_quiesce_reason(),
1652 });
1653 }
1654 Ok(configured)
1655 }
1656
1657 fn read_roster(
1664 &self,
1665 ) -> Result<
1666 std::sync::RwLockReadGuard<'_, HashMap<String, Arc<ConfiguredChannel>>>,
1667 ChannelRegistryError,
1668 > {
1669 #[cfg(test)]
1670 ROSTER_READS.with(|reads| reads.set(reads.get() + 1));
1671 self.channels
1672 .read()
1673 .map_err(|_poisoned| ChannelRegistryError::RosterUnavailable {
1674 message: ROSTER_POISONED.to_owned(),
1675 })
1676 }
1677
1678 fn write_roster(
1681 &self,
1682 ) -> Result<
1683 std::sync::RwLockWriteGuard<'_, HashMap<String, Arc<ConfiguredChannel>>>,
1684 ChannelRegistryError,
1685 > {
1686 self.channels
1687 .write()
1688 .map_err(|_poisoned| ChannelRegistryError::RosterUnavailable {
1689 message: ROSTER_POISONED.to_owned(),
1690 })
1691 }
1692
1693 fn give_back_tap(&self, channel: &str) {
1697 self.console.release_tap(channel, |subject| {
1698 crate::console::ServerPublisher::retire_console_channel(self, subject)
1699 });
1700 }
1701
1702 fn open_subscription(
1706 &self,
1707 channel: &str,
1708 accepted_schemas: &[ProtocolSchemaId],
1709 install: Option<liminal::channel::InboxInstall>,
1710 consumer: Option<&str>,
1711 ) -> Result<ConnectionSubscription, ServerError> {
1712 refuse_console_tap_target(channel).map_err(|error| access_to_server_error(&error))?;
1715 let configured = self
1721 .admit_channel(channel)
1722 .map_err(|error| access_to_server_error(&error))?;
1723 let selected_schema = if accepted_schemas.is_empty() {
1724 configured.protocol_schema
1725 } else {
1726 liminal::protocol::negotiate_schema(configured.protocol_schema, accepted_schemas)
1727 .map_err(|error| server_error_from_protocol(&error))?
1728 };
1729 self.build_subscription(
1730 channel,
1731 &configured.handle,
1732 selected_schema,
1733 install,
1734 consumer,
1735 )
1736 }
1737
1738 fn build_subscription(
1739 &self,
1740 channel: &str,
1741 handle: &ChannelHandle,
1742 selected_schema: ProtocolSchemaId,
1743 install: Option<liminal::channel::InboxInstall>,
1744 consumer: Option<&str>,
1745 ) -> Result<ConnectionSubscription, ServerError> {
1746 let subscription = match (consumer, install) {
1751 (Some(consumer), Some(install)) => handle.subscribe_consumer(consumer, install),
1752 (Some(_), None) => {
1753 return Err(ServerError::ListenerAccept {
1754 message: "a durable consumer requires a bounded inbox install".to_owned(),
1755 });
1756 }
1757 (None, Some(install)) => handle.subscribe_with_install(install),
1758 (None, None) => handle.subscribe(),
1759 }
1760 .map_err(|error| ServerError::ListenerAccept {
1761 message: format!("liminal subscribe failed for channel '{channel}': {error}"),
1762 })?;
1763 let id = self.next_subscription_id.fetch_add(1, Ordering::Relaxed);
1764 let mut connection_subscription = ConnectionSubscription::new(
1765 id,
1766 selected_schema,
1767 Box::new(LiminalSubscriptionResource { subscription }),
1768 );
1769 connection_subscription.set_channel(channel.to_owned());
1770 Ok(connection_subscription)
1774 }
1775
1776 pub(crate) fn subscribe_consumer(
1777 &self,
1778 channel: &str,
1779 consumer: &str,
1780 install: liminal::channel::InboxInstall,
1781 ) -> Result<ConnectionSubscription, ServerError> {
1782 self.open_subscription(channel, &[], Some(install), Some(consumer))
1783 }
1784
1785 pub(crate) fn install_exporter(
1786 &self,
1787 exporter: &Arc<crate::link_bridge::subscriptions::ExportSubscriptions>,
1788 ) -> Result<(), ServerError> {
1789 let mut exporters =
1790 self.export_subscriptions
1791 .lock()
1792 .map_err(|_| ServerError::ListenerAccept {
1793 message: "export subscription registry lock poisoned".to_owned(),
1794 })?;
1795 exporters.retain(|exporter| exporter.strong_count() > 0);
1796 if exporters
1797 .iter()
1798 .filter_map(std::sync::Weak::upgrade)
1799 .any(|existing| existing.link() == exporter.link())
1800 {
1801 return Err(ServerError::ListenerAccept {
1802 message: format!(
1803 "link '{}' already owns an export subscription",
1804 exporter.link()
1805 ),
1806 });
1807 }
1808 let mut prepared = Vec::new();
1809 for channel in self
1810 .registered_channels()
1811 .map_err(|error| ServerError::ListenerAccept {
1812 message: error.to_string(),
1813 })?
1814 {
1815 if matches!(channel.state, ChannelState::Active) {
1816 if let Some(binding) = exporter.binding(&channel.name) {
1817 let subscription =
1818 self.subscribe_consumer(&channel.name, &binding.consumer, binding.install)?;
1819 prepared.push(
1820 crate::link_bridge::subscriptions::PreparedSubscription::new(
1821 binding.prefix,
1822 channel.name,
1823 subscription,
1824 )?,
1825 );
1826 }
1827 }
1828 }
1829 for subscription in prepared {
1830 exporter.attach(subscription);
1831 }
1832 exporters.push(Arc::downgrade(exporter));
1833 drop(exporters);
1834 Ok(())
1835 }
1836
1837 fn refuse_console_tap_limit(&self, channel: &str) -> Result<(), ChannelAccessError> {
1848 match self.console.reserve_tap(channel) {
1849 Ok(false) => Err(ChannelAccessError::ConsoleTapLimit {
1850 name: channel.to_owned(),
1851 limit: crate::console::MAX_LIVE_TAPS,
1852 }),
1853 Ok(true) | Err(_) => Ok(()),
1854 }
1855 }
1856}
1857
1858impl crate::console::ServerPublisher for LiminalConnectionServices {
1859 fn publish_console(
1863 &self,
1864 subject: &str,
1865 payload: &[u8],
1866 ) -> Result<(), crate::console::ConsoleError> {
1867 let payload = self.agent_topology_payload(subject, payload)?;
1868 let payload = if subject == crate::console::TOPOLOGY_SUBJECT {
1869 if let Some(lifecycle) = self.site_lifecycle.get() {
1870 std::borrow::Cow::Owned(
1871 crate::console::topology::with_sites(
1872 &payload,
1873 lifecycle,
1874 self.console.device(),
1875 )
1876 .map_err(|error| crate::console::ConsoleError::Server {
1877 subject: subject.into(),
1878 source: site_services::site_error(error),
1879 })?,
1880 )
1881 } else {
1882 payload
1883 }
1884 } else {
1885 payload
1886 };
1887 let envelope = MessageEnvelope::new(
1888 ProtocolSchemaId::new([0_u8; ProtocolSchemaId::WIRE_LEN]),
1889 liminal::protocol::CausalContext::independent(),
1890 payload.to_vec(),
1891 );
1892 self.publish_with_origin(
1893 subject,
1894 &envelope,
1895 None,
1896 PublishOrigin::Server {
1897 device: self.console.device(),
1898 },
1899 )
1900 .map(|_published| ())
1901 .map_err(|source| crate::console::ConsoleError::Server {
1902 subject: subject.to_owned(),
1903 source,
1904 })
1905 }
1906
1907 fn retire_console_channel(&self, subject: &str) -> Result<(), crate::console::ConsoleError> {
1910 if !super::channel_registry::is_console_subject(subject) {
1911 return Ok(());
1912 }
1913 let mut channels =
1914 self.write_roster()
1915 .map_err(|error| crate::console::ConsoleError::Server {
1916 subject: subject.to_owned(),
1917 source: ServerError::ListenerAccept {
1918 message: error.to_string(),
1919 },
1920 })?;
1921 channels.remove(subject);
1922 drop(channels);
1923 Ok(())
1924 }
1925}
1926
1927struct InstalledParticipant {
1931 service: InstalledParticipantService,
1932 unloadable_conversations: UnloadableConversationRecord,
1933 credential_reissuer: Arc<dyn OperatorCredentialReissuer>,
1934 warm: ParticipantWarm,
1935}
1936
1937fn install_participant(
1947 durable_store: &Arc<dyn DurableStore>,
1948 participant: &ParticipantConfig,
1949 signals: &ServerSignals,
1950) -> Result<InstalledParticipant, ServerError> {
1951 let handler = ProductionParticipantHandler::new(Arc::clone(durable_store), *participant)
1952 .map_err(|error| ServerError::ParticipantStartupRestore {
1953 message: error.to_string(),
1954 })?;
1955 handler.install_signals(signals.clone()).map_err(|error| {
1961 ServerError::ParticipantStartupRestore {
1962 message: error.to_string(),
1963 }
1964 })?;
1965 let unloadable_conversations = handler.unloadable_record();
1966 let handler = Arc::new(handler);
1967 let warm = handler
1968 .install_warm()
1969 .map_err(|error| ServerError::ParticipantStartupRestore {
1970 message: error.to_string(),
1971 })?;
1972 let credential_reissuer: Arc<dyn OperatorCredentialReissuer> =
1976 Arc::<ProductionParticipantHandler>::clone(&handler);
1977 let service = InstalledParticipantService::new(
1978 handler,
1979 Arc::clone(durable_store),
1980 participant.wire_frame_limit,
1981 )
1982 .map_err(|error| ServerError::ConfigValidation {
1983 message: format!(
1984 "participant.wire_frame_limit: {} is below the protocol's minimum \
1985 complete participant frame ({error:?})",
1986 participant.wire_frame_limit
1987 ),
1988 })?;
1989 Ok(InstalledParticipant {
1990 service,
1991 unloadable_conversations,
1992 credential_reissuer,
1993 warm,
1994 })
1995}
1996
1997fn console_hub(
2004 device: &str,
2005 channels: &HashMap<String, Arc<ConfiguredChannel>>,
2006 depot: bool,
2007 lys: Option<&crate::config::types::LysConfig>,
2008) -> Result<crate::console::ConsoleHub, ServerError> {
2009 let this = crate::console::ThisDevice {
2010 id: device.to_owned(),
2011 liminal: env!("CARGO_PKG_VERSION").to_owned(),
2012 started: Instant::now(),
2013 depot,
2014 };
2015 let lines = channels.iter().map(|(name, configured)| {
2016 let state = match configured.state() {
2017 ChannelState::Active => crate::console::records::LineState::Live,
2018 ChannelState::Quiesced { .. } => crate::console::records::LineState::Quiesced,
2019 };
2020 (name.clone(), line_life(configured.durable), state)
2021 });
2022 let mut model = crate::console::TopologyModel::new(this, [], lines, [], []);
2023 if let Some(machine) = lys.and_then(|lys| lys.machine.as_ref()) {
2024 model = model.with_machine(machine);
2025 }
2026 crate::console::wall::WallBase::now()
2027 .and_then(|wall| crate::console::ConsoleHub::new(device, model, wall))
2028 .map_err(|error| ServerError::ListenerAccept {
2029 message: format!("console hub: {error}"),
2030 })
2031}
2032
2033fn status_of(configured: &ConfiguredChannel) -> ChannelStatus {
2036 let mode = configured.handle.config().mode;
2037 match configured.state() {
2038 ChannelState::Active => ChannelStatus::Active {
2039 origin: configured.origin,
2040 mode,
2041 schema: configured.protocol_schema,
2042 },
2043 ChannelState::Quiesced { reason } => ChannelStatus::Quiesced {
2044 reason,
2045 origin: configured.origin,
2046 mode,
2047 },
2048 }
2049}
2050
2051#[cfg(test)]
2052thread_local! {
2053 static ROSTER_READS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
2055}
2056
2057#[cfg(test)]
2059pub(crate) fn roster_reads() -> u64 {
2060 ROSTER_READS.with(std::cell::Cell::get)
2061}
2062
2063const fn line_life(durable: bool) -> crate::console::records::LineLife {
2065 if durable {
2066 crate::console::records::LineLife::Kept
2067 } else {
2068 crate::console::records::LineLife::Passing
2069 }
2070}
2071
2072const ROSTER_POISONED: &str = "channel roster lock poisoned";
2075
2076fn runtime_registered_count(channels: &HashMap<String, Arc<ConfiguredChannel>>) -> usize {
2084 channels
2085 .values()
2086 .filter(|configured| configured.origin == ChannelOrigin::RuntimeRegistered)
2087 .count()
2088}
2089
2090fn compare_registration(
2110 existing: &ConfiguredChannel,
2111 spec: &ChannelRegistration,
2112 resolved: &ChannelSchema,
2113) -> Result<Registered, ChannelRegistryError> {
2114 let requested_mode = if spec.durable {
2115 ChannelMode::Durable
2116 } else {
2117 ChannelMode::Ephemeral
2118 };
2119 let config = existing.handle.config();
2120 let mismatch = if config.mode == requested_mode {
2121 if existing.protocol_schema == resolved.protocol_id {
2122 if *config.schema.definition() == resolved.document {
2123 None
2124 } else {
2125 Some(ChannelConfigField::SchemaDocument)
2126 }
2127 } else {
2128 Some(ChannelConfigField::SchemaId)
2129 }
2130 } else {
2131 Some(ChannelConfigField::Mode)
2132 };
2133 mismatch.map_or(Ok(Registered::AlreadyIdentical), |field| {
2134 Err(ChannelRegistryError::AlreadyRegistered {
2135 name: spec.name.clone(),
2136 field,
2137 })
2138 })
2139}
2140
2141fn schema_of(configured: &ConfiguredChannel) -> ChannelSchema {
2145 ChannelSchema {
2146 document: configured.handle.config().schema.definition().clone(),
2147 protocol_id: configured.protocol_schema,
2148 }
2149}
2150
2151fn build_configured_channel(
2175 name: &str,
2176 resolved: ChannelSchema,
2177 durable: bool,
2178 origin: ChannelOrigin,
2179 durable_store: &Arc<dyn DurableStore>,
2180 supervisor: &ChannelSupervisor,
2181) -> Result<ConfiguredChannel, ChannelBuildError> {
2182 let schema =
2183 Schema::new(resolved.document).map_err(|error| ChannelBuildError::SchemaRejected {
2184 message: error.to_string(),
2185 })?;
2186 let channel_config = if durable {
2187 ChannelConfig::new(name.to_owned(), schema, ChannelMode::Durable)
2188 } else {
2189 ChannelConfig::new(name.to_owned(), schema, ChannelMode::Ephemeral)
2190 };
2191 let handle = if durable {
2192 ChannelHandle::new_durable_with_supervisor(
2193 channel_config,
2194 Arc::clone(durable_store),
2195 supervisor.clone(),
2196 )
2197 .map_err(|error| ChannelBuildError::DurableInitFailed {
2198 message: error.to_string(),
2199 })?
2200 } else {
2201 ChannelHandle::with_supervisor(channel_config, supervisor.clone())
2202 };
2203 Ok(ConfiguredChannel {
2204 handle,
2205 protocol_schema: resolved.protocol_id,
2206 durable,
2207 origin,
2208 state: AtomicU8::new(STATE_ACTIVE),
2209 quiesce_reason: OnceLock::new(),
2210 })
2211}
2212
2213fn dedup_timestamp_millis() -> u64 {
2219 use std::time::{SystemTime, UNIX_EPOCH};
2220 SystemTime::now()
2221 .duration_since(UNIX_EPOCH)
2222 .ok()
2223 .and_then(|duration| u64::try_from(duration.as_millis()).ok())
2224 .unwrap_or(0)
2225}
2226
2227const DEFAULT_SHARD_COUNT: usize = 8;
2234
2235const THROWAWAY_SHARD_COUNT: usize = 1;
2238
2239const DELIVERY_DEDUP_NAMESPACE: &str = "liminal:delivery-dedup";
2245
2246fn ephemeral_dedup_namespace() -> String {
2259 use std::time::{SystemTime, UNIX_EPOCH};
2260 let boot_nanos = SystemTime::now()
2261 .duration_since(UNIX_EPOCH)
2262 .map_or(0, |since| since.as_nanos());
2263 format!(
2264 "{DELIVERY_DEDUP_NAMESPACE}:incarnation:{}-{boot_nanos}",
2265 std::process::id()
2266 )
2267}
2268
2269fn combined_store_counts(
2272 durable_store: &Arc<dyn DurableStore>,
2273 ephemeral_store: &Arc<dyn DurableStore>,
2274) -> StoreCountsHandle {
2275 let durable = durable_store.store_counts().unwrap_or_default();
2276 if Arc::ptr_eq(durable_store, ephemeral_store) {
2277 return durable;
2278 }
2279 durable.with(&ephemeral_store.store_counts().unwrap_or_default())
2280}
2281
2282fn ephemeral_dedup_store(
2291 durable_store: &Arc<dyn DurableStore>,
2292 history: StoreHistory,
2293 subsystems: &dyn SubsystemFactory,
2294) -> Arc<dyn DurableStore> {
2295 match (history, durable_store) {
2296 #[cfg(any(test, feature = "test-support"))]
2297 (StoreHistory::Fresh, store) => Arc::clone(store),
2298 (StoreHistory::Created | StoreHistory::Recorded, _) => Arc::new(CountingStore::new(
2299 DeferredStore::new(subsystems.throwaway_store()),
2300 )),
2301 }
2302}
2303
2304#[cfg(test)]
2309#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
2310pub(super) enum SchedulerSubsystem {
2311 ChannelSupervisor,
2313 ConversationSupervisor,
2315 HaematiteStore,
2317}
2318
2319pub(super) trait SubsystemFactory {
2332 fn channel_cluster(
2338 &self,
2339 cluster_config: Option<&ClusterConfig>,
2340 ) -> Result<ChannelCluster, ServerError>;
2341
2342 fn conversation_supervisor(&self) -> Result<Arc<ConversationSupervisor>, ServerError>;
2348
2349 fn durable_store(&self, persistence_path: &Path) -> Result<Arc<dyn DurableStore>, ServerError>;
2354
2355 fn throwaway_store(&self) -> ThrowawayStoreOpener;
2359
2360 fn ephemeral_sweep_counts(&self) -> Option<EphemeralSweepCounts> {
2361 None
2362 }
2363}
2364
2365#[cfg(any(test, feature = "test-support"))]
2367pub(super) struct ProductionSubsystems;
2368
2369#[cfg(any(test, feature = "test-support"))]
2370impl SubsystemFactory for ProductionSubsystems {
2371 fn channel_cluster(
2372 &self,
2373 cluster_config: Option<&ClusterConfig>,
2374 ) -> Result<ChannelCluster, ServerError> {
2375 build_channel_cluster(cluster_config)
2376 }
2377
2378 fn conversation_supervisor(&self) -> Result<Arc<ConversationSupervisor>, ServerError> {
2379 Ok(Arc::new(ConversationSupervisor::new().map_err(
2380 |error| ServerError::ConfigValidation {
2381 message: format!("failed to start conversation supervisor: {error}"),
2382 },
2383 )?))
2384 }
2385
2386 fn durable_store(&self, persistence_path: &Path) -> Result<Arc<dyn DurableStore>, ServerError> {
2387 build_durable_store(persistence_path)
2388 }
2389
2390 fn throwaway_store(&self) -> ThrowawayStoreOpener {
2391 Box::new(|| open_ephemeral(THROWAWAY_SHARD_COUNT))
2392 }
2393}
2394
2395struct RootedProductionSubsystems {
2396 root: Arc<EphemeralRoot>,
2397}
2398
2399impl RootedProductionSubsystems {
2400 fn new(path: &Path) -> Result<Self, ServerError> {
2401 let root = EphemeralRoot::open(path).map_err(|error| ServerError::ConfigValidation {
2402 message: error.to_string(),
2403 })?;
2404 Ok(Self { root })
2405 }
2406}
2407
2408impl SubsystemFactory for RootedProductionSubsystems {
2409 fn channel_cluster(
2410 &self,
2411 config: Option<&ClusterConfig>,
2412 ) -> Result<ChannelCluster, ServerError> {
2413 build_channel_cluster(config)
2414 }
2415
2416 fn conversation_supervisor(&self) -> Result<Arc<ConversationSupervisor>, ServerError> {
2417 Ok(Arc::new(ConversationSupervisor::new().map_err(
2418 |error| ServerError::ConfigValidation {
2419 message: format!("failed to start conversation supervisor: {error}"),
2420 },
2421 )?))
2422 }
2423
2424 fn durable_store(&self, path: &Path) -> Result<Arc<dyn DurableStore>, ServerError> {
2425 build_durable_store(path)
2426 }
2427
2428 fn throwaway_store(&self) -> ThrowawayStoreOpener {
2429 let root = Arc::clone(&self.root);
2430 Box::new(move || root.open_store(THROWAWAY_SHARD_COUNT))
2431 }
2432
2433 fn ephemeral_sweep_counts(&self) -> Option<EphemeralSweepCounts> {
2434 Some(self.root.sweep_counts())
2435 }
2436}
2437
2438#[cfg(test)]
2442pub(super) mod subsystem_census {
2443 use std::path::{Path, PathBuf};
2444 use std::sync::Arc;
2445 use std::sync::atomic::{AtomicUsize, Ordering};
2446
2447 use liminal::conversation::ConversationSupervisor;
2448 use liminal::durability::{DurableStore, open_ephemeral_rooted};
2449
2450 use super::{
2451 ChannelCluster, ProductionSubsystems, SchedulerSubsystem, SubsystemFactory,
2452 THROWAWAY_SHARD_COUNT, ThrowawayStoreOpener, build_durable_store,
2453 };
2454 use crate::ServerError;
2455 use crate::config::types::ClusterConfig;
2456
2457 pub struct RecordingSubsystems {
2465 census: [AtomicUsize; 3],
2467 ephemeral_root: PathBuf,
2468 }
2469
2470 impl RecordingSubsystems {
2471 pub fn rooted(ephemeral_root: &Path) -> Self {
2473 Self {
2474 census: [
2475 AtomicUsize::new(0),
2476 AtomicUsize::new(0),
2477 AtomicUsize::new(0),
2478 ],
2479 ephemeral_root: ephemeral_root.to_path_buf(),
2480 }
2481 }
2482
2483 pub fn recorded(&self) -> Vec<SchedulerSubsystem> {
2486 KINDS
2487 .iter()
2488 .zip(&self.census)
2489 .flat_map(|(kind, count)| std::iter::repeat_n(*kind, count.load(Ordering::SeqCst)))
2490 .collect()
2491 }
2492
2493 fn record(&self, subsystem: SchedulerSubsystem) {
2494 if let Some(count) = KINDS
2495 .iter()
2496 .position(|kind| *kind == subsystem)
2497 .and_then(|index| self.census.get(index))
2498 {
2499 count.fetch_add(1, Ordering::SeqCst);
2500 }
2501 }
2502 }
2503
2504 const KINDS: [SchedulerSubsystem; 3] = [
2506 SchedulerSubsystem::ChannelSupervisor,
2507 SchedulerSubsystem::ConversationSupervisor,
2508 SchedulerSubsystem::HaematiteStore,
2509 ];
2510
2511 impl SubsystemFactory for RecordingSubsystems {
2512 fn channel_cluster(
2513 &self,
2514 cluster_config: Option<&ClusterConfig>,
2515 ) -> Result<ChannelCluster, ServerError> {
2516 let cluster = ProductionSubsystems.channel_cluster(cluster_config)?;
2517 self.record(SchedulerSubsystem::ChannelSupervisor);
2518 Ok(cluster)
2519 }
2520
2521 fn conversation_supervisor(&self) -> Result<Arc<ConversationSupervisor>, ServerError> {
2522 let supervisor = ProductionSubsystems.conversation_supervisor()?;
2523 self.record(SchedulerSubsystem::ConversationSupervisor);
2524 Ok(supervisor)
2525 }
2526
2527 fn durable_store(
2528 &self,
2529 persistence_path: &Path,
2530 ) -> Result<Arc<dyn DurableStore>, ServerError> {
2531 let store = build_durable_store(persistence_path)?;
2532 self.record(SchedulerSubsystem::HaematiteStore);
2533 Ok(store)
2534 }
2535
2536 fn throwaway_store(&self) -> ThrowawayStoreOpener {
2537 let root = self.ephemeral_root.clone();
2538 Box::new(move || open_ephemeral_rooted(&root, THROWAWAY_SHARD_COUNT))
2539 }
2540 }
2541}
2542
2543fn require_full_profile(config: &ServerConfig) -> Result<(), ServerError> {
2550 match config.services.profile()? {
2551 ServiceProfile::Full => Ok(()),
2552 ServiceProfile::WorkerFrontDoor => Err(ServerError::ConfigValidation {
2553 message: format!(
2554 "services.profile: \"{}\" cannot construct the full LiminalConnectionServices; \
2555 build profile-selected services via build_connection_services",
2556 ServiceProfile::WORKER_FRONT_DOOR
2557 ),
2558 }),
2559 }
2560}
2561
2562pub fn build_connection_services(
2579 config: &ServerConfig,
2580) -> Result<Arc<dyn ConnectionServices>, ServerError> {
2581 match config.services.profile()? {
2582 ServiceProfile::Full => {
2583 let subsystems = RootedProductionSubsystems::new(required_persistence_path(config)?)?;
2584 build_connection_services_via(config, &subsystems)
2585 }
2586 ServiceProfile::WorkerFrontDoor => build_worker_front_door(config),
2587 }
2588}
2589
2590pub(super) fn build_connection_services_via(
2606 config: &ServerConfig,
2607 subsystems: &dyn SubsystemFactory,
2608) -> Result<Arc<dyn ConnectionServices>, ServerError> {
2609 match config.services.profile()? {
2610 ServiceProfile::Full => {
2611 let path = required_persistence_path(config)?;
2612 let history = StoreHistory::of(path);
2613 let store = subsystems.durable_store(path)?;
2614 Ok(Arc::new(
2615 LiminalConnectionServices::from_config_with_store_via(
2616 config, store, history, subsystems,
2617 )?,
2618 ))
2619 }
2620 ServiceProfile::WorkerFrontDoor => build_worker_front_door(config),
2621 }
2622}
2623
2624fn build_worker_front_door(
2625 config: &ServerConfig,
2626) -> Result<Arc<dyn ConnectionServices>, ServerError> {
2627 let errors = crate::config::validation::worker_front_door_field_errors(config);
2628 if !errors.is_empty() {
2629 return Err(ServerError::ConfigValidation {
2630 message: errors.join("; "),
2631 });
2632 }
2633 Ok(Arc::new(WorkerFrontDoorServices::new()))
2634}
2635
2636pub(crate) fn open_persistent_store(
2638 persistence_path: &Path,
2639) -> Result<Arc<dyn DurableStore>, ServerError> {
2640 build_durable_store(persistence_path)
2641}
2642
2643const SPINE_COMMIT_THRESHOLD_BYTES: NonZeroU64 = match NonZeroU64::new(1 << 20) {
2656 Some(threshold) => threshold,
2657 None => unreachable!(),
2658};
2659
2660const SPINE_COMMIT_POLICY: CommitPolicy = CommitPolicy::buffered(SPINE_COMMIT_THRESHOLD_BYTES);
2662
2663fn build_durable_store(path: &Path) -> Result<Arc<dyn DurableStore>, ServerError> {
2664 let data_dir = durable_data_dir(path);
2665 let database = open_or_create_database(&data_dir)?;
2666 let event_store = EventStore::with_commit_policy(database, SPINE_COMMIT_POLICY);
2667 Ok(Arc::new(CountingStore::new(HaematiteStore::new(Arc::new(
2668 event_store,
2669 )))))
2670}
2671
2672pub(super) fn durable_data_dir(persistence_path: &Path) -> PathBuf {
2674 persistence_path.join("durability")
2675}
2676
2677pub(super) fn database_exists(data_dir: &Path) -> bool {
2680 database_config_file(data_dir).exists()
2681}
2682
2683const UNTOUCHED_DATABASE_ENTRIES: [&str; 2] = ["config.json", "writer.lock"];
2687
2688pub(super) fn database_untouched(data_dir: &Path) -> bool {
2692 std::fs::read_dir(data_dir).is_ok_and(|entries| {
2693 entries.into_iter().all(|entry| {
2694 entry.is_ok_and(|entry| {
2695 entry
2696 .file_name()
2697 .to_str()
2698 .is_some_and(|name| UNTOUCHED_DATABASE_ENTRIES.contains(&name))
2699 })
2700 })
2701 })
2702}
2703
2704fn database_config_file(data_dir: &Path) -> PathBuf {
2705 data_dir.join("config.json")
2706}
2707
2708pub(super) const DURABLE_RETENTION: Retention = Retention::reclaim(0);
2713
2714fn open_or_create_database(data_dir: &Path) -> Result<Database, ServerError> {
2716 let config_file = database_config_file(data_dir);
2717 let result = if config_file.exists() {
2718 adopt_missing_config_keys(data_dir, &config_file)?;
2719 Database::open_with_retention(
2720 data_dir,
2721 haematite::RuntimeOptions {
2722 long_wait_interval: std::time::Duration::from_secs(5),
2723 reclaim_start: haematite::ReclaimStart::Held,
2726 },
2727 DURABLE_RETENTION,
2728 )
2729 } else {
2730 Database::create(
2731 DatabaseConfig {
2732 data_dir: data_dir.to_path_buf(),
2733 shard_count: DEFAULT_SHARD_COUNT,
2734 distributed: None,
2735 executor_threads: None,
2736 node_cache_budget: Some(haematite::NodeCacheBudget::Unlimited),
2745 retention: DURABLE_RETENTION,
2746 },
2747 haematite::RuntimeOptions {
2748 long_wait_interval: std::time::Duration::from_secs(5),
2749 reclaim_start: haematite::ReclaimStart::Held,
2752 },
2753 )
2754 };
2755 let database = result.map_err(|error| ServerError::ConfigValidation {
2756 message: format!(
2757 "failed to open durable store at {}: {error}",
2758 data_dir.display()
2759 ),
2760 })?;
2761 liminal::durability::record_store_opened();
2762 super::reclaim_placement::arm_watch(&database);
2763 Ok(database)
2764}
2765
2766const ADOPTED_CONFIG_KEYS: [(&str, &str); 2] = [
2778 ("node_cache_budget", "unlimited"),
2779 ("retention", "keep_all"),
2780];
2781
2782fn adopt_missing_config_keys(data_dir: &Path, config_file: &Path) -> Result<(), ServerError> {
2800 let Ok(bytes) = std::fs::read(config_file) else {
2801 return Ok(());
2802 };
2803 let Ok(mut config) = serde_json::from_slice::<serde_json::Value>(&bytes) else {
2804 return Ok(());
2805 };
2806 let Some(object) = config.as_object_mut() else {
2807 return Ok(());
2808 };
2809 let mut adopted = Vec::new();
2810 for (key, value) in ADOPTED_CONFIG_KEYS {
2811 if !object.contains_key(key) {
2812 object.insert(key.to_owned(), serde_json::Value::String(value.to_owned()));
2813 adopted.push(key);
2814 }
2815 }
2816 if adopted.is_empty() {
2817 return Ok(());
2818 }
2819 let keys = adopted.join(", ");
2820 let migrated =
2821 serde_json::to_vec_pretty(&config).map_err(|error| ServerError::ConfigValidation {
2822 message: format!(
2823 "durable store config at {} predates `{keys}` and its migration could not be \
2824 serialized: {error}",
2825 config_file.display()
2826 ),
2827 })?;
2828 let write_error = |step: &str, error: std::io::Error| ServerError::ConfigValidation {
2829 message: format!(
2830 "durable store config at {} predates `{keys}` and migrating it failed at {step}: \
2831 {error}",
2832 config_file.display()
2833 ),
2834 };
2835 let tmp_file = data_dir.join("config.json.migrate-tmp");
2836 let mut tmp = std::fs::File::create(&tmp_file)
2837 .map_err(|error| write_error("creating the temporary file", error))?;
2838 std::io::Write::write_all(&mut tmp, &migrated)
2839 .map_err(|error| write_error("writing the temporary file", error))?;
2840 tmp.sync_all()
2841 .map_err(|error| write_error("syncing the temporary file", error))?;
2842 drop(tmp);
2843 std::fs::rename(&tmp_file, config_file)
2844 .map_err(|error| write_error("renaming over config.json", error))?;
2845 std::fs::File::open(data_dir)
2846 .and_then(|dir| dir.sync_all())
2847 .map_err(|error| write_error("syncing the data directory", error))?;
2848 tracing::warn!(
2849 config = %config_file.display(),
2850 keys = %keys,
2851 "durable store config predates these keys -- wrote the create-arm values (the \
2852 behaviour-preserving ones) so the store opens with no operator action"
2853 );
2854 Ok(())
2855}
2856
2857pub(super) fn reserved_namespace_refusal(channel: &str) -> Option<ServerError> {
2858 if channel.starts_with("liminal.") {
2859 crate::metrics::self_stream_reserved_publication();
2860 Some(ServerError::ReservedNamespace {
2861 channel: channel.to_owned(),
2862 })
2863 } else {
2864 None
2865 }
2866}
2867
2868impl LiminalConnectionServices {
2869 pub(in crate::server) const fn signals(&self) -> &ServerSignals {
2870 &self.signals
2871 }
2872
2873 pub(in crate::server) fn self_stream_head(&self, name: &str) -> Result<u64, ServerError> {
2874 let channels = self.read_channels()?;
2875 let channel = channels
2876 .get(name)
2877 .ok_or_else(|| ServerError::ListenerAccept {
2878 message: format!("self stream '{name}' is absent after append"),
2879 })?;
2880 let heads =
2881 channel
2882 .handle
2883 .durable_positions()
2884 .map_err(|error| ServerError::ListenerAccept {
2885 message: format!("self stream '{name}' head is unavailable: {error}"),
2886 })?;
2887 drop(channels);
2888 match heads.as_slice() {
2889 [head] => Ok(*head),
2890 _ => Err(ServerError::ListenerAccept {
2891 message: format!("self stream '{name}' must have exactly one durable partition"),
2892 }),
2893 }
2894 }
2895
2896 pub(in crate::server) fn publish_self_stream_batch(
2897 &self,
2898 payloads: &[Vec<u8>],
2899 publisher: &str,
2900 ) -> Result<u64, ServerError> {
2901 if publisher.trim().is_empty() || payloads.is_empty() {
2902 return Err(ServerError::ConfigValidation {
2903 message: "self-stream batch requires a publisher and rows".to_owned(),
2904 });
2905 }
2906 let configured = self
2907 .admit_channel("liminal.events")
2908 .map_err(|error| access_to_server_error(&error))?;
2909 let deliveries = configured
2910 .handle
2911 .publish_batch_with_delivery(
2912 payloads,
2913 &liminal::envelope::PublisherId::new(publisher),
2914 None,
2915 )
2916 .map_err(|error| match error {
2917 liminal::error::LiminalError::PublishUnresolved { append } => {
2918 ServerError::PublishUnresolved {
2919 channel: "liminal.events".to_owned(),
2920 idempotency_key: None,
2921 sequence: Some(append.sequence()),
2922 message: append.to_string(),
2923 }
2924 }
2925 error => ServerError::ListenerAccept {
2926 message: format!("self-stream receipt batch failed: {error}"),
2927 },
2928 })?;
2929 if deliveries.len() != payloads.len()
2930 || deliveries
2931 .iter()
2932 .any(|delivery| delivery.durable_position().is_none())
2933 {
2934 return Err(ServerError::ListenerAccept {
2935 message: "self-stream receipt batch was refused before append".to_owned(),
2936 });
2937 }
2938 for pair in deliveries.windows(2) {
2939 if pair[0]
2940 .durable_position()
2941 .and_then(|position| position.checked_add(1))
2942 != pair[1].durable_position()
2943 {
2944 return Err(ServerError::ListenerAccept {
2945 message: "self-stream receipt batch positions are not contiguous".to_owned(),
2946 });
2947 }
2948 }
2949 for delivery in &deliveries {
2950 Self::record_delivery_counts(delivery);
2951 }
2952 deliveries
2953 .last()
2954 .and_then(ChannelDelivery::durable_position)
2955 .and_then(|position| position.checked_add(1))
2956 .ok_or_else(|| ServerError::ListenerAccept {
2957 message: "self-stream receipt batch head is unavailable".to_owned(),
2958 })
2959 }
2960
2961 pub(in crate::server) fn checkpoint_self_stream_heads(
2962 &self,
2963 heads: [u64; 2],
2964 ) -> Result<(), ServerError> {
2965 for (name, head) in ["liminal.events:0", "liminal.cost:0"]
2966 .into_iter()
2967 .zip(heads)
2968 {
2969 block_on(liminal::durability::head_checkpoint::advance_head(
2970 self.durable_store.as_ref(),
2971 name,
2972 head,
2973 ))
2974 .map_err(|error| ServerError::ShutdownFlush {
2975 message: format!("self-stream head checkpoint bridge for '{name}' failed: {error}"),
2976 })?
2977 .map_err(|error| ServerError::ShutdownFlush {
2978 message: format!("self-stream head checkpoint for '{name}' failed: {error}"),
2979 })?;
2980 }
2981 Ok(())
2982 }
2983
2984 pub(in crate::server) fn install_self_stream_channels(&self) -> Result<(), ServerError> {
2985 let names = ["liminal.cost", "liminal.events"];
2986 {
2987 let channels = self.read_channels()?;
2988 for name in names {
2989 if channels.contains_key(name) {
2990 return Err(ServerError::ConfigValidation {
2991 message: format!("self-stream channel '{name}' is already configured"),
2992 });
2993 }
2994 }
2995 }
2996 let mut built = Vec::with_capacity(names.len());
2997 for name in names {
2998 let schema =
2999 resolve_schema_bytes(None).map_err(|error| ServerError::ConfigValidation {
3000 message: format!("self-stream channel '{name}' schema failed: {error}"),
3001 })?;
3002 let channel = build_configured_channel(
3003 name,
3004 schema,
3005 true,
3006 ChannelOrigin::BootConfigured,
3007 &self.durable_store,
3008 self.cluster.supervisor(),
3009 )
3010 .map_err(|error| ServerError::ConfigValidation {
3011 message: error.boot_message(name),
3012 })?;
3013 channel
3014 .handle
3015 .install_publication_identity(self.publication_identity.clone())
3016 .map_err(|error| ServerError::ConfigValidation {
3017 message: error.to_string(),
3018 })?;
3019 built.push((name.to_owned(), Arc::new(channel)));
3020 }
3021 let mut channels =
3022 self.channels
3023 .write()
3024 .map_err(|error| ServerError::ConfigValidation {
3025 message: format!("self-stream channel roster lock poisoned: {error}"),
3026 })?;
3027 for (name, _) in &built {
3028 if channels.contains_key(name) {
3029 return Err(ServerError::ConfigValidation {
3030 message: format!("self-stream channel '{name}' was concurrently configured"),
3031 });
3032 }
3033 }
3034 channels.extend(built);
3035 drop(channels);
3036 for name in names {
3039 self.console_changed(crate::console::TopologyChange::Channel {
3040 name: name.to_owned(),
3041 life: line_life(true),
3042 state: crate::console::records::LineState::Live,
3043 });
3044 }
3045 Ok(())
3046 }
3047
3048 pub(in crate::server) fn install_console_channels(&self) -> Result<(), ServerError> {
3053 self.register_console_channel(crate::console::TOPOLOGY_SUBJECT)
3054 .map_err(|error| ServerError::ConfigValidation {
3055 message: format!("console channel registration failed: {error}"),
3056 })
3057 }
3058
3059 fn register_console_channel(&self, name: &str) -> Result<(), ChannelRegistryError> {
3062 if self.read_roster()?.contains_key(name) {
3063 return Ok(());
3064 }
3065 let schema = resolve_schema_bytes(None).map_err(|error| {
3066 ChannelRegistryError::RosterUnavailable {
3067 message: format!("console channel '{name}' schema failed: {error}"),
3068 }
3069 })?;
3070 let channel = build_configured_channel(
3071 name,
3072 schema,
3073 false,
3074 ChannelOrigin::BootConfigured,
3075 &self.durable_store,
3076 self.cluster.supervisor(),
3077 )
3078 .map_err(|error| error.into_registry_error(name))?;
3079 channel
3080 .handle
3081 .install_publication_identity(self.publication_identity.clone())
3082 .map_err(|error| ChannelRegistryError::RosterUnavailable {
3083 message: error.to_string(),
3084 })?;
3085 self.write_roster()?
3086 .entry(name.to_owned())
3087 .or_insert_with(|| Arc::new(channel));
3088 Ok(())
3089 }
3090
3091 pub fn publish_embedded(
3103 &self,
3104 host: &str,
3105 channel: &str,
3106 envelope: &MessageEnvelope,
3107 idempotency_key: Option<&str>,
3108 ) -> Result<PublishOutcome, ServerError> {
3109 self.publish_with_origin(
3110 channel,
3111 envelope,
3112 idempotency_key,
3113 PublishOrigin::Host { name: host },
3114 )
3115 .map(|published| published.outcome)
3116 }
3117
3118 pub(in crate::server) fn publish_with_origin(
3119 &self,
3120 channel: &str,
3121 envelope: &MessageEnvelope,
3122 idempotency_key: Option<&str>,
3123 origin: PublishOrigin<'_>,
3124 ) -> Result<OriginPublication, ServerError> {
3125 if !origin.is_server() {
3126 if plan_subject(channel).is_some() {
3127 return Err(site_services::site_error(
3128 crate::services::lifecycle::site::SiteError::Invalid(
3129 "plan subject is server-owned",
3130 ),
3131 ));
3132 }
3133 crate::agents::binding::refuse_server_publish(channel)?;
3134 }
3135 validate_publication_origin(channel, idempotency_key, origin)?;
3136 let mut recipients = self.signals.lend_recipients();
3140 let received_at = recipients.is_some().then(Instant::now);
3144 let result = (|| {
3145 let configured = self
3154 .admit_channel(channel)
3155 .map_err(|error| access_to_server_error(&error))?;
3156
3157 if let (Some(key), ChannelMode::Durable) =
3168 (idempotency_key, configured.handle.config().mode)
3169 {
3170 return self
3171 .publish_durable_keyed(
3172 &configured,
3173 channel,
3174 (envelope, origin),
3175 key,
3176 recipients.as_mut(),
3177 )
3178 .map(OriginPublication::connection);
3179 }
3180 let dedup = self.dedup_for(&configured);
3181 if let Some(key) = idempotency_key {
3182 if !Self::claim_delivery(dedup, key)? {
3183 crate::metrics::publish_accepted();
3187 return Ok(OriginPublication::connection(PublishOutcome {
3188 message_id: self.next_message_id()?,
3189 delivered: false,
3190 durable_position: None,
3191 reach: Reach::Suppressed,
3192 }));
3193 }
3194 }
3195
3196 let delivery = fan_out(&configured, envelope, origin, recipients.as_mut());
3197 let delivery = match delivery {
3198 Ok(delivery) => delivery,
3199 Err(liminal::error::LiminalError::PublishUnresolved { append }) => {
3203 return Err(ServerError::PublishUnresolved {
3204 channel: channel.to_owned(),
3205 idempotency_key: None,
3206 sequence: Some(append.sequence()),
3207 message: append.to_string(),
3208 });
3209 }
3210 Err(error) => {
3211 if let Some(key) = idempotency_key {
3218 Self::release_claim(dedup, key);
3219 }
3220 return Err(ServerError::ListenerAccept {
3221 message: format!("liminal publish failed for channel '{channel}': {error}"),
3222 });
3223 }
3224 };
3225
3226 if let Some(key) = idempotency_key {
3243 if delivery.is_admitted() {
3244 block_on(dedup.complete_receipt(key, ProcessingReceipt::new(Vec::new())))
3245 .map_err(|error| ServerError::ListenerAccept {
3246 message: format!(
3247 "dedup receipt bridge failed for key '{key}': {error}"
3248 ),
3249 })?
3250 .map_err(|error| ServerError::ListenerAccept {
3251 message: format!("dedup receipt write failed for key '{key}': {error}"),
3252 })?;
3253 } else {
3254 Self::release_claim(dedup, key);
3255 }
3256 }
3257
3258 let self_row_at = delivery.durable_position().filter(|_| origin.is_server());
3259 Ok(OriginPublication {
3260 outcome: Self::delivered(&delivery)?,
3261 self_row_appended: self_row_at.is_some(),
3262 self_row_at,
3263 })
3264 })();
3265 self.signals.published(
3266 channel,
3267 origin,
3268 envelope,
3269 &result,
3270 recipients.as_ref().zip(received_at),
3271 );
3272 if let Some(buffer) = recipients {
3273 self.signals.give_back(buffer);
3274 }
3275 result
3276 }
3277}
3278
3279fn fan_out(
3282 configured: &ConfiguredChannel,
3283 envelope: &MessageEnvelope,
3284 origin: PublishOrigin<'_>,
3285 recipients: Option<&mut liminal::channel::PublishRecord>,
3286) -> Result<liminal::channel::ChannelDelivery, liminal::error::LiminalError> {
3287 let publisher = || publisher_for_origin(origin);
3288 recipients.map_or_else(
3289 || {
3290 configured
3291 .handle
3292 .publish_with_delivery(&envelope.payload, publisher(), None)
3293 },
3294 |recipients| {
3295 configured.handle.publish_recording(
3296 &envelope.payload,
3297 publisher(),
3298 None,
3299 None,
3300 recipients,
3301 )
3302 },
3303 )
3304}
3305
3306fn validate_publication_origin(
3307 channel: &str,
3308 idempotency_key: Option<&str>,
3309 origin: PublishOrigin<'_>,
3310) -> Result<(), ServerError> {
3311 if !origin.is_server() {
3312 refuse_console_namespace(channel).map_err(|error| access_to_server_error(&error))?;
3313 if let Some(error) = reserved_namespace_refusal(channel) {
3314 return Err(error);
3315 }
3316 }
3317 if let PublishOrigin::Host { name } = origin
3318 && name.trim().is_empty()
3319 {
3320 return Err(ServerError::ConfigValidation {
3321 message: "embedded publication requires a nonempty host name".to_owned(),
3322 });
3323 }
3324 if let PublishOrigin::Server { device } = origin {
3325 if device.trim().is_empty() || idempotency_key.is_some() {
3326 return Err(ServerError::ConfigValidation {
3327 message: "server-origin publication requires a device and no idempotency key"
3328 .to_owned(),
3329 });
3330 }
3331 }
3332 Ok(())
3333}
3334
3335fn publisher_for_origin(origin: PublishOrigin<'_>) -> liminal::envelope::PublisherId {
3336 match origin {
3337 PublishOrigin::Connection { id } => liminal::envelope::PublisherId::new(id.to_string()),
3338 PublishOrigin::Host { name } => {
3339 liminal::envelope::PublisherId::new(format!("liminal.{name}"))
3340 }
3341 PublishOrigin::Server { device } => {
3342 liminal::envelope::PublisherId::new(format!("liminal.{device}"))
3343 }
3344 }
3345}
3346
3347impl ConnectionServices for LiminalConnectionServices {
3348 fn construction_request(
3349 &self,
3350 session: Option<&dyn crate::auth_lys::session::LysSession>,
3351 subject: &str,
3352 bytes: &[u8],
3353 at_ms: u64,
3354 ) -> Option<Result<serde_json::Value, ServerError>> {
3355 if let Some(answer) = self.agent_host_request(session, subject, bytes, at_ms) {
3356 return Some(answer);
3357 }
3358 is_site_route(subject).then(|| {
3359 self.serve_site_tool(session, subject, bytes, at_ms)
3360 .map_err(site_services::site_error)
3361 })
3362 }
3363 fn prepare_site_trial(
3364 &self,
3365 connection: u64,
3366 session: Option<&dyn crate::auth_lys::session::LysSession>,
3367 registration: &liminal::protocol::WorkerRegistration,
3368 process: Option<&crate::services::lifecycle::ProcessGrant>,
3369 at_ms: u64,
3370 ) -> Result<Option<crate::services::lifecycle::trial::TrialPermit>, ServerError> {
3371 self.site_lifecycle.get().map_or(Ok(None), |lifecycle| {
3372 lifecycle
3373 .prepare_trial(connection, session, registration, process, at_ms)
3374 .map_err(site_services::site_error)
3375 })
3376 }
3377 fn admit_site_call(
3378 &self,
3379 session: Option<&dyn crate::auth_lys::session::LysSession>,
3380 subject: &str,
3381 at_ms: u64,
3382 ) -> Result<bool, ServerError> {
3383 self.site_lifecycle.get().map_or(Ok(false), |lifecycle| {
3384 lifecycle
3385 .admit_trial_call(session, subject, at_ms)
3386 .map_err(site_services::site_error)
3387 })
3388 }
3389 fn site_registration_closed(&self, connection: u64) -> Result<(), ServerError> {
3390 if let Some(lifecycle) = self.site_lifecycle.get() {
3391 lifecycle
3392 .trial_closed(connection)
3393 .map_err(site_services::site_error)?;
3394 lifecycle
3395 .refresh_sites()
3396 .map_err(site_services::site_error)?;
3397 }
3398 Ok(())
3399 }
3400 fn publish_identified(
3401 &self,
3402 connection: u64,
3403 session: Option<&dyn crate::auth_lys::session::LysSession>,
3404 channel: &str,
3405 envelope: &MessageEnvelope,
3406 key: Option<&str>,
3407 ) -> Result<PublishOutcome, ServerError> {
3408 if plan_subject(channel).is_some() {
3409 return Err(site_services::site_error(
3410 crate::services::lifecycle::site::SiteError::Invalid(
3411 "plan subject is server-owned",
3412 ),
3413 ));
3414 }
3415 self.publish_agent(connection, session, channel, envelope, key)
3416 }
3417 fn agents(&self) -> Option<&Arc<crate::agents::runtime::Agents>> {
3418 self.agents.as_ref()
3419 }
3420
3421 fn subscribe_identified(
3422 &self,
3423 connection: u64,
3424 session: Option<&dyn crate::auth_lys::session::LysSession>,
3425 channel: &str,
3426 schemas: &[ProtocolSchemaId],
3427 install: Option<liminal::channel::InboxInstall>,
3428 ) -> Result<ConnectionSubscription, ServerError> {
3429 if plan_subject(channel).is_some() {
3430 return self.subscribe_site_plan(session, channel, schemas, install);
3431 }
3432 self.subscribe_agent(connection, session, channel, schemas, install)
3433 }
3434 fn participant_service(&self) -> Option<InstalledParticipantService> {
3435 self.participant_service.clone()
3436 }
3437
3438 fn admit_channel(
3465 &self,
3466 operation: ChannelOperation,
3467 channel: &str,
3468 ) -> Result<(), ChannelAccessError> {
3469 if let Some(name) = plan_subject(channel) {
3470 let refused = |message| ChannelAccessError::RosterUnavailable { message };
3471 if operation == ChannelOperation::Publish {
3472 return Err(refused("plan subject is server-owned".into()));
3473 }
3474 let lifecycle = self
3475 .site_lifecycle
3476 .get()
3477 .ok_or_else(|| refused("construction not installed".into()))?;
3478 lifecycle
3479 .record(name)
3480 .map_err(|error| refused(error.to_string()))?;
3481 return Ok(());
3482 }
3483 if let Some(admitted) = self.agent_channel_admission(channel) {
3484 return admitted;
3485 }
3486 match operation {
3487 ChannelOperation::Publish => refuse_console_namespace(channel)?,
3488 ChannelOperation::Subscribe => {
3489 refuse_console_tap_target(channel)?;
3490 self.refuse_console_tap_limit(channel)?;
3491 let admitted = if self.signals.console_enabled()
3494 && super::channel_registry::console_tap_target(channel).is_some()
3495 {
3496 self.register_console_channel(channel).map_err(|error| {
3497 ChannelAccessError::RosterUnavailable {
3498 message: error.to_string(),
3499 }
3500 })
3501 } else {
3502 Ok(())
3503 }
3504 .and_then(|()| Self::admit_channel(self, channel).map(|_| ()));
3505 if admitted.is_err() {
3506 self.give_back_tap(channel);
3507 }
3508 return admitted;
3509 }
3510 }
3511 Self::admit_channel(self, channel)?;
3512 Ok(())
3513 }
3514
3515 fn publish(
3516 &self,
3517 connection: u64,
3518 channel: &str,
3519 envelope: &MessageEnvelope,
3520 idempotency_key: Option<&str>,
3521 ) -> Result<PublishOutcome, ServerError> {
3522 refuse_console_namespace(channel).map_err(|error| access_to_server_error(&error))?;
3526 if plan_subject(channel).is_some() {
3527 return Err(site_services::site_error(
3528 crate::services::lifecycle::site::SiteError::Invalid(
3529 "plan subject is server-owned",
3530 ),
3531 ));
3532 }
3533 crate::agents::binding::refuse_server_publish(channel)?;
3534 if crate::agents::binding::subject(channel).is_some() {
3535 return Err(crate::agents::error::AgentError::Invalid(
3536 "agent publish requires the verified identity route",
3537 )
3538 .into());
3539 }
3540 self.publish_with_origin(
3541 channel,
3542 envelope,
3543 idempotency_key,
3544 PublishOrigin::Connection { id: connection },
3545 )
3546 .map(|published| published.outcome)
3547 }
3548
3549 fn subscribe(
3550 &self,
3551 channel: &str,
3552 accepted_schemas: &[ProtocolSchemaId],
3553 install: Option<liminal::channel::InboxInstall>,
3554 ) -> Result<ConnectionSubscription, ServerError> {
3555 if plan_subject(channel).is_some() {
3556 return Err(site_services::site_error(
3557 crate::services::lifecycle::site::SiteError::Invalid(
3558 "plan subscription needs an admitted pass",
3559 ),
3560 ));
3561 }
3562 if crate::agents::binding::subject(channel).is_some() {
3563 return Err(crate::agents::error::AgentError::Invalid(
3564 "agent subscribe requires the verified identity route",
3565 )
3566 .into());
3567 }
3568 let subscribed = self.open_subscription(channel, accepted_schemas, install, None);
3569 if subscribed.is_err() {
3570 self.give_back_tap(channel);
3574 }
3575 subscribed
3576 }
3577
3578 fn unsubscribe(&self, subscription: ConnectionSubscription) -> Result<(), ServerError> {
3579 subscription.unsubscribe()
3580 }
3581
3582 fn open_conversation(
3583 &self,
3584 conversation_id: u64,
3585 subject: &str,
3586 ) -> Result<ConnectionConversation, ServerError> {
3587 self.open_routed_conversation(conversation_id, subject)
3588 }
3589
3590 fn conversation_message(
3591 &self,
3592 conversation: &ConnectionConversation,
3593 envelope: &MessageEnvelope,
3594 op_id: Option<u64>,
3595 ) -> Result<(), ServerError> {
3596 self.routed_conversation_message(conversation, envelope, op_id)
3597 }
3598
3599 fn close_conversation(&self, conversation: ConnectionConversation) -> Result<(), ServerError> {
3600 conversation.close()
3601 }
3602
3603 fn listening(self: Arc<Self>) -> Option<std::thread::JoinHandle<()>> {
3604 self.resolve_claims_behind_listener()
3605 }
3606
3607 fn stop_participant_warm(&self) {
3608 if let Some(warm) = &self.participant_warm {
3613 warm.stop();
3614 warm.record_clean_stop();
3615 }
3616 }
3617
3618 fn finish_background_store_work(&self) -> Result<(), ServerError> {
3619 self.finish_background_work()
3620 }
3621
3622 fn release_store_holders(&self) -> Result<(), ServerError> {
3623 self.release_holders()
3624 }
3625
3626 fn flush_durable_state(&self) -> Result<(), ServerError> {
3627 self.signals.with_flush_barrier(|| {
3630 block_on(self.head_checkpoint_stop.checkpoint_heads())
3631 .map_err(|error| ServerError::ShutdownFlush {
3632 message: format!("clean-stop head checkpoint bridge failed: {error}"),
3633 })?
3634 .map_err(|error| ServerError::ShutdownFlush {
3635 message: format!("clean-stop head checkpoint failed: {error}"),
3636 })?;
3637 block_on(self.durable_store.flush())
3638 .map_err(|error| ServerError::ShutdownFlush {
3639 message: format!("shared durable store flush bridge failed: {error}"),
3640 })?
3641 .map_err(|error| ServerError::ShutdownFlush {
3642 message: format!("shared durable store flush failed: {error}"),
3643 })?;
3644 Ok(())
3645 })?;
3646 Ok(())
3647 }
3648}
3649
3650#[derive(Debug)]
3653pub(super) struct ConfiguredChannel {
3654 handle: ChannelHandle,
3655 protocol_schema: ProtocolSchemaId,
3656 durable: bool,
3658 origin: ChannelOrigin,
3662 state: AtomicU8,
3666 quiesce_reason: OnceLock<String>,
3671}
3672
3673impl ConfiguredChannel {
3674 fn state(&self) -> ChannelState {
3680 if self.state.load(Ordering::Acquire) == STATE_QUIESCED {
3681 ChannelState::Quiesced {
3682 reason: self.recorded_quiesce_reason(),
3683 }
3684 } else {
3685 ChannelState::Active
3686 }
3687 }
3688
3689 fn recorded_quiesce_reason(&self) -> String {
3692 self.quiesce_reason
3693 .get()
3694 .map_or_else(|| UNRECORDED_QUIESCE_REASON.to_owned(), Clone::clone)
3695 }
3696
3697 fn quiesce(&self, name: &str, reason: &str) -> Result<(), ChannelRegistryError> {
3706 let recorded = match self.quiesce_reason.set(reason.to_owned()) {
3712 Ok(()) => reason.to_owned(),
3713 Err(_rejected) => self.recorded_quiesce_reason(),
3714 };
3715 let _flipped_here = self.state.compare_exchange(
3720 STATE_ACTIVE,
3721 STATE_QUIESCED,
3722 Ordering::Release,
3723 Ordering::Acquire,
3724 );
3725 if recorded == reason {
3726 return Ok(());
3727 }
3728 Err(ChannelRegistryError::AlreadyQuiesced {
3729 name: name.to_owned(),
3730 reason: recorded,
3731 })
3732 }
3733}
3734
3735#[derive(Debug)]
3736struct LiminalSubscriptionResource {
3737 subscription: liminal::channel::SubscriptionHandle,
3738}
3739
3740impl SubscriptionResource for LiminalSubscriptionResource {
3741 fn try_next_positioned(
3742 &mut self,
3743 ) -> Result<Option<(liminal::envelope::Envelope, Option<u64>)>, ServerError> {
3744 self.subscription
3745 .try_next_positioned()
3746 .map_err(|error| subscription_failure(&error))
3747 }
3748
3749 fn acknowledge_position(&self, position: u64) -> Result<(), ServerError> {
3750 self.subscription
3751 .acknowledge_position(position)
3752 .map_err(|error| subscription_failure(&error))
3753 }
3754
3755 fn consumer_position(&self) -> Result<Option<u64>, ServerError> {
3756 self.subscription
3757 .consumer_position()
3758 .map_err(|error| subscription_failure(&error))
3759 }
3760 fn unsubscribe(self: Box<Self>) -> Result<(), ServerError> {
3761 drop(self.subscription);
3762 Ok(())
3763 }
3764
3765 fn is_overflowed(&self) -> bool {
3766 self.subscription.is_overflowed()
3767 }
3768
3769 fn subscriber_pid(&self) -> Option<u64> {
3770 Some(self.subscription.subscriber_pid())
3771 }
3772
3773 fn has_pending(&self) -> bool {
3774 self.subscription.has_pending()
3775 }
3776
3777 fn try_next(&mut self) -> Option<liminal::envelope::Envelope> {
3778 match self.subscription.try_next() {
3779 Ok(envelope) => envelope,
3780 Err(error) => {
3781 tracing::error!(
3782 %error,
3783 "subscription could not receive its next envelope"
3784 );
3785 None
3786 }
3787 }
3788 }
3789}
3790
3791fn subscription_failure(error: &liminal::error::LiminalError) -> ServerError {
3793 ServerError::ListenerAccept {
3794 message: format!("liminal subscription: {error}"),
3795 }
3796}
3797
3798pub(super) fn access_to_server_error(error: &ChannelAccessError) -> ServerError {
3820 let message = match error {
3821 ChannelAccessError::NotRegistered { name } => {
3822 format!("channel '{name}' is not configured")
3823 }
3824 ChannelAccessError::RosterUnavailable { message } => message.clone(),
3825 ChannelAccessError::Quiesced { .. }
3826 | ChannelAccessError::ConsoleNamespace { .. }
3827 | ChannelAccessError::ConsoleTapTarget { .. }
3828 | ChannelAccessError::ConsoleTapLimit { .. } => error.to_string(),
3829 };
3830 ServerError::ListenerAccept { message }
3831}
3832
3833fn refuse_console_tap_target(channel: &str) -> Result<(), ChannelAccessError> {
3839 use super::channel_registry::{console_tap_target, is_console_subject};
3840 match console_tap_target(channel) {
3841 Some(target) if target.is_empty() || is_console_subject(target) => {
3842 Err(ChannelAccessError::ConsoleTapTarget {
3843 name: channel.to_owned(),
3844 })
3845 }
3846 _ => Ok(()),
3847 }
3848}
3849
3850fn refuse_console_namespace(channel: &str) -> Result<(), ChannelAccessError> {
3855 if super::channel_registry::is_console_subject(channel) {
3856 return Err(ChannelAccessError::ConsoleNamespace {
3857 name: channel.to_owned(),
3858 });
3859 }
3860 Ok(())
3861}
3862
3863pub(super) fn server_error_from_protocol(error: &ProtocolError) -> ServerError {
3864 ServerError::ListenerAccept {
3865 message: format!("protocol operation failed: {error}"),
3866 }
3867}
3868
3869#[cfg(test)]
3872#[path = "services_registry_tests.rs"]
3873mod services_registry_tests;
3874
3875#[cfg(test)]
3876mod durable_store_tests {
3877 type TestResult = Result<(), Box<dyn std::error::Error>>;
3879
3880 trait Must<T> {
3882 fn must(self, what: &str) -> Result<T, String>;
3883 }
3884
3885 impl<T, E: std::fmt::Debug> Must<T> for Result<T, E> {
3886 fn must(self, what: &str) -> Result<T, String> {
3887 self.map_err(|error| format!("{what}: {error:?}"))
3888 }
3889 }
3890
3891 impl<T> Must<T> for Option<T> {
3892 fn must(self, what: &str) -> Result<T, String> {
3893 self.ok_or_else(|| what.to_owned())
3894 }
3895 }
3896
3897 use liminal::durability::open_ephemeral_rooted;
3898
3899 use super::subsystem_census::RecordingSubsystems;
3900 use super::{
3901 ConnectionServices, DEFAULT_SHARD_COUNT, LiminalConnectionServices, SchedulerSubsystem,
3902 build_connection_services, build_connection_services_via, build_durable_store,
3903 };
3904 use crate::ServerError;
3905 use crate::config::types::{LimitsConfig, ServerConfig, ServicesConfig};
3906
3907 fn entry_count(root: &std::path::Path) -> Result<usize, String> {
3910 Ok(std::fs::read_dir(root)
3911 .must("ephemeral root is readable")?
3912 .count())
3913 }
3914
3915 fn config_with_profile(profile: &str) -> Result<ServerConfig, String> {
3920 Ok(ServerConfig {
3921 listen_address: "127.0.0.1:0".parse().must("valid socket addr")?,
3922 extra_listen_addresses: Vec::new(),
3923 health_listen_address: "127.0.0.1:1".parse().must("valid socket addr")?,
3924 drain_timeout_ms: 30_000,
3925 channels: Vec::new(),
3926 routing_rules: Vec::new(),
3927 persistence_path: None,
3928 cluster: None,
3929 auth: None,
3930 services: ServicesConfig {
3931 profile: profile.to_owned(),
3932 ..ServicesConfig::default()
3933 },
3934 limits: LimitsConfig::default(),
3935 participant: None,
3936 websocket: None,
3937 })
3938 }
3939
3940 #[test]
3951 fn worker_front_door_builds_no_store_and_no_temp_dir() -> TestResult {
3952 let front_door_root = tempfile::tempdir().must("test can create an ephemeral root")?;
3953 let full_root = tempfile::tempdir().must("test can create an ephemeral root")?;
3954
3955 let front_door_subsystems = RecordingSubsystems::rooted(front_door_root.path());
3956 let front_door: std::sync::Arc<dyn ConnectionServices> = build_connection_services_via(
3957 &config_with_profile("worker-front-door")?,
3958 &front_door_subsystems,
3959 )
3960 .must("worker-front-door services build")?;
3961 assert!(
3962 !front_door.supports_channel_operations(),
3963 "the worker front door serves no channel operations"
3964 );
3965 assert_eq!(
3966 entry_count(front_door_root.path())?,
3967 0,
3968 "the worker front door creates no ephemeral store directory (no haematite, no temp dir)"
3969 );
3970
3971 let full_subsystems = RecordingSubsystems::rooted(full_root.path());
3972 let mut config = config_with_profile("full")?;
3973 config.persistence_path = Some(full_root.path().to_path_buf());
3974 let full =
3975 build_connection_services_via(&config, &full_subsystems).must("full services build")?;
3976 assert!(
3977 full.supports_channel_operations(),
3978 "full mode serves channel operations"
3979 );
3980 assert_eq!(
3981 entry_count(full_root.path())?,
3982 1,
3983 "full mode builds its persistent database in the explicit directory"
3984 );
3985
3986 drop(front_door);
3987 drop(full);
3988 Ok(())
3989 }
3990
3991 #[test]
4009 fn worker_profile_census_is_empty_and_full_profile_records_all_schedulers() -> TestResult {
4010 let worker_root = tempfile::tempdir().must("test can create an ephemeral root")?;
4011 let full_root = tempfile::tempdir().must("test can create an ephemeral root")?;
4012
4013 let worker_subsystems = RecordingSubsystems::rooted(worker_root.path());
4014 let front_door = build_connection_services_via(
4015 &config_with_profile("worker-front-door")?,
4016 &worker_subsystems,
4017 )
4018 .must("worker-front-door services build")?;
4019 assert_eq!(
4020 worker_subsystems.recorded(),
4021 Vec::<SchedulerSubsystem>::new(),
4022 "the worker front door constructs no scheduler-owning subsystem"
4023 );
4024
4025 let full_subsystems = RecordingSubsystems::rooted(full_root.path());
4026 let mut config = config_with_profile("full")?;
4027 config.persistence_path = Some(full_root.path().to_path_buf());
4028 let full =
4029 build_connection_services_via(&config, &full_subsystems).must("full services build")?;
4030 assert_eq!(
4031 full_subsystems.recorded(),
4032 vec![
4033 SchedulerSubsystem::ChannelSupervisor,
4034 SchedulerSubsystem::ConversationSupervisor,
4035 SchedulerSubsystem::HaematiteStore,
4036 ],
4037 "the full profile constructs every scheduler-owning subsystem, once each — \
4038 the positive control proving the census instrument detects them"
4039 );
4040
4041 drop(front_door);
4042 drop(full);
4043 Ok(())
4044 }
4045
4046 #[test]
4050 fn full_only_constructors_reject_worker_profile() -> TestResult {
4051 let config = config_with_profile("worker-front-door")?;
4052
4053 let from_config = LiminalConnectionServices::from_config(&config);
4054 assert!(
4055 matches!(from_config, Err(ServerError::ConfigValidation { .. })),
4056 "from_config must reject a worker-front-door profile with ConfigValidation, got {from_config:?}"
4057 );
4058
4059 let root = tempfile::tempdir().must("test can create an ephemeral root")?;
4060 let store = open_ephemeral_rooted(root.path(), DEFAULT_SHARD_COUNT)
4061 .must("test store for the rejection check builds")?;
4062 let from_config_with_store =
4063 LiminalConnectionServices::from_config_with_store(&config, std::sync::Arc::new(store));
4064 assert!(
4065 matches!(
4066 from_config_with_store,
4067 Err(ServerError::ConfigValidation { .. })
4068 ),
4069 "from_config_with_store must reject a worker-front-door profile with ConfigValidation"
4070 );
4071 Ok(())
4072 }
4073
4074 #[test]
4079 fn build_connection_services_rejects_worker_profile_with_full_only_fields() -> TestResult {
4080 let mut config = config_with_profile("worker-front-door")?;
4081 config.channels = vec![crate::config::types::ChannelDef {
4082 name: "orders".to_owned(),
4083 schema_ref: None,
4084 durable: false,
4085 loaded_schema: None,
4086 }];
4087 config.persistence_path = Some(std::path::PathBuf::from("/tmp"));
4088
4089 let result = build_connection_services(&config);
4090 let Err(ServerError::ConfigValidation { message }) = result else {
4091 return Err(
4092 "expected ConfigValidation for worker profile with full-only fields".into(),
4093 );
4094 };
4095 assert!(message.contains("builds no channels"), "got: {message}");
4096 assert!(
4097 message.contains("builds no durable store"),
4098 "got: {message}"
4099 );
4100 Ok(())
4101 }
4102
4103 #[test]
4106 fn persistent_store_uses_configured_path_and_creates_no_temp_dir() -> TestResult {
4107 let home = tempfile::tempdir().must("test can create a temp dir")?;
4108 let ephemeral_root = tempfile::tempdir().must("test can create an ephemeral root")?;
4109
4110 let store = build_durable_store(home.path()).must("persistent store builds")?;
4111
4112 assert!(
4113 home.path().join("durability").join("config.json").exists(),
4114 "the persistent database is created under the configured path"
4115 );
4116 assert_eq!(
4117 entry_count(ephemeral_root.path())?,
4118 0,
4119 "the persistent branch creates no ephemeral guard directory"
4120 );
4121
4122 drop(store);
4123 Ok(())
4124 }
4125
4126 #[test]
4127 fn missing_path_refuses_before_any_store_is_opened() -> Result<(), Box<dyn std::error::Error>> {
4128 let root = tempfile::tempdir()?;
4129 let factory = RecordingSubsystems::rooted(root.path());
4130 let result = build_connection_services_via(&config_with_profile("full")?, &factory);
4131 assert!(
4132 matches!(result, Err(ServerError::ConfigValidation { message }) if message.contains("no store is opened"))
4133 );
4134 assert_eq!(entry_count(root.path())?, 0);
4135 assert!(factory.recorded().is_empty());
4136 Ok(())
4137 }
4138
4139 #[test]
4147 fn a_store_whose_config_predates_the_budget_field_opens_with_no_operator_hand() -> TestResult {
4148 let home = tempfile::tempdir().must("test can create a temp dir")?;
4149 let store = build_durable_store(home.path()).must("persistent store builds")?;
4150 drop(store);
4151
4152 let config_file = home.path().join("durability").join("config.json");
4153 let bytes = std::fs::read(&config_file).must("config.json is readable")?;
4154 let mut config: serde_json::Value =
4155 serde_json::from_slice(&bytes).must("config.json parses")?;
4156 config
4157 .as_object_mut()
4158 .must("config.json is an object")?
4159 .remove("node_cache_budget")
4160 .must("the created config carries the budget to remove")?;
4161 std::fs::write(
4162 &config_file,
4163 serde_json::to_vec_pretty(&config).must("serializes")?,
4164 )
4165 .must("test can age the config")?;
4166
4167 let reopened = build_durable_store(home.path())
4168 .must("a pre-budget store opens with no operator action")?;
4169 drop(reopened);
4170
4171 let after = std::fs::read(&config_file).must("config.json is readable after")?;
4172 let migrated: serde_json::Value =
4173 serde_json::from_slice(&after).must("migrated config parses")?;
4174 assert_eq!(
4175 migrated.get("node_cache_budget"),
4176 Some(&serde_json::Value::String("unlimited".to_owned())),
4177 "the migrated file carries the budget the create arm chooses"
4178 );
4179 Ok(())
4180 }
4181
4182 #[test]
4188 fn a_corrupt_config_still_refuses_and_is_left_untouched() -> TestResult {
4189 let home = tempfile::tempdir().must("test can create a temp dir")?;
4190 let data_dir = home.path().join("durability");
4191 std::fs::create_dir_all(&data_dir).must("test can create the data dir")?;
4192 let config_file = data_dir.join("config.json");
4193 let garbage = b"{ not json at all";
4194 std::fs::write(&config_file, garbage).must("test can write garbage")?;
4195
4196 let refused = build_durable_store(home.path());
4197 assert!(refused.is_err(), "a corrupt config refuses to open");
4198 assert_eq!(
4199 std::fs::read(&config_file)
4200 .must("config.json is still readable")?
4201 .as_slice(),
4202 garbage,
4203 "the refusing file is left byte-identical for the operator to read"
4204 );
4205 Ok(())
4206 }
4207
4208 #[test]
4212 fn a_config_already_carrying_the_budget_is_left_byte_identical() -> TestResult {
4213 let home = tempfile::tempdir().must("test can create a temp dir")?;
4214 let store = build_durable_store(home.path()).must("persistent store builds")?;
4215 drop(store);
4216
4217 let config_file = home.path().join("durability").join("config.json");
4218 let before = std::fs::read(&config_file).must("config.json is readable")?;
4219 let reopened = build_durable_store(home.path()).must("a current store reopens")?;
4220 drop(reopened);
4221 assert_eq!(
4222 std::fs::read(&config_file).must("config.json is readable after")?,
4223 before,
4224 "a config that already carries the budget is left byte-identical"
4225 );
4226 Ok(())
4227 }
4228
4229 #[test]
4233 fn a_keep_all_durable_store_reclaims_after_the_next_open() -> TestResult {
4234 let dir = tempfile::tempdir().must("test can create a store dir")?;
4235 let data_dir = dir.path().join("durability");
4236 let database = haematite::Database::create(
4237 haematite::DatabaseConfig {
4238 data_dir: data_dir.clone(),
4239 shard_count: 1,
4240 distributed: None,
4241 executor_threads: None,
4242 node_cache_budget: Some(haematite::NodeCacheBudget::Unlimited),
4243 retention: haematite::Retention::KeepAll,
4244 },
4245 haematite::RuntimeOptions {
4246 long_wait_interval: std::time::Duration::from_secs(5),
4247 reclaim_start: haematite::ReclaimStart::AtOpen,
4248 },
4249 )
4250 .must("a keep-all store is created")?;
4251 for round in 0_u32..200 {
4252 let mut value = round.to_be_bytes().to_vec();
4253 value.resize(1_024, 0x5a);
4254 database.put(b"key".to_vec(), value).must("put")?;
4255 database.commit().must("commit")?;
4256 }
4257 drop(database);
4258 let grown =
4259 haematite::db::vacuum::stats(&haematite::db::vacuum::Options::new(data_dir.clone()))
4260 .must("stats of the grown store")?
4261 .totals
4262 .enumerated_bytes;
4263
4264 let reopened = super::open_or_create_database(&data_dir).must("the server opens it")?;
4265 reopened
4268 .release_reclaim()
4269 .must("the held reclaim is released")?;
4270 reopened.quiesce_reclaim().must("reclaim runs to rest")?;
4271 drop(reopened);
4272
4273 let config: serde_json::Value = serde_json::from_slice(
4274 &std::fs::read(data_dir.join("config.json")).must("config is readable")?,
4275 )
4276 .must("config is JSON")?;
4277 assert_eq!(
4278 config["retention"],
4279 serde_json::to_value(super::DURABLE_RETENTION).must("the retention serializes")?
4280 );
4281 let report = haematite::db::vacuum::stats(&haematite::db::vacuum::Options::new(data_dir))
4282 .must("stats of the reclaimed store")?;
4283 assert!(
4284 report.totals.enumerated_bytes <= report.totals.marked_bytes.saturating_mul(3),
4285 "the reopened store holds about its live set: enumerated {} B, marked {} B",
4286 report.totals.enumerated_bytes,
4287 report.totals.marked_bytes
4288 );
4289 assert!(
4290 report.totals.enumerated_bytes.saturating_mul(10) < grown,
4291 "the old copies are gone: {} B now against {grown} B grown",
4292 report.totals.enumerated_bytes
4293 );
4294 Ok(())
4295 }
4296
4297 const STORED_RECLAIM_CONFIG: &str = r#"{
4303 "chunking_policy": {
4304 "chunking": "bytes-v2",
4305 "internal_target_bytes": 49152,
4306 "leaf_target_bytes": 65536
4307 },
4308 "data_dir": "DATA_DIR",
4309 "distributed": null,
4310 "executor_threads": null,
4311 "format_version": 2,
4312 "node_cache_budget": "unlimited",
4313 "retention": {
4314 "reclaim": {
4315 "past_roots": 0
4316 }
4317 },
4318 "shard_count": 8
4319}"#;
4320
4321 #[test]
4328 fn a_stored_reclaim_store_grown_without_reclaim_gives_its_old_copies_back_on_open() -> TestResult
4329 {
4330 let dir = tempfile::tempdir().must("test can create a store dir")?;
4331 let data_dir = dir.path().join("durability");
4332 let database = haematite::Database::create(
4333 haematite::DatabaseConfig {
4334 data_dir: data_dir.clone(),
4335 shard_count: 8,
4336 distributed: None,
4337 executor_threads: None,
4338 node_cache_budget: Some(haematite::NodeCacheBudget::Unlimited),
4339 retention: haematite::Retention::KeepAll,
4340 },
4341 haematite::RuntimeOptions {
4342 long_wait_interval: std::time::Duration::from_secs(5),
4343 reclaim_start: haematite::ReclaimStart::AtOpen,
4344 },
4345 )
4346 .must("a keep-all store is created")?;
4347 for round in 0_u32..200 {
4348 let mut value = round.to_be_bytes().to_vec();
4349 value.resize(1_024, 0x5a);
4350 database.put(b"key".to_vec(), value).must("put")?;
4351 database.commit().must("commit")?;
4352 }
4353 for row in 0_u32..200 {
4354 let key = [b"pruned-".as_slice(), &row.to_be_bytes()].concat();
4355 database.put(key, vec![0xa5; 1_024]).must("put")?;
4356 database.commit().must("commit")?;
4357 }
4358 for row in 0_u32..200 {
4359 let key = [b"pruned-".as_slice(), &row.to_be_bytes()].concat();
4360 database.delete(key).must("delete")?;
4361 }
4362 database.commit().must("commit")?;
4363 drop(database);
4364 let grown_report =
4365 haematite::db::vacuum::stats(&haematite::db::vacuum::Options::new(data_dir.clone()))
4366 .must("stats of the grown store")?;
4367 let grown = grown_report.totals;
4368 assert!(
4369 grown.unmarked_nodes > 0,
4370 "the seed leaves copies no root reaches"
4371 );
4372 let mut owed: std::collections::BTreeSet<usize> = grown_report
4373 .shards
4374 .iter()
4375 .filter(|shard| shard.nodes.unmarked_nodes > 0)
4376 .map(|shard| shard.shard_id)
4377 .collect();
4378
4379 let config_file = data_dir.join("config.json");
4380 let seeded = STORED_RECLAIM_CONFIG
4381 .replace("DATA_DIR", data_dir.to_str().must("the test dir is UTF-8")?);
4382 std::fs::write(&config_file, &seeded).must("config.json is written")?;
4383
4384 let reopened = super::open_or_create_database(&data_dir).must("the server opens it")?;
4385 let watch = reopened
4386 .watch_reclaim_passes(64)
4387 .must("every shard accepts a pass watch")?;
4388 reopened
4390 .release_reclaim()
4391 .must("the held reclaim is released")?;
4392 let mut unmarked_by_shard = std::collections::BTreeMap::new();
4393 while !owed.is_empty() {
4394 let report = watch
4395 .recv_timeout(std::time::Duration::from_secs(30))
4396 .must("each pass the open owes ends without a write or a quiesce")?;
4397 assert_eq!(
4398 report.outcome,
4399 haematite::ReclaimPassOutcome::Passed,
4400 "the pass the open owed on shard {} finished",
4401 report.shard_id
4402 );
4403 if owed.remove(&report.shard_id) {
4404 unmarked_by_shard.insert(report.shard_id, report.unmarked_bytes);
4405 }
4406 }
4407 drop(reopened);
4408
4409 let after = haematite::db::vacuum::stats(&haematite::db::vacuum::Options::new(data_dir))
4410 .must("stats of the reclaimed store")?
4411 .totals;
4412 eprintln!(
4413 "grown {} B, after {} B, {} unreachable nodes seeded, unmarked bytes by shard {unmarked_by_shard:?}",
4414 grown.enumerated_bytes, after.enumerated_bytes, grown.unmarked_nodes
4415 );
4416 assert!(
4417 after.enumerated_bytes.saturating_mul(10) < grown.enumerated_bytes,
4418 "the old copies are gone: {} B now against {} B grown",
4419 after.enumerated_bytes,
4420 grown.enumerated_bytes
4421 );
4422 let config: serde_json::Value = serde_json::from_slice(
4423 &std::fs::read(&config_file).must("config.json is readable after")?,
4424 )
4425 .must("config is JSON")?;
4426 assert_eq!(
4427 config["retention"],
4428 serde_json::to_value(super::DURABLE_RETENTION).must("the retention serializes")?,
4429 "config.json names reclaim after the open"
4430 );
4431 Ok(())
4432 }
4433}