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: [AtomicUsize::new(0), AtomicUsize::new(0), AtomicUsize::new(0)],
2475 ephemeral_root: ephemeral_root.to_path_buf(),
2476 }
2477 }
2478
2479 pub fn recorded(&self) -> Vec<SchedulerSubsystem> {
2482 KINDS
2483 .iter()
2484 .zip(&self.census)
2485 .flat_map(|(kind, count)| {
2486 std::iter::repeat_n(*kind, count.load(Ordering::SeqCst))
2487 })
2488 .collect()
2489 }
2490
2491 fn record(&self, subsystem: SchedulerSubsystem) {
2492 if let Some(count) = KINDS
2493 .iter()
2494 .position(|kind| *kind == subsystem)
2495 .and_then(|index| self.census.get(index))
2496 {
2497 count.fetch_add(1, Ordering::SeqCst);
2498 }
2499 }
2500 }
2501
2502 const KINDS: [SchedulerSubsystem; 3] = [
2504 SchedulerSubsystem::ChannelSupervisor,
2505 SchedulerSubsystem::ConversationSupervisor,
2506 SchedulerSubsystem::HaematiteStore,
2507 ];
2508
2509 impl SubsystemFactory for RecordingSubsystems {
2510 fn channel_cluster(
2511 &self,
2512 cluster_config: Option<&ClusterConfig>,
2513 ) -> Result<ChannelCluster, ServerError> {
2514 let cluster = ProductionSubsystems.channel_cluster(cluster_config)?;
2515 self.record(SchedulerSubsystem::ChannelSupervisor);
2516 Ok(cluster)
2517 }
2518
2519 fn conversation_supervisor(&self) -> Result<Arc<ConversationSupervisor>, ServerError> {
2520 let supervisor = ProductionSubsystems.conversation_supervisor()?;
2521 self.record(SchedulerSubsystem::ConversationSupervisor);
2522 Ok(supervisor)
2523 }
2524
2525 fn durable_store(
2526 &self,
2527 persistence_path: &Path,
2528 ) -> Result<Arc<dyn DurableStore>, ServerError> {
2529 let store = build_durable_store(persistence_path)?;
2530 self.record(SchedulerSubsystem::HaematiteStore);
2531 Ok(store)
2532 }
2533
2534 fn throwaway_store(&self) -> ThrowawayStoreOpener {
2535 let root = self.ephemeral_root.clone();
2536 Box::new(move || open_ephemeral_rooted(&root, THROWAWAY_SHARD_COUNT))
2537 }
2538 }
2539}
2540
2541fn require_full_profile(config: &ServerConfig) -> Result<(), ServerError> {
2548 match config.services.profile()? {
2549 ServiceProfile::Full => Ok(()),
2550 ServiceProfile::WorkerFrontDoor => Err(ServerError::ConfigValidation {
2551 message: format!(
2552 "services.profile: \"{}\" cannot construct the full LiminalConnectionServices; \
2553 build profile-selected services via build_connection_services",
2554 ServiceProfile::WORKER_FRONT_DOOR
2555 ),
2556 }),
2557 }
2558}
2559
2560pub fn build_connection_services(
2577 config: &ServerConfig,
2578) -> Result<Arc<dyn ConnectionServices>, ServerError> {
2579 match config.services.profile()? {
2580 ServiceProfile::Full => {
2581 let subsystems = RootedProductionSubsystems::new(required_persistence_path(config)?)?;
2582 build_connection_services_via(config, &subsystems)
2583 }
2584 ServiceProfile::WorkerFrontDoor => build_worker_front_door(config),
2585 }
2586}
2587
2588pub(super) fn build_connection_services_via(
2604 config: &ServerConfig,
2605 subsystems: &dyn SubsystemFactory,
2606) -> Result<Arc<dyn ConnectionServices>, ServerError> {
2607 match config.services.profile()? {
2608 ServiceProfile::Full => {
2609 let path = required_persistence_path(config)?;
2610 let history = StoreHistory::of(path);
2611 let store = subsystems.durable_store(path)?;
2612 Ok(Arc::new(
2613 LiminalConnectionServices::from_config_with_store_via(
2614 config, store, history, subsystems,
2615 )?,
2616 ))
2617 }
2618 ServiceProfile::WorkerFrontDoor => build_worker_front_door(config),
2619 }
2620}
2621
2622fn build_worker_front_door(
2623 config: &ServerConfig,
2624) -> Result<Arc<dyn ConnectionServices>, ServerError> {
2625 let errors = crate::config::validation::worker_front_door_field_errors(config);
2626 if !errors.is_empty() {
2627 return Err(ServerError::ConfigValidation {
2628 message: errors.join("; "),
2629 });
2630 }
2631 Ok(Arc::new(WorkerFrontDoorServices::new()))
2632}
2633
2634pub(crate) fn open_persistent_store(
2636 persistence_path: &Path,
2637) -> Result<Arc<dyn DurableStore>, ServerError> {
2638 build_durable_store(persistence_path)
2639}
2640
2641const SPINE_COMMIT_THRESHOLD_BYTES: NonZeroU64 = match NonZeroU64::new(1 << 20) {
2654 Some(threshold) => threshold,
2655 None => unreachable!(),
2656};
2657
2658const SPINE_COMMIT_POLICY: CommitPolicy = CommitPolicy::buffered(SPINE_COMMIT_THRESHOLD_BYTES);
2660
2661fn build_durable_store(path: &Path) -> Result<Arc<dyn DurableStore>, ServerError> {
2662 let data_dir = durable_data_dir(path);
2663 let database = open_or_create_database(&data_dir)?;
2664 let event_store = EventStore::with_commit_policy(database, SPINE_COMMIT_POLICY);
2665 Ok(Arc::new(CountingStore::new(HaematiteStore::new(Arc::new(
2666 event_store,
2667 )))))
2668}
2669
2670pub(super) fn durable_data_dir(persistence_path: &Path) -> PathBuf {
2672 persistence_path.join("durability")
2673}
2674
2675pub(super) fn database_exists(data_dir: &Path) -> bool {
2678 database_config_file(data_dir).exists()
2679}
2680
2681const UNTOUCHED_DATABASE_ENTRIES: [&str; 2] = ["config.json", "writer.lock"];
2685
2686pub(super) fn database_untouched(data_dir: &Path) -> bool {
2690 std::fs::read_dir(data_dir).is_ok_and(|entries| {
2691 entries.into_iter().all(|entry| {
2692 entry.is_ok_and(|entry| {
2693 entry
2694 .file_name()
2695 .to_str()
2696 .is_some_and(|name| UNTOUCHED_DATABASE_ENTRIES.contains(&name))
2697 })
2698 })
2699 })
2700}
2701
2702fn database_config_file(data_dir: &Path) -> PathBuf {
2703 data_dir.join("config.json")
2704}
2705
2706pub(super) const DURABLE_RETENTION: Retention = Retention::reclaim(0);
2711
2712fn open_or_create_database(data_dir: &Path) -> Result<Database, ServerError> {
2714 let config_file = database_config_file(data_dir);
2715 let result = if config_file.exists() {
2716 adopt_missing_config_keys(data_dir, &config_file)?;
2717 Database::open_with_retention(
2718 data_dir,
2719 haematite::RuntimeOptions {
2720 long_wait_interval: std::time::Duration::from_secs(5),
2721 reclaim_start: haematite::ReclaimStart::Held,
2724 },
2725 DURABLE_RETENTION,
2726 )
2727 } else {
2728 Database::create(
2729 DatabaseConfig {
2730 data_dir: data_dir.to_path_buf(),
2731 shard_count: DEFAULT_SHARD_COUNT,
2732 distributed: None,
2733 executor_threads: None,
2734 node_cache_budget: Some(haematite::NodeCacheBudget::Unlimited),
2743 retention: DURABLE_RETENTION,
2744 },
2745 haematite::RuntimeOptions {
2746 long_wait_interval: std::time::Duration::from_secs(5),
2747 reclaim_start: haematite::ReclaimStart::Held,
2750 },
2751 )
2752 };
2753 let database = result.map_err(|error| ServerError::ConfigValidation {
2754 message: format!(
2755 "failed to open durable store at {}: {error}",
2756 data_dir.display()
2757 ),
2758 })?;
2759 liminal::durability::record_store_opened();
2760 super::reclaim_placement::arm_watch(&database);
2761 Ok(database)
2762}
2763
2764const ADOPTED_CONFIG_KEYS: [(&str, &str); 2] = [
2776 ("node_cache_budget", "unlimited"),
2777 ("retention", "keep_all"),
2778];
2779
2780fn adopt_missing_config_keys(data_dir: &Path, config_file: &Path) -> Result<(), ServerError> {
2798 let Ok(bytes) = std::fs::read(config_file) else {
2799 return Ok(());
2800 };
2801 let Ok(mut config) = serde_json::from_slice::<serde_json::Value>(&bytes) else {
2802 return Ok(());
2803 };
2804 let Some(object) = config.as_object_mut() else {
2805 return Ok(());
2806 };
2807 let mut adopted = Vec::new();
2808 for (key, value) in ADOPTED_CONFIG_KEYS {
2809 if !object.contains_key(key) {
2810 object.insert(key.to_owned(), serde_json::Value::String(value.to_owned()));
2811 adopted.push(key);
2812 }
2813 }
2814 if adopted.is_empty() {
2815 return Ok(());
2816 }
2817 let keys = adopted.join(", ");
2818 let migrated =
2819 serde_json::to_vec_pretty(&config).map_err(|error| ServerError::ConfigValidation {
2820 message: format!(
2821 "durable store config at {} predates `{keys}` and its migration could not be \
2822 serialized: {error}",
2823 config_file.display()
2824 ),
2825 })?;
2826 let write_error = |step: &str, error: std::io::Error| ServerError::ConfigValidation {
2827 message: format!(
2828 "durable store config at {} predates `{keys}` and migrating it failed at {step}: \
2829 {error}",
2830 config_file.display()
2831 ),
2832 };
2833 let tmp_file = data_dir.join("config.json.migrate-tmp");
2834 let mut tmp = std::fs::File::create(&tmp_file)
2835 .map_err(|error| write_error("creating the temporary file", error))?;
2836 std::io::Write::write_all(&mut tmp, &migrated)
2837 .map_err(|error| write_error("writing the temporary file", error))?;
2838 tmp.sync_all()
2839 .map_err(|error| write_error("syncing the temporary file", error))?;
2840 drop(tmp);
2841 std::fs::rename(&tmp_file, config_file)
2842 .map_err(|error| write_error("renaming over config.json", error))?;
2843 std::fs::File::open(data_dir)
2844 .and_then(|dir| dir.sync_all())
2845 .map_err(|error| write_error("syncing the data directory", error))?;
2846 tracing::warn!(
2847 config = %config_file.display(),
2848 keys = %keys,
2849 "durable store config predates these keys -- wrote the create-arm values (the \
2850 behaviour-preserving ones) so the store opens with no operator action"
2851 );
2852 Ok(())
2853}
2854
2855pub(super) fn reserved_namespace_refusal(channel: &str) -> Option<ServerError> {
2856 if channel.starts_with("liminal.") {
2857 crate::metrics::self_stream_reserved_publication();
2858 Some(ServerError::ReservedNamespace {
2859 channel: channel.to_owned(),
2860 })
2861 } else {
2862 None
2863 }
2864}
2865
2866impl LiminalConnectionServices {
2867 pub(in crate::server) const fn signals(&self) -> &ServerSignals {
2868 &self.signals
2869 }
2870
2871 pub(in crate::server) fn self_stream_head(&self, name: &str) -> Result<u64, ServerError> {
2872 let channels = self.read_channels()?;
2873 let channel = channels
2874 .get(name)
2875 .ok_or_else(|| ServerError::ListenerAccept {
2876 message: format!("self stream '{name}' is absent after append"),
2877 })?;
2878 let heads =
2879 channel
2880 .handle
2881 .durable_positions()
2882 .map_err(|error| ServerError::ListenerAccept {
2883 message: format!("self stream '{name}' head is unavailable: {error}"),
2884 })?;
2885 drop(channels);
2886 match heads.as_slice() {
2887 [head] => Ok(*head),
2888 _ => Err(ServerError::ListenerAccept {
2889 message: format!("self stream '{name}' must have exactly one durable partition"),
2890 }),
2891 }
2892 }
2893
2894 pub(in crate::server) fn publish_self_stream_batch(
2895 &self,
2896 payloads: &[Vec<u8>],
2897 publisher: &str,
2898 ) -> Result<u64, ServerError> {
2899 if publisher.trim().is_empty() || payloads.is_empty() {
2900 return Err(ServerError::ConfigValidation {
2901 message: "self-stream batch requires a publisher and rows".to_owned(),
2902 });
2903 }
2904 let configured = self
2905 .admit_channel("liminal.events")
2906 .map_err(|error| access_to_server_error(&error))?;
2907 let deliveries = configured
2908 .handle
2909 .publish_batch_with_delivery(
2910 payloads,
2911 &liminal::envelope::PublisherId::new(publisher),
2912 None,
2913 )
2914 .map_err(|error| match error {
2915 liminal::error::LiminalError::PublishUnresolved { append } => {
2916 ServerError::PublishUnresolved {
2917 channel: "liminal.events".to_owned(),
2918 idempotency_key: None,
2919 sequence: Some(append.sequence()),
2920 message: append.to_string(),
2921 }
2922 }
2923 error => ServerError::ListenerAccept {
2924 message: format!("self-stream receipt batch failed: {error}"),
2925 },
2926 })?;
2927 if deliveries.len() != payloads.len()
2928 || deliveries
2929 .iter()
2930 .any(|delivery| delivery.durable_position().is_none())
2931 {
2932 return Err(ServerError::ListenerAccept {
2933 message: "self-stream receipt batch was refused before append".to_owned(),
2934 });
2935 }
2936 for pair in deliveries.windows(2) {
2937 if pair[0]
2938 .durable_position()
2939 .and_then(|position| position.checked_add(1))
2940 != pair[1].durable_position()
2941 {
2942 return Err(ServerError::ListenerAccept {
2943 message: "self-stream receipt batch positions are not contiguous".to_owned(),
2944 });
2945 }
2946 }
2947 for delivery in &deliveries {
2948 Self::record_delivery_counts(delivery);
2949 }
2950 deliveries
2951 .last()
2952 .and_then(ChannelDelivery::durable_position)
2953 .and_then(|position| position.checked_add(1))
2954 .ok_or_else(|| ServerError::ListenerAccept {
2955 message: "self-stream receipt batch head is unavailable".to_owned(),
2956 })
2957 }
2958
2959 pub(in crate::server) fn checkpoint_self_stream_heads(
2960 &self,
2961 heads: [u64; 2],
2962 ) -> Result<(), ServerError> {
2963 for (name, head) in ["liminal.events:0", "liminal.cost:0"]
2964 .into_iter()
2965 .zip(heads)
2966 {
2967 block_on(liminal::durability::head_checkpoint::advance_head(
2968 self.durable_store.as_ref(),
2969 name,
2970 head,
2971 ))
2972 .map_err(|error| ServerError::ShutdownFlush {
2973 message: format!("self-stream head checkpoint bridge for '{name}' failed: {error}"),
2974 })?
2975 .map_err(|error| ServerError::ShutdownFlush {
2976 message: format!("self-stream head checkpoint for '{name}' failed: {error}"),
2977 })?;
2978 }
2979 Ok(())
2980 }
2981
2982 pub(in crate::server) fn install_self_stream_channels(&self) -> Result<(), ServerError> {
2983 let names = ["liminal.cost", "liminal.events"];
2984 {
2985 let channels = self.read_channels()?;
2986 for name in names {
2987 if channels.contains_key(name) {
2988 return Err(ServerError::ConfigValidation {
2989 message: format!("self-stream channel '{name}' is already configured"),
2990 });
2991 }
2992 }
2993 }
2994 let mut built = Vec::with_capacity(names.len());
2995 for name in names {
2996 let schema =
2997 resolve_schema_bytes(None).map_err(|error| ServerError::ConfigValidation {
2998 message: format!("self-stream channel '{name}' schema failed: {error}"),
2999 })?;
3000 let channel = build_configured_channel(
3001 name,
3002 schema,
3003 true,
3004 ChannelOrigin::BootConfigured,
3005 &self.durable_store,
3006 self.cluster.supervisor(),
3007 )
3008 .map_err(|error| ServerError::ConfigValidation {
3009 message: error.boot_message(name),
3010 })?;
3011 channel
3012 .handle
3013 .install_publication_identity(self.publication_identity.clone())
3014 .map_err(|error| ServerError::ConfigValidation {
3015 message: error.to_string(),
3016 })?;
3017 built.push((name.to_owned(), Arc::new(channel)));
3018 }
3019 let mut channels =
3020 self.channels
3021 .write()
3022 .map_err(|error| ServerError::ConfigValidation {
3023 message: format!("self-stream channel roster lock poisoned: {error}"),
3024 })?;
3025 for (name, _) in &built {
3026 if channels.contains_key(name) {
3027 return Err(ServerError::ConfigValidation {
3028 message: format!("self-stream channel '{name}' was concurrently configured"),
3029 });
3030 }
3031 }
3032 channels.extend(built);
3033 drop(channels);
3034 for name in names {
3037 self.console_changed(crate::console::TopologyChange::Channel {
3038 name: name.to_owned(),
3039 life: line_life(true),
3040 state: crate::console::records::LineState::Live,
3041 });
3042 }
3043 Ok(())
3044 }
3045
3046 pub(in crate::server) fn install_console_channels(&self) -> Result<(), ServerError> {
3051 self.register_console_channel(crate::console::TOPOLOGY_SUBJECT)
3052 .map_err(|error| ServerError::ConfigValidation {
3053 message: format!("console channel registration failed: {error}"),
3054 })
3055 }
3056
3057 fn register_console_channel(&self, name: &str) -> Result<(), ChannelRegistryError> {
3060 if self.read_roster()?.contains_key(name) {
3061 return Ok(());
3062 }
3063 let schema = resolve_schema_bytes(None).map_err(|error| {
3064 ChannelRegistryError::RosterUnavailable {
3065 message: format!("console channel '{name}' schema failed: {error}"),
3066 }
3067 })?;
3068 let channel = build_configured_channel(
3069 name,
3070 schema,
3071 false,
3072 ChannelOrigin::BootConfigured,
3073 &self.durable_store,
3074 self.cluster.supervisor(),
3075 )
3076 .map_err(|error| error.into_registry_error(name))?;
3077 channel
3078 .handle
3079 .install_publication_identity(self.publication_identity.clone())
3080 .map_err(|error| ChannelRegistryError::RosterUnavailable {
3081 message: error.to_string(),
3082 })?;
3083 self.write_roster()?
3084 .entry(name.to_owned())
3085 .or_insert_with(|| Arc::new(channel));
3086 Ok(())
3087 }
3088
3089 pub fn publish_embedded(
3101 &self,
3102 host: &str,
3103 channel: &str,
3104 envelope: &MessageEnvelope,
3105 idempotency_key: Option<&str>,
3106 ) -> Result<PublishOutcome, ServerError> {
3107 self.publish_with_origin(
3108 channel,
3109 envelope,
3110 idempotency_key,
3111 PublishOrigin::Host { name: host },
3112 )
3113 .map(|published| published.outcome)
3114 }
3115
3116 pub(in crate::server) fn publish_with_origin(
3117 &self,
3118 channel: &str,
3119 envelope: &MessageEnvelope,
3120 idempotency_key: Option<&str>,
3121 origin: PublishOrigin<'_>,
3122 ) -> Result<OriginPublication, ServerError> {
3123 if !origin.is_server() {
3124 if plan_subject(channel).is_some() {
3125 return Err(site_services::site_error(
3126 crate::services::lifecycle::site::SiteError::Invalid(
3127 "plan subject is server-owned",
3128 ),
3129 ));
3130 }
3131 crate::agents::binding::refuse_server_publish(channel)?;
3132 }
3133 validate_publication_origin(channel, idempotency_key, origin)?;
3134 let mut recipients = self.signals.lend_recipients();
3138 let received_at = recipients.is_some().then(Instant::now);
3142 let result = (|| {
3143 let configured = self
3152 .admit_channel(channel)
3153 .map_err(|error| access_to_server_error(&error))?;
3154
3155 if let (Some(key), ChannelMode::Durable) =
3166 (idempotency_key, configured.handle.config().mode)
3167 {
3168 return self
3169 .publish_durable_keyed(
3170 &configured,
3171 channel,
3172 (envelope, origin),
3173 key,
3174 recipients.as_mut(),
3175 )
3176 .map(OriginPublication::connection);
3177 }
3178 let dedup = self.dedup_for(&configured);
3179 if let Some(key) = idempotency_key {
3180 if !Self::claim_delivery(dedup, key)? {
3181 crate::metrics::publish_accepted();
3185 return Ok(OriginPublication::connection(PublishOutcome {
3186 message_id: self.next_message_id()?,
3187 delivered: false,
3188 durable_position: None,
3189 reach: Reach::Suppressed,
3190 }));
3191 }
3192 }
3193
3194 let delivery = fan_out(&configured, envelope, origin, recipients.as_mut());
3195 let delivery = match delivery {
3196 Ok(delivery) => delivery,
3197 Err(liminal::error::LiminalError::PublishUnresolved { append }) => {
3201 return Err(ServerError::PublishUnresolved {
3202 channel: channel.to_owned(),
3203 idempotency_key: None,
3204 sequence: Some(append.sequence()),
3205 message: append.to_string(),
3206 });
3207 }
3208 Err(error) => {
3209 if let Some(key) = idempotency_key {
3216 Self::release_claim(dedup, key);
3217 }
3218 return Err(ServerError::ListenerAccept {
3219 message: format!("liminal publish failed for channel '{channel}': {error}"),
3220 });
3221 }
3222 };
3223
3224 if let Some(key) = idempotency_key {
3241 if delivery.is_admitted() {
3242 block_on(dedup.complete_receipt(key, ProcessingReceipt::new(Vec::new())))
3243 .map_err(|error| ServerError::ListenerAccept {
3244 message: format!(
3245 "dedup receipt bridge failed for key '{key}': {error}"
3246 ),
3247 })?
3248 .map_err(|error| ServerError::ListenerAccept {
3249 message: format!("dedup receipt write failed for key '{key}': {error}"),
3250 })?;
3251 } else {
3252 Self::release_claim(dedup, key);
3253 }
3254 }
3255
3256 let self_row_at = delivery.durable_position().filter(|_| origin.is_server());
3257 Ok(OriginPublication {
3258 outcome: Self::delivered(&delivery)?,
3259 self_row_appended: self_row_at.is_some(),
3260 self_row_at,
3261 })
3262 })();
3263 self.signals.published(
3264 channel,
3265 origin,
3266 envelope,
3267 &result,
3268 recipients.as_ref().zip(received_at),
3269 );
3270 if let Some(buffer) = recipients {
3271 self.signals.give_back(buffer);
3272 }
3273 result
3274 }
3275}
3276
3277fn fan_out(
3280 configured: &ConfiguredChannel,
3281 envelope: &MessageEnvelope,
3282 origin: PublishOrigin<'_>,
3283 recipients: Option<&mut liminal::channel::PublishRecord>,
3284) -> Result<liminal::channel::ChannelDelivery, liminal::error::LiminalError> {
3285 let publisher = || publisher_for_origin(origin);
3286 recipients.map_or_else(
3287 || {
3288 configured
3289 .handle
3290 .publish_with_delivery(&envelope.payload, publisher(), None)
3291 },
3292 |recipients| {
3293 configured.handle.publish_recording(
3294 &envelope.payload,
3295 publisher(),
3296 None,
3297 None,
3298 recipients,
3299 )
3300 },
3301 )
3302}
3303
3304fn validate_publication_origin(
3305 channel: &str,
3306 idempotency_key: Option<&str>,
3307 origin: PublishOrigin<'_>,
3308) -> Result<(), ServerError> {
3309 if !origin.is_server() {
3310 refuse_console_namespace(channel).map_err(|error| access_to_server_error(&error))?;
3311 if let Some(error) = reserved_namespace_refusal(channel) {
3312 return Err(error);
3313 }
3314 }
3315 if let PublishOrigin::Host { name } = origin
3316 && name.trim().is_empty()
3317 {
3318 return Err(ServerError::ConfigValidation {
3319 message: "embedded publication requires a nonempty host name".to_owned(),
3320 });
3321 }
3322 if let PublishOrigin::Server { device } = origin {
3323 if device.trim().is_empty() || idempotency_key.is_some() {
3324 return Err(ServerError::ConfigValidation {
3325 message: "server-origin publication requires a device and no idempotency key"
3326 .to_owned(),
3327 });
3328 }
3329 }
3330 Ok(())
3331}
3332
3333fn publisher_for_origin(origin: PublishOrigin<'_>) -> liminal::envelope::PublisherId {
3334 match origin {
3335 PublishOrigin::Connection { id } => liminal::envelope::PublisherId::new(id.to_string()),
3336 PublishOrigin::Host { name } => {
3337 liminal::envelope::PublisherId::new(format!("liminal.{name}"))
3338 }
3339 PublishOrigin::Server { device } => {
3340 liminal::envelope::PublisherId::new(format!("liminal.{device}"))
3341 }
3342 }
3343}
3344
3345impl ConnectionServices for LiminalConnectionServices {
3346 fn construction_request(
3347 &self,
3348 session: Option<&dyn crate::auth_lys::session::LysSession>,
3349 subject: &str,
3350 bytes: &[u8],
3351 at_ms: u64,
3352 ) -> Option<Result<serde_json::Value, ServerError>> {
3353 if let Some(answer) = self.agent_host_request(session, subject, bytes, at_ms) {
3354 return Some(answer);
3355 }
3356 is_site_route(subject).then(|| {
3357 self.serve_site_tool(session, subject, bytes, at_ms)
3358 .map_err(site_services::site_error)
3359 })
3360 }
3361 fn prepare_site_trial(
3362 &self,
3363 connection: u64,
3364 session: Option<&dyn crate::auth_lys::session::LysSession>,
3365 registration: &liminal::protocol::WorkerRegistration,
3366 process: Option<&crate::services::lifecycle::ProcessGrant>,
3367 at_ms: u64,
3368 ) -> Result<Option<crate::services::lifecycle::trial::TrialPermit>, ServerError> {
3369 self.site_lifecycle.get().map_or(Ok(None), |lifecycle| {
3370 lifecycle
3371 .prepare_trial(connection, session, registration, process, at_ms)
3372 .map_err(site_services::site_error)
3373 })
3374 }
3375 fn admit_site_call(
3376 &self,
3377 session: Option<&dyn crate::auth_lys::session::LysSession>,
3378 subject: &str,
3379 at_ms: u64,
3380 ) -> Result<bool, ServerError> {
3381 self.site_lifecycle.get().map_or(Ok(false), |lifecycle| {
3382 lifecycle
3383 .admit_trial_call(session, subject, at_ms)
3384 .map_err(site_services::site_error)
3385 })
3386 }
3387 fn site_registration_closed(&self, connection: u64) -> Result<(), ServerError> {
3388 if let Some(lifecycle) = self.site_lifecycle.get() {
3389 lifecycle
3390 .trial_closed(connection)
3391 .map_err(site_services::site_error)?;
3392 lifecycle
3393 .refresh_sites()
3394 .map_err(site_services::site_error)?;
3395 }
3396 Ok(())
3397 }
3398 fn publish_identified(
3399 &self,
3400 connection: u64,
3401 session: Option<&dyn crate::auth_lys::session::LysSession>,
3402 channel: &str,
3403 envelope: &MessageEnvelope,
3404 key: Option<&str>,
3405 ) -> Result<PublishOutcome, ServerError> {
3406 if plan_subject(channel).is_some() {
3407 return Err(site_services::site_error(
3408 crate::services::lifecycle::site::SiteError::Invalid(
3409 "plan subject is server-owned",
3410 ),
3411 ));
3412 }
3413 self.publish_agent(connection, session, channel, envelope, key)
3414 }
3415 fn agents(&self) -> Option<&Arc<crate::agents::runtime::Agents>> {
3416 self.agents.as_ref()
3417 }
3418
3419 fn subscribe_identified(
3420 &self,
3421 connection: u64,
3422 session: Option<&dyn crate::auth_lys::session::LysSession>,
3423 channel: &str,
3424 schemas: &[ProtocolSchemaId],
3425 install: Option<liminal::channel::InboxInstall>,
3426 ) -> Result<ConnectionSubscription, ServerError> {
3427 if plan_subject(channel).is_some() {
3428 return self.subscribe_site_plan(session, channel, schemas, install);
3429 }
3430 self.subscribe_agent(connection, session, channel, schemas, install)
3431 }
3432 fn participant_service(&self) -> Option<InstalledParticipantService> {
3433 self.participant_service.clone()
3434 }
3435
3436 fn admit_channel(
3463 &self,
3464 operation: ChannelOperation,
3465 channel: &str,
3466 ) -> Result<(), ChannelAccessError> {
3467 if let Some(name) = plan_subject(channel) {
3468 let refused = |message| ChannelAccessError::RosterUnavailable { message };
3469 if operation == ChannelOperation::Publish {
3470 return Err(refused("plan subject is server-owned".into()));
3471 }
3472 let lifecycle = self
3473 .site_lifecycle
3474 .get()
3475 .ok_or_else(|| refused("construction not installed".into()))?;
3476 lifecycle
3477 .record(name)
3478 .map_err(|error| refused(error.to_string()))?;
3479 return Ok(());
3480 }
3481 if let Some(admitted) = self.agent_channel_admission(channel) {
3482 return admitted;
3483 }
3484 match operation {
3485 ChannelOperation::Publish => refuse_console_namespace(channel)?,
3486 ChannelOperation::Subscribe => {
3487 refuse_console_tap_target(channel)?;
3488 self.refuse_console_tap_limit(channel)?;
3489 let admitted = if self.signals.console_enabled()
3492 && super::channel_registry::console_tap_target(channel).is_some()
3493 {
3494 self.register_console_channel(channel).map_err(|error| {
3495 ChannelAccessError::RosterUnavailable {
3496 message: error.to_string(),
3497 }
3498 })
3499 } else {
3500 Ok(())
3501 }
3502 .and_then(|()| Self::admit_channel(self, channel).map(|_| ()));
3503 if admitted.is_err() {
3504 self.give_back_tap(channel);
3505 }
3506 return admitted;
3507 }
3508 }
3509 Self::admit_channel(self, channel)?;
3510 Ok(())
3511 }
3512
3513 fn publish(
3514 &self,
3515 connection: u64,
3516 channel: &str,
3517 envelope: &MessageEnvelope,
3518 idempotency_key: Option<&str>,
3519 ) -> Result<PublishOutcome, ServerError> {
3520 refuse_console_namespace(channel).map_err(|error| access_to_server_error(&error))?;
3524 if plan_subject(channel).is_some() {
3525 return Err(site_services::site_error(
3526 crate::services::lifecycle::site::SiteError::Invalid(
3527 "plan subject is server-owned",
3528 ),
3529 ));
3530 }
3531 crate::agents::binding::refuse_server_publish(channel)?;
3532 if crate::agents::binding::subject(channel).is_some() {
3533 return Err(crate::agents::error::AgentError::Invalid(
3534 "agent publish requires the verified identity route",
3535 )
3536 .into());
3537 }
3538 self.publish_with_origin(
3539 channel,
3540 envelope,
3541 idempotency_key,
3542 PublishOrigin::Connection { id: connection },
3543 )
3544 .map(|published| published.outcome)
3545 }
3546
3547 fn subscribe(
3548 &self,
3549 channel: &str,
3550 accepted_schemas: &[ProtocolSchemaId],
3551 install: Option<liminal::channel::InboxInstall>,
3552 ) -> Result<ConnectionSubscription, ServerError> {
3553 if plan_subject(channel).is_some() {
3554 return Err(site_services::site_error(
3555 crate::services::lifecycle::site::SiteError::Invalid(
3556 "plan subscription needs an admitted pass",
3557 ),
3558 ));
3559 }
3560 if crate::agents::binding::subject(channel).is_some() {
3561 return Err(crate::agents::error::AgentError::Invalid(
3562 "agent subscribe requires the verified identity route",
3563 )
3564 .into());
3565 }
3566 let subscribed = self.open_subscription(channel, accepted_schemas, install, None);
3567 if subscribed.is_err() {
3568 self.give_back_tap(channel);
3572 }
3573 subscribed
3574 }
3575
3576 fn unsubscribe(&self, subscription: ConnectionSubscription) -> Result<(), ServerError> {
3577 subscription.unsubscribe()
3578 }
3579
3580 fn open_conversation(
3581 &self,
3582 conversation_id: u64,
3583 subject: &str,
3584 ) -> Result<ConnectionConversation, ServerError> {
3585 self.open_routed_conversation(conversation_id, subject)
3586 }
3587
3588 fn conversation_message(
3589 &self,
3590 conversation: &ConnectionConversation,
3591 envelope: &MessageEnvelope,
3592 op_id: Option<u64>,
3593 ) -> Result<(), ServerError> {
3594 self.routed_conversation_message(conversation, envelope, op_id)
3595 }
3596
3597 fn close_conversation(&self, conversation: ConnectionConversation) -> Result<(), ServerError> {
3598 conversation.close()
3599 }
3600
3601 fn listening(self: Arc<Self>) -> Option<std::thread::JoinHandle<()>> {
3602 self.resolve_claims_behind_listener()
3603 }
3604
3605 fn stop_participant_warm(&self) {
3606 if let Some(warm) = &self.participant_warm {
3611 warm.stop();
3612 warm.record_clean_stop();
3613 }
3614 }
3615
3616 fn finish_background_store_work(&self) -> Result<(), ServerError> {
3617 self.finish_background_work()
3618 }
3619
3620 fn release_store_holders(&self) -> Result<(), ServerError> {
3621 self.release_holders()
3622 }
3623
3624 fn flush_durable_state(&self) -> Result<(), ServerError> {
3625 self.signals.with_flush_barrier(|| {
3628 block_on(self.head_checkpoint_stop.checkpoint_heads())
3629 .map_err(|error| ServerError::ShutdownFlush {
3630 message: format!("clean-stop head checkpoint bridge failed: {error}"),
3631 })?
3632 .map_err(|error| ServerError::ShutdownFlush {
3633 message: format!("clean-stop head checkpoint failed: {error}"),
3634 })?;
3635 block_on(self.durable_store.flush())
3636 .map_err(|error| ServerError::ShutdownFlush {
3637 message: format!("shared durable store flush bridge failed: {error}"),
3638 })?
3639 .map_err(|error| ServerError::ShutdownFlush {
3640 message: format!("shared durable store flush failed: {error}"),
3641 })?;
3642 Ok(())
3643 })?;
3644 Ok(())
3645 }
3646}
3647
3648#[derive(Debug)]
3651pub(super) struct ConfiguredChannel {
3652 handle: ChannelHandle,
3653 protocol_schema: ProtocolSchemaId,
3654 durable: bool,
3656 origin: ChannelOrigin,
3660 state: AtomicU8,
3664 quiesce_reason: OnceLock<String>,
3669}
3670
3671impl ConfiguredChannel {
3672 fn state(&self) -> ChannelState {
3678 if self.state.load(Ordering::Acquire) == STATE_QUIESCED {
3679 ChannelState::Quiesced {
3680 reason: self.recorded_quiesce_reason(),
3681 }
3682 } else {
3683 ChannelState::Active
3684 }
3685 }
3686
3687 fn recorded_quiesce_reason(&self) -> String {
3690 self.quiesce_reason
3691 .get()
3692 .map_or_else(|| UNRECORDED_QUIESCE_REASON.to_owned(), Clone::clone)
3693 }
3694
3695 fn quiesce(&self, name: &str, reason: &str) -> Result<(), ChannelRegistryError> {
3704 let recorded = match self.quiesce_reason.set(reason.to_owned()) {
3710 Ok(()) => reason.to_owned(),
3711 Err(_rejected) => self.recorded_quiesce_reason(),
3712 };
3713 let _flipped_here = self.state.compare_exchange(
3718 STATE_ACTIVE,
3719 STATE_QUIESCED,
3720 Ordering::Release,
3721 Ordering::Acquire,
3722 );
3723 if recorded == reason {
3724 return Ok(());
3725 }
3726 Err(ChannelRegistryError::AlreadyQuiesced {
3727 name: name.to_owned(),
3728 reason: recorded,
3729 })
3730 }
3731}
3732
3733#[derive(Debug)]
3734struct LiminalSubscriptionResource {
3735 subscription: liminal::channel::SubscriptionHandle,
3736}
3737
3738impl SubscriptionResource for LiminalSubscriptionResource {
3739 fn try_next_positioned(
3740 &mut self,
3741 ) -> Result<Option<(liminal::envelope::Envelope, Option<u64>)>, ServerError> {
3742 self.subscription
3743 .try_next_positioned()
3744 .map_err(|error| subscription_failure(&error))
3745 }
3746
3747 fn acknowledge_position(&self, position: u64) -> Result<(), ServerError> {
3748 self.subscription
3749 .acknowledge_position(position)
3750 .map_err(|error| subscription_failure(&error))
3751 }
3752
3753 fn consumer_position(&self) -> Result<Option<u64>, ServerError> {
3754 self.subscription
3755 .consumer_position()
3756 .map_err(|error| subscription_failure(&error))
3757 }
3758 fn unsubscribe(self: Box<Self>) -> Result<(), ServerError> {
3759 drop(self.subscription);
3760 Ok(())
3761 }
3762
3763 fn is_overflowed(&self) -> bool {
3764 self.subscription.is_overflowed()
3765 }
3766
3767 fn subscriber_pid(&self) -> Option<u64> {
3768 Some(self.subscription.subscriber_pid())
3769 }
3770
3771 fn has_pending(&self) -> bool {
3772 self.subscription.has_pending()
3773 }
3774
3775 fn try_next(&mut self) -> Option<liminal::envelope::Envelope> {
3776 match self.subscription.try_next() {
3777 Ok(envelope) => envelope,
3778 Err(error) => {
3779 tracing::error!(
3780 %error,
3781 "subscription could not receive its next envelope"
3782 );
3783 None
3784 }
3785 }
3786 }
3787}
3788
3789fn subscription_failure(error: &liminal::error::LiminalError) -> ServerError {
3791 ServerError::ListenerAccept {
3792 message: format!("liminal subscription: {error}"),
3793 }
3794}
3795
3796pub(super) fn access_to_server_error(error: &ChannelAccessError) -> ServerError {
3818 let message = match error {
3819 ChannelAccessError::NotRegistered { name } => {
3820 format!("channel '{name}' is not configured")
3821 }
3822 ChannelAccessError::RosterUnavailable { message } => message.clone(),
3823 ChannelAccessError::Quiesced { .. }
3824 | ChannelAccessError::ConsoleNamespace { .. }
3825 | ChannelAccessError::ConsoleTapTarget { .. }
3826 | ChannelAccessError::ConsoleTapLimit { .. } => error.to_string(),
3827 };
3828 ServerError::ListenerAccept { message }
3829}
3830
3831fn refuse_console_tap_target(channel: &str) -> Result<(), ChannelAccessError> {
3837 use super::channel_registry::{console_tap_target, is_console_subject};
3838 match console_tap_target(channel) {
3839 Some(target) if target.is_empty() || is_console_subject(target) => {
3840 Err(ChannelAccessError::ConsoleTapTarget {
3841 name: channel.to_owned(),
3842 })
3843 }
3844 _ => Ok(()),
3845 }
3846}
3847
3848fn refuse_console_namespace(channel: &str) -> Result<(), ChannelAccessError> {
3853 if super::channel_registry::is_console_subject(channel) {
3854 return Err(ChannelAccessError::ConsoleNamespace {
3855 name: channel.to_owned(),
3856 });
3857 }
3858 Ok(())
3859}
3860
3861pub(super) fn server_error_from_protocol(error: &ProtocolError) -> ServerError {
3862 ServerError::ListenerAccept {
3863 message: format!("protocol operation failed: {error}"),
3864 }
3865}
3866
3867#[cfg(test)]
3870#[path = "services_registry_tests.rs"]
3871mod services_registry_tests;
3872
3873#[cfg(test)]
3874mod durable_store_tests {
3875 type TestResult = Result<(), Box<dyn std::error::Error>>;
3877
3878 trait Must<T> {
3880 fn must(self, what: &str) -> Result<T, String>;
3881 }
3882
3883 impl<T, E: std::fmt::Debug> Must<T> for Result<T, E> {
3884 fn must(self, what: &str) -> Result<T, String> {
3885 self.map_err(|error| format!("{what}: {error:?}"))
3886 }
3887 }
3888
3889 impl<T> Must<T> for Option<T> {
3890 fn must(self, what: &str) -> Result<T, String> {
3891 self.ok_or_else(|| what.to_owned())
3892 }
3893 }
3894
3895 use liminal::durability::open_ephemeral_rooted;
3896
3897 use super::subsystem_census::RecordingSubsystems;
3898 use super::{
3899 ConnectionServices, DEFAULT_SHARD_COUNT, LiminalConnectionServices, SchedulerSubsystem,
3900 build_connection_services, build_connection_services_via, build_durable_store,
3901 };
3902 use crate::ServerError;
3903 use crate::config::types::{LimitsConfig, ServerConfig, ServicesConfig};
3904
3905 fn entry_count(root: &std::path::Path) -> Result<usize, String> {
3908 Ok(std::fs::read_dir(root)
3909 .must("ephemeral root is readable")?
3910 .count())
3911 }
3912
3913 fn config_with_profile(profile: &str) -> Result<ServerConfig, String> {
3918 Ok(ServerConfig {
3919 listen_address: "127.0.0.1:0".parse().must("valid socket addr")?,
3920 extra_listen_addresses: Vec::new(),
3921 health_listen_address: "127.0.0.1:1".parse().must("valid socket addr")?,
3922 drain_timeout_ms: 30_000,
3923 channels: Vec::new(),
3924 routing_rules: Vec::new(),
3925 persistence_path: None,
3926 cluster: None,
3927 auth: None,
3928 services: ServicesConfig {
3929 profile: profile.to_owned(),
3930 ..ServicesConfig::default()
3931 },
3932 limits: LimitsConfig::default(),
3933 participant: None,
3934 websocket: None,
3935 })
3936 }
3937
3938 #[test]
3949 fn worker_front_door_builds_no_store_and_no_temp_dir() -> TestResult {
3950 let front_door_root = tempfile::tempdir().must("test can create an ephemeral root")?;
3951 let full_root = tempfile::tempdir().must("test can create an ephemeral root")?;
3952
3953 let front_door_subsystems = RecordingSubsystems::rooted(front_door_root.path());
3954 let front_door: std::sync::Arc<dyn ConnectionServices> = build_connection_services_via(
3955 &config_with_profile("worker-front-door")?,
3956 &front_door_subsystems,
3957 )
3958 .must("worker-front-door services build")?;
3959 assert!(
3960 !front_door.supports_channel_operations(),
3961 "the worker front door serves no channel operations"
3962 );
3963 assert_eq!(
3964 entry_count(front_door_root.path())?,
3965 0,
3966 "the worker front door creates no ephemeral store directory (no haematite, no temp dir)"
3967 );
3968
3969 let full_subsystems = RecordingSubsystems::rooted(full_root.path());
3970 let mut config = config_with_profile("full")?;
3971 config.persistence_path = Some(full_root.path().to_path_buf());
3972 let full =
3973 build_connection_services_via(&config, &full_subsystems).must("full services build")?;
3974 assert!(
3975 full.supports_channel_operations(),
3976 "full mode serves channel operations"
3977 );
3978 assert_eq!(
3979 entry_count(full_root.path())?,
3980 1,
3981 "full mode builds its persistent database in the explicit directory"
3982 );
3983
3984 drop(front_door);
3985 drop(full);
3986 Ok(())
3987 }
3988
3989 #[test]
4007 fn worker_profile_census_is_empty_and_full_profile_records_all_schedulers() -> TestResult {
4008 let worker_root = tempfile::tempdir().must("test can create an ephemeral root")?;
4009 let full_root = tempfile::tempdir().must("test can create an ephemeral root")?;
4010
4011 let worker_subsystems = RecordingSubsystems::rooted(worker_root.path());
4012 let front_door = build_connection_services_via(
4013 &config_with_profile("worker-front-door")?,
4014 &worker_subsystems,
4015 )
4016 .must("worker-front-door services build")?;
4017 assert_eq!(
4018 worker_subsystems.recorded(),
4019 Vec::<SchedulerSubsystem>::new(),
4020 "the worker front door constructs no scheduler-owning subsystem"
4021 );
4022
4023 let full_subsystems = RecordingSubsystems::rooted(full_root.path());
4024 let mut config = config_with_profile("full")?;
4025 config.persistence_path = Some(full_root.path().to_path_buf());
4026 let full =
4027 build_connection_services_via(&config, &full_subsystems).must("full services build")?;
4028 assert_eq!(
4029 full_subsystems.recorded(),
4030 vec![
4031 SchedulerSubsystem::ChannelSupervisor,
4032 SchedulerSubsystem::ConversationSupervisor,
4033 SchedulerSubsystem::HaematiteStore,
4034 ],
4035 "the full profile constructs every scheduler-owning subsystem, once each — \
4036 the positive control proving the census instrument detects them"
4037 );
4038
4039 drop(front_door);
4040 drop(full);
4041 Ok(())
4042 }
4043
4044 #[test]
4048 fn full_only_constructors_reject_worker_profile() -> TestResult {
4049 let config = config_with_profile("worker-front-door")?;
4050
4051 let from_config = LiminalConnectionServices::from_config(&config);
4052 assert!(
4053 matches!(from_config, Err(ServerError::ConfigValidation { .. })),
4054 "from_config must reject a worker-front-door profile with ConfigValidation, got {from_config:?}"
4055 );
4056
4057 let root = tempfile::tempdir().must("test can create an ephemeral root")?;
4058 let store = open_ephemeral_rooted(root.path(), DEFAULT_SHARD_COUNT)
4059 .must("test store for the rejection check builds")?;
4060 let from_config_with_store =
4061 LiminalConnectionServices::from_config_with_store(&config, std::sync::Arc::new(store));
4062 assert!(
4063 matches!(
4064 from_config_with_store,
4065 Err(ServerError::ConfigValidation { .. })
4066 ),
4067 "from_config_with_store must reject a worker-front-door profile with ConfigValidation"
4068 );
4069 Ok(())
4070 }
4071
4072 #[test]
4077 fn build_connection_services_rejects_worker_profile_with_full_only_fields() -> TestResult {
4078 let mut config = config_with_profile("worker-front-door")?;
4079 config.channels = vec![crate::config::types::ChannelDef {
4080 name: "orders".to_owned(),
4081 schema_ref: None,
4082 durable: false,
4083 loaded_schema: None,
4084 }];
4085 config.persistence_path = Some(std::path::PathBuf::from("/tmp"));
4086
4087 let result = build_connection_services(&config);
4088 let Err(ServerError::ConfigValidation { message }) = result else {
4089 return Err("expected ConfigValidation for worker profile with full-only fields".into());
4090 };
4091 assert!(message.contains("builds no channels"), "got: {message}");
4092 assert!(
4093 message.contains("builds no durable store"),
4094 "got: {message}"
4095 );
4096 Ok(())
4097 }
4098
4099 #[test]
4102 fn persistent_store_uses_configured_path_and_creates_no_temp_dir() -> TestResult {
4103 let home = tempfile::tempdir().must("test can create a temp dir")?;
4104 let ephemeral_root = tempfile::tempdir().must("test can create an ephemeral root")?;
4105
4106 let store = build_durable_store(home.path()).must("persistent store builds")?;
4107
4108 assert!(
4109 home.path().join("durability").join("config.json").exists(),
4110 "the persistent database is created under the configured path"
4111 );
4112 assert_eq!(
4113 entry_count(ephemeral_root.path())?,
4114 0,
4115 "the persistent branch creates no ephemeral guard directory"
4116 );
4117
4118 drop(store);
4119 Ok(())
4120 }
4121
4122 #[test]
4123 fn missing_path_refuses_before_any_store_is_opened() -> Result<(), Box<dyn std::error::Error>> {
4124 let root = tempfile::tempdir()?;
4125 let factory = RecordingSubsystems::rooted(root.path());
4126 let result = build_connection_services_via(&config_with_profile("full")?, &factory);
4127 assert!(
4128 matches!(result, Err(ServerError::ConfigValidation { message }) if message.contains("no store is opened"))
4129 );
4130 assert_eq!(entry_count(root.path())?, 0);
4131 assert!(factory.recorded().is_empty());
4132 Ok(())
4133 }
4134
4135 #[test]
4143 fn a_store_whose_config_predates_the_budget_field_opens_with_no_operator_hand() -> TestResult {
4144 let home = tempfile::tempdir().must("test can create a temp dir")?;
4145 let store = build_durable_store(home.path()).must("persistent store builds")?;
4146 drop(store);
4147
4148 let config_file = home.path().join("durability").join("config.json");
4149 let bytes = std::fs::read(&config_file).must("config.json is readable")?;
4150 let mut config: serde_json::Value =
4151 serde_json::from_slice(&bytes).must("config.json parses")?;
4152 config
4153 .as_object_mut()
4154 .must("config.json is an object")?
4155 .remove("node_cache_budget")
4156 .must("the created config carries the budget to remove")?;
4157 std::fs::write(
4158 &config_file,
4159 serde_json::to_vec_pretty(&config).must("serializes")?,
4160 )
4161 .must("test can age the config")?;
4162
4163 let reopened = build_durable_store(home.path())
4164 .must("a pre-budget store opens with no operator action")?;
4165 drop(reopened);
4166
4167 let after = std::fs::read(&config_file).must("config.json is readable after")?;
4168 let migrated: serde_json::Value =
4169 serde_json::from_slice(&after).must("migrated config parses")?;
4170 assert_eq!(
4171 migrated.get("node_cache_budget"),
4172 Some(&serde_json::Value::String("unlimited".to_owned())),
4173 "the migrated file carries the budget the create arm chooses"
4174 );
4175 Ok(())
4176 }
4177
4178 #[test]
4184 fn a_corrupt_config_still_refuses_and_is_left_untouched() -> TestResult {
4185 let home = tempfile::tempdir().must("test can create a temp dir")?;
4186 let data_dir = home.path().join("durability");
4187 std::fs::create_dir_all(&data_dir).must("test can create the data dir")?;
4188 let config_file = data_dir.join("config.json");
4189 let garbage = b"{ not json at all";
4190 std::fs::write(&config_file, garbage).must("test can write garbage")?;
4191
4192 let refused = build_durable_store(home.path());
4193 assert!(refused.is_err(), "a corrupt config refuses to open");
4194 assert_eq!(
4195 std::fs::read(&config_file)
4196 .must("config.json is still readable")?
4197 .as_slice(),
4198 garbage,
4199 "the refusing file is left byte-identical for the operator to read"
4200 );
4201 Ok(())
4202 }
4203
4204 #[test]
4208 fn a_config_already_carrying_the_budget_is_left_byte_identical() -> TestResult {
4209 let home = tempfile::tempdir().must("test can create a temp dir")?;
4210 let store = build_durable_store(home.path()).must("persistent store builds")?;
4211 drop(store);
4212
4213 let config_file = home.path().join("durability").join("config.json");
4214 let before = std::fs::read(&config_file).must("config.json is readable")?;
4215 let reopened = build_durable_store(home.path()).must("a current store reopens")?;
4216 drop(reopened);
4217 assert_eq!(
4218 std::fs::read(&config_file).must("config.json is readable after")?,
4219 before,
4220 "a config that already carries the budget is left byte-identical"
4221 );
4222 Ok(())
4223 }
4224
4225 #[test]
4229 fn a_keep_all_durable_store_reclaims_after_the_next_open() -> TestResult {
4230 let dir = tempfile::tempdir().must("test can create a store dir")?;
4231 let data_dir = dir.path().join("durability");
4232 let database = haematite::Database::create(
4233 haematite::DatabaseConfig {
4234 data_dir: data_dir.clone(),
4235 shard_count: 1,
4236 distributed: None,
4237 executor_threads: None,
4238 node_cache_budget: Some(haematite::NodeCacheBudget::Unlimited),
4239 retention: haematite::Retention::KeepAll,
4240 },
4241 haematite::RuntimeOptions {
4242 long_wait_interval: std::time::Duration::from_secs(5),
4243 reclaim_start: haematite::ReclaimStart::AtOpen,
4244 },
4245 )
4246 .must("a keep-all store is created")?;
4247 for round in 0_u32..200 {
4248 let mut value = round.to_be_bytes().to_vec();
4249 value.resize(1_024, 0x5a);
4250 database.put(b"key".to_vec(), value).must("put")?;
4251 database.commit().must("commit")?;
4252 }
4253 drop(database);
4254 let grown =
4255 haematite::db::vacuum::stats(&haematite::db::vacuum::Options::new(data_dir.clone()))
4256 .must("stats of the grown store")?
4257 .totals
4258 .enumerated_bytes;
4259
4260 let reopened = super::open_or_create_database(&data_dir).must("the server opens it")?;
4261 reopened
4264 .release_reclaim()
4265 .must("the held reclaim is released")?;
4266 reopened.quiesce_reclaim().must("reclaim runs to rest")?;
4267 drop(reopened);
4268
4269 let config: serde_json::Value = serde_json::from_slice(
4270 &std::fs::read(data_dir.join("config.json")).must("config is readable")?,
4271 )
4272 .must("config is JSON")?;
4273 assert_eq!(
4274 config["retention"],
4275 serde_json::to_value(super::DURABLE_RETENTION).must("the retention serializes")?
4276 );
4277 let report = haematite::db::vacuum::stats(&haematite::db::vacuum::Options::new(data_dir))
4278 .must("stats of the reclaimed store")?;
4279 assert!(
4280 report.totals.enumerated_bytes <= report.totals.marked_bytes.saturating_mul(3),
4281 "the reopened store holds about its live set: enumerated {} B, marked {} B",
4282 report.totals.enumerated_bytes,
4283 report.totals.marked_bytes
4284 );
4285 assert!(
4286 report.totals.enumerated_bytes.saturating_mul(10) < grown,
4287 "the old copies are gone: {} B now against {grown} B grown",
4288 report.totals.enumerated_bytes
4289 );
4290 Ok(())
4291 }
4292
4293 const STORED_RECLAIM_CONFIG: &str = r#"{
4299 "chunking_policy": {
4300 "chunking": "bytes-v2",
4301 "internal_target_bytes": 49152,
4302 "leaf_target_bytes": 65536
4303 },
4304 "data_dir": "DATA_DIR",
4305 "distributed": null,
4306 "executor_threads": null,
4307 "format_version": 2,
4308 "node_cache_budget": "unlimited",
4309 "retention": {
4310 "reclaim": {
4311 "past_roots": 0
4312 }
4313 },
4314 "shard_count": 8
4315}"#;
4316
4317 #[test]
4324 fn a_stored_reclaim_store_grown_without_reclaim_gives_its_old_copies_back_on_open() -> TestResult {
4325 let dir = tempfile::tempdir().must("test can create a store dir")?;
4326 let data_dir = dir.path().join("durability");
4327 let database = haematite::Database::create(
4328 haematite::DatabaseConfig {
4329 data_dir: data_dir.clone(),
4330 shard_count: 8,
4331 distributed: None,
4332 executor_threads: None,
4333 node_cache_budget: Some(haematite::NodeCacheBudget::Unlimited),
4334 retention: haematite::Retention::KeepAll,
4335 },
4336 haematite::RuntimeOptions {
4337 long_wait_interval: std::time::Duration::from_secs(5),
4338 reclaim_start: haematite::ReclaimStart::AtOpen,
4339 },
4340 )
4341 .must("a keep-all store is created")?;
4342 for round in 0_u32..200 {
4343 let mut value = round.to_be_bytes().to_vec();
4344 value.resize(1_024, 0x5a);
4345 database.put(b"key".to_vec(), value).must("put")?;
4346 database.commit().must("commit")?;
4347 }
4348 for row in 0_u32..200 {
4349 let key = [b"pruned-".as_slice(), &row.to_be_bytes()].concat();
4350 database.put(key, vec![0xa5; 1_024]).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.delete(key).must("delete")?;
4356 }
4357 database.commit().must("commit")?;
4358 drop(database);
4359 let grown_report =
4360 haematite::db::vacuum::stats(&haematite::db::vacuum::Options::new(data_dir.clone()))
4361 .must("stats of the grown store")?;
4362 let grown = grown_report.totals;
4363 assert!(
4364 grown.unmarked_nodes > 0,
4365 "the seed leaves copies no root reaches"
4366 );
4367 let mut owed: std::collections::BTreeSet<usize> = grown_report
4368 .shards
4369 .iter()
4370 .filter(|shard| shard.nodes.unmarked_nodes > 0)
4371 .map(|shard| shard.shard_id)
4372 .collect();
4373
4374 let config_file = data_dir.join("config.json");
4375 let seeded = STORED_RECLAIM_CONFIG.replace(
4376 "DATA_DIR",
4377 data_dir.to_str().must("the test dir is UTF-8")?,
4378 );
4379 std::fs::write(&config_file, &seeded).must("config.json is written")?;
4380
4381 let reopened = super::open_or_create_database(&data_dir).must("the server opens it")?;
4382 let watch = reopened
4383 .watch_reclaim_passes(64)
4384 .must("every shard accepts a pass watch")?;
4385 reopened
4387 .release_reclaim()
4388 .must("the held reclaim is released")?;
4389 let mut unmarked_by_shard = std::collections::BTreeMap::new();
4390 while !owed.is_empty() {
4391 let report = watch
4392 .recv_timeout(std::time::Duration::from_secs(30))
4393 .must("each pass the open owes ends without a write or a quiesce")?;
4394 assert_eq!(
4395 report.outcome,
4396 haematite::ReclaimPassOutcome::Passed,
4397 "the pass the open owed on shard {} finished",
4398 report.shard_id
4399 );
4400 if owed.remove(&report.shard_id) {
4401 unmarked_by_shard.insert(report.shard_id, report.unmarked_bytes);
4402 }
4403 }
4404 drop(reopened);
4405
4406 let after = haematite::db::vacuum::stats(&haematite::db::vacuum::Options::new(data_dir))
4407 .must("stats of the reclaimed store")?
4408 .totals;
4409 eprintln!(
4410 "grown {} B, after {} B, {} unreachable nodes seeded, unmarked bytes by shard {unmarked_by_shard:?}",
4411 grown.enumerated_bytes, after.enumerated_bytes, grown.unmarked_nodes
4412 );
4413 assert!(
4414 after.enumerated_bytes.saturating_mul(10) < grown.enumerated_bytes,
4415 "the old copies are gone: {} B now against {} B grown",
4416 after.enumerated_bytes,
4417 grown.enumerated_bytes
4418 );
4419 let config: serde_json::Value = serde_json::from_slice(
4420 &std::fs::read(&config_file).must("config.json is readable after")?,
4421 )
4422 .must("config is JSON")?;
4423 assert_eq!(
4424 config["retention"],
4425 serde_json::to_value(super::DURABLE_RETENTION).must("the retention serializes")?,
4426 "config.json names reclaim after the open"
4427 );
4428 Ok(())
4429 }
4430}