1use crate::ServerError;
2use crate::auth_pass::PassVerifier;
3use crate::cluster::{self, ClusterHandle};
4use crate::config::file::load_config;
5use crate::config::types::{ClusterConfig, ServiceProfile};
6use crate::health::{ReadinessState, SharedReadinessState, start_health_server};
7use crate::server::connection::WebSocketListener;
8use crate::server::connection::services::{
9 ChannelCluster, LiminalConnectionServices, build_connection_services,
10};
11use crate::server::connection::{
12 ConnectionSupervisor, LysInstall, configured_lys, lys_ask_runtime, start_context_watch,
13};
14use crate::server::listener::ServerListener;
15use crate::server::self_streams::cost::ServerMetadata;
16use crate::server::self_streams::owner::PublisherRuntime;
17use crate::server::self_streams::sink::SelfStreams;
18use crate::server::shutdown::{
19 ShutdownHandle, register_signal_handlers, run_shutdown_sequence_before_flush,
20};
21use crate::services::production::ServiceBindings;
22
23use std::fmt;
24use std::path::Path;
25use std::sync::Arc;
26use std::time::Instant;
27
28fn configured_authentication(
29 config: &crate::config::types::ServerConfig,
30) -> Result<(Option<Vec<u8>>, Option<PassVerifier>), ServerError> {
31 let auth_token = config
32 .auth
33 .as_ref()
34 .map(|auth| auth.token.clone().into_bytes());
35 let pass_verifier = config
36 .auth
37 .as_ref()
38 .map(PassVerifier::from_auth)
39 .transpose()?
40 .flatten();
41 Ok((auth_token, pass_verifier))
42}
43
44pub fn run_participant_log_migration(
57 config_path: &Path,
58) -> Result<crate::server::participant::ParticipantLogMigration, ServerError> {
59 let config = load_config(config_path)?;
60 let Some(persistence_path) = config.persistence_path.as_deref() else {
61 return Err(ServerError::ConfigValidation {
62 message: "the participant log migration needs persistence_path; an ephemeral store has no logs to migrate".to_owned(),
63 });
64 };
65 let Some(participant) = config.participant.as_ref() else {
66 return Err(ServerError::ConfigValidation {
67 message: "the participant log migration needs the [participant] section".to_owned(),
68 });
69 };
70 let store = crate::server::connection::services::open_persistent_store(persistence_path)?;
71 Ok(crate::server::participant::migrate_participant_logs(
72 &store,
73 participant,
74 )?)
75}
76
77pub fn run(config_path: &Path) -> Result<(), ServerError> {
83 let mut stages = StartStages::new();
84 validate_config_path(config_path)?;
85
86 let config = load_config(config_path)?;
87 stages.mark("config");
88
89 crate::metrics::init();
93
94 let readiness = SharedReadinessState::new(ReadinessState::default());
95 let health_server = start_health_server(config.health_listen_address, readiness.clone())?;
96 stages.mark("health");
97 let shutdown_handle = ShutdownHandle::new();
98 let signal_registration = register_signal_handlers(shutdown_handle.clone())?;
99
100 let (auth_token, pass_verifier) = configured_authentication(&config)?;
105 stages.mark("auth");
106 let lys_ask = lys_ask_runtime(config.auth.as_ref())?;
112 let lys = lys_for(&config, lys_ask.as_ref())?;
113 stages.mark("lys");
114 let mut context_watch = None;
115
116 let mut self_stream_publisher = None;
124 let mut spine = None;
125 let (connection_supervisor, cluster_handle) = match config.services.profile()? {
126 ServiceProfile::Full => {
127 let full = start_full_profile(
128 &config,
129 (auth_token, pass_verifier),
130 (lys, lys_ask.as_ref()),
131 &shutdown_handle,
132 &health_server,
133 &readiness,
134 &mut stages,
135 )?;
136 context_watch = full.context_watch;
137 self_stream_publisher = full.self_stream_publisher;
138 spine = full.spine;
139 (full.connection_supervisor, full.cluster_handle)
140 }
141 ServiceProfile::WorkerFrontDoor => {
142 let services = build_connection_services(&config)?;
143 let connection_supervisor = ConnectionSupervisor::with_lys_and_fatal_shutdown(
147 services,
148 auth_token,
149 pass_verifier,
150 lys,
151 config.limits,
152 shutdown_handle.clone(),
153 )?;
154 readiness.set_cluster_configured(false);
155 stages.mark("supervisor");
156 (connection_supervisor, None)
157 }
158 };
159
160 let services =
169 super::services_boot::install(&config, &connection_supervisor, lys_ask.as_ref())?;
170 let (mut listener, mut websocket_listener) = open_listeners(
171 &config,
172 connection_supervisor,
173 &readiness,
174 &mut stages,
175 spine.as_ref(),
176 )?;
177
178 services.listening(listener.local_addr())?;
180 stages.mark("services_registry");
181
182 tracing::debug!(
183 config_path = %config_path.display(),
184 listen_address = %config.listen_address,
185 health_listen_address = %health_server.local_addr(),
186 "liminal server configuration validated"
187 );
188
189 tracing::info!(
190 listen_address = %listener.local_addr(),
191 health_listen_address = %health_server.local_addr(),
192 start_stages = %stages,
193 time_to_listening_ms = u64::try_from(stages.began.elapsed().as_millis()).unwrap_or(u64::MAX),
196 "liminal server started"
197 );
198
199 shutdown_handle.wait();
200 readiness.set_listener_bound(false);
201 services.shutdown_begun();
204
205 if let Some(mut cluster_handle) = cluster_handle {
209 cluster_handle.shutdown();
210 }
211
212 let supervisor = listener.supervisor();
213 let shutdown_result = run_shutdown_sequence_before_flush(
214 &mut listener,
215 websocket_listener.as_mut(),
216 &supervisor,
217 config.drain_timeout(),
218 || finish_self_stream_publisher(&mut self_stream_publisher),
219 );
220 let participant_fatal = supervisor.participant_service_fatal();
221 let publisher_shutdown = finish_self_stream_publisher(&mut self_stream_publisher);
222 drop(context_watch);
224 drop(lys_ask);
225 drop(websocket_listener);
226 drop(signal_registration);
227 raise_in_precedence(
230 health_server.shutdown(),
231 raise_in_precedence(publisher_shutdown, shutdown_result),
232 )?;
233 participant_fatal?.map_or(Ok(()), |fatal| {
234 Err(ServerError::ParticipantServiceFatal { fatal })
235 })
236}
237
238struct FullProfile {
242 connection_supervisor: ConnectionSupervisor,
243 spine: Option<Arc<haematite::EventStore>>,
245 cluster_handle: Option<ClusterHandle>,
246 context_watch: Option<crate::services::context_watch::ContextWatch>,
247 self_stream_publisher: Option<PublisherRuntime>,
248}
249
250fn start_full_profile(
255 config: &crate::config::types::ServerConfig,
256 (auth_token, pass_verifier): (Option<Vec<u8>>, Option<PassVerifier>),
257 (lys, lys_ask): (Option<LysInstall>, Option<&tokio::runtime::Runtime>),
258 shutdown_handle: &ShutdownHandle,
259 health_server: &crate::health::HealthServerHandle,
260 readiness: &SharedReadinessState,
261 stages: &mut StartStages,
262) -> Result<FullProfile, ServerError> {
263 let services = Arc::new(LiminalConnectionServices::from_config(config)?);
264 stages.mark("services");
265 let spine = services.durable_store().reclaim_spine();
268 if let Some(spine) = &spine {
269 health_server.install_spine(spine);
270 }
271 install_ephemeral_sweep_report(&services, health_server)?;
272 if let Some(record) = services.unloadable_conversation_record() {
283 health_server.install_unloadable_record(record);
284 }
285 if let Some(reissuer) = services.credential_reissuer() {
290 health_server.install_credential_reissuer(reissuer);
291 }
292 let channel_cluster = services.channel_cluster().clone();
293 let signals_owner = Arc::clone(&services);
294 let lys = lys
295 .map(|install| configured_service_bindings(install, config, &services, lys_ask))
296 .transpose()?;
297 let bindings = lys.as_ref().and_then(|install| install.services.clone());
298 let connection_supervisor = ConnectionSupervisor::with_lys_and_fatal_shutdown(
299 services,
300 auth_token,
301 pass_verifier,
302 lys,
303 config.limits,
304 shutdown_handle.clone(),
305 )?;
306 stages.mark("supervisor");
307 let mut context_watch = None;
308 if let Some(bindings) = bindings.as_deref() {
309 context_watch = start_context_watch(signals_owner.as_ref(), bindings, &config.limits)?;
310 stages.mark("services");
311 }
312
313 let self_stream_publisher = start_self_stream_publisher(
314 &signals_owner,
315 &connection_supervisor,
316 config,
317 shutdown_handle,
318 )?;
319 stages.mark("incarnation");
320
321 let cluster_handle =
326 start_configured_cluster(&channel_cluster, config.cluster.as_ref(), readiness)?;
327 stages.mark("cluster");
328 Ok(FullProfile {
329 connection_supervisor,
330 spine,
331 cluster_handle,
332 context_watch,
333 self_stream_publisher,
334 })
335}
336
337fn configured_service_bindings(
343 mut install: LysInstall,
344 config: &crate::config::types::ServerConfig,
345 services: &Arc<LiminalConnectionServices>,
346 lys_ask: Option<&tokio::runtime::Runtime>,
347) -> Result<LysInstall, ServerError> {
348 let Some(lys) = config.auth.as_ref().and_then(|auth| auth.lys.as_ref()) else {
349 return Ok(install);
350 };
351 let Some(runtime) = lys_ask else {
352 return Err(ServerError::ConfigValidation {
353 message: "auth.lys.services needs the Lys ask runtime; none was started".to_owned(),
354 });
355 };
356 let now = std::time::SystemTime::now()
357 .duration_since(std::time::UNIX_EPOCH)
358 .map_or(0, |elapsed| elapsed.as_secs());
359 install.services =
360 ServiceBindings::configure(lys, Arc::clone(services), runtime.handle().clone(), now)
361 .map_err(|error| ServerError::ConfigValidation {
362 message: format!("auth.lys.services: {error}"),
363 })?
364 .map(Arc::new);
365 Ok(install)
366}
367
368fn lys_for(
371 config: &crate::config::types::ServerConfig,
372 lys_ask: Option<&tokio::runtime::Runtime>,
373) -> Result<Option<LysInstall>, ServerError> {
374 config
375 .auth
376 .as_ref()
377 .map(|auth| configured_lys(auth, lys_ask.map(|runtime| runtime.handle().clone())))
378 .transpose()
379 .map(Option::flatten)
380}
381
382fn open_listeners(
387 config: &crate::config::types::ServerConfig,
388 supervisor: ConnectionSupervisor,
389 readiness: &SharedReadinessState,
390 stages: &mut StartStages,
391 spine: Option<&Arc<haematite::EventStore>>,
392) -> Result<(ServerListener, Option<WebSocketListener>), ServerError> {
393 readiness.track_admission(supervisor.admission_readiness());
394 let listeners = bind_listeners(config, supervisor, readiness)?;
395 stages.mark("listeners");
396 if let Some(spine) = spine {
397 crate::server::connection::reclaim_placement::release_after_bind(spine, stages.began);
398 }
399 Ok(listeners)
400}
401
402fn bind_listeners(
403 config: &crate::config::types::ServerConfig,
404 supervisor: ConnectionSupervisor,
405 readiness: &SharedReadinessState,
406) -> Result<(ServerListener, Option<WebSocketListener>), ServerError> {
407 let listener = ServerListener::bind(config, supervisor)?;
408 let websocket_listener = match config.websocket.as_ref() {
413 Some(websocket_config) => Some(WebSocketListener::bind(
414 websocket_config,
415 listener.supervisor(),
416 )?),
417 None => None,
418 };
419 readiness.set_config_loaded(true);
420 readiness.set_listener_bound(true);
421 Ok((listener, websocket_listener))
422}
423
424struct StartStages {
427 began: Instant,
428 marks: Vec<(&'static str, u128)>,
429}
430
431impl StartStages {
432 fn new() -> Self {
433 Self {
434 began: Instant::now(),
435 marks: Vec::with_capacity(8),
436 }
437 }
438
439 fn mark(&mut self, name: &'static str) {
441 self.marks.push((name, self.began.elapsed().as_millis()));
442 }
443}
444
445impl fmt::Display for StartStages {
448 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
449 let mut before = 0;
450 for (index, (name, at)) in self.marks.iter().enumerate() {
451 if index > 0 {
452 formatter.write_str(", ")?;
453 }
454 write!(formatter, "{name} {} ({at})", at.saturating_sub(before))?;
455 before = *at;
456 }
457 Ok(())
458 }
459}
460
461fn validate_config_path(config_path: &Path) -> Result<(), ServerError> {
462 if config_path.as_os_str().is_empty() {
463 return Err(ServerError::ConfigLoad {
464 message: "configuration path is empty".to_owned(),
465 });
466 }
467 Ok(())
468}
469
470pub(in crate::server) fn start_self_stream_publisher(
471 services: &Arc<LiminalConnectionServices>,
472 supervisor: &ConnectionSupervisor,
473 config: &crate::config::types::ServerConfig,
474 shutdown: &ShutdownHandle,
475) -> Result<Option<PublisherRuntime>, ServerError> {
476 bind_self_streams(services, supervisor, config, shutdown)?
477 .map(|streams| {
478 PublisherRuntime::start(services.signals().wake(), streams, shutdown.clone())
479 })
480 .transpose()
481}
482
483fn finish_self_stream_publisher(
484 publisher: &mut Option<PublisherRuntime>,
485) -> Result<(), ServerError> {
486 publisher.as_mut().map_or(Ok(()), PublisherRuntime::finish)
487}
488
489fn start_configured_cluster(
490 channels: &ChannelCluster,
491 config: Option<&ClusterConfig>,
492 readiness: &SharedReadinessState,
493) -> Result<Option<ClusterHandle>, ServerError> {
494 readiness.set_cluster_configured(config.is_some());
495 config
496 .map(|config| start_cluster(channels, config, readiness))
497 .transpose()
498}
499
500pub(in crate::server) fn bind_self_streams(
502 services: &Arc<LiminalConnectionServices>,
503 supervisor: &ConnectionSupervisor,
504 config: &crate::config::types::ServerConfig,
505 shutdown: &ShutdownHandle,
506) -> Result<Option<Arc<SelfStreams>>, ServerError> {
507 let signals = services.signals();
508 supervisor.install_signals(signals.clone())?;
509 let Some(incarnation) = supervisor.current_server_incarnation()? else {
510 crate::metrics::self_streams_disabled();
511 tracing::warn!(
512 "SELF_STREAMS_REQUIRE_PARTICIPANT: self streams disabled because no participant is configured"
513 );
514 crate::metrics::console_streams_disabled();
518 tracing::warn!(
519 "CONSOLE_STREAMS_REQUIRE_PARTICIPANT: console streams disabled because no participant is configured"
520 );
521 return Ok(None);
522 };
523 services.install_self_stream_channels()?;
524 services
526 .console()
527 .incarnation_started(std::time::Instant::now())
528 .map_err(|error| ServerError::ListenerAccept {
529 message: format!("console hub: {error}"),
530 })?;
531 let metadata = ServerMetadata::from_config(config, incarnation).map_err(|error| {
532 ServerError::ConfigValidation {
533 message: error.to_string(),
534 }
535 })?;
536 let streams = Arc::new(SelfStreams::new(
537 metadata,
538 services,
539 signals.wake(),
540 shutdown.clone(),
541 config.services.self_streams,
542 ));
543 signals.install(Arc::clone(&streams))?;
544 services.install_console_channels()?;
545 signals.enable_console()?;
546 let wake = signals.wake();
547 wake.install_source(&streams)?;
548 let counts = services.store_counts();
549 let names = ["durable", "ephemeral-dedup"];
550 let configured = names
551 .get(..counts.store_count())
552 .ok_or_else(|| SelfStreams::failure("unexpected number of counted stores"))?;
553 counts.install_observer(configured, wake).map_err(|error| {
554 SelfStreams::failure(&format!("store count observer installation: {error}"))
555 })?;
556 Ok(Some(streams))
557}
558
559fn raise_in_precedence(
564 health_shutdown: Result<(), ServerError>,
565 shutdown_sequence: Result<(), ServerError>,
566) -> Result<(), ServerError> {
567 if let Err(error) = health_shutdown {
568 if let Err(sequence_error) = &shutdown_sequence {
569 tracing::error!(
570 %sequence_error,
571 "shutdown sequence failed; the health endpoint shutdown failure takes the exit code"
572 );
573 }
574 return Err(error);
575 }
576 shutdown_sequence
577}
578
579fn start_cluster(
591 channel_cluster: &ChannelCluster,
592 cluster_config: &ClusterConfig,
593 readiness: &SharedReadinessState,
594) -> Result<ClusterHandle, ServerError> {
595 let resolver = channel_cluster
596 .resolver()
597 .cloned()
598 .ok_or_else(|| ServerError::ClusterJoin {
599 message: "clustering configured but channel supervisor has no distribution resolver"
600 .to_owned(),
601 })?;
602 let scheduler = channel_cluster.supervisor().scheduler();
603 let observer = Arc::clone(channel_cluster.observer());
604 let readiness = readiness.clone();
605 cluster::start(
606 &scheduler,
607 resolver,
608 cluster_config,
609 move |sync| {
610 let peers = Arc::clone(&observer);
613 if !sync.set_peer_listener(Arc::new(move |name, present| peers.peer(name, present))) {
614 tracing::error!("cluster peer listener was already installed");
615 }
616 if let Err(error) = observer.set_cluster(Arc::new(sync)) {
619 tracing::error!(%error, "cluster observer was not installed");
620 }
621 },
622 move || readiness.set_cluster_membership_established(true),
623 )
624}
625
626fn install_ephemeral_sweep_report(
627 services: &LiminalConnectionServices,
628 health: &crate::health::HealthServerHandle,
629) -> Result<(), ServerError> {
630 let report = services
631 .ephemeral_sweep_counts()
632 .ok_or_else(|| ServerError::HealthEndpoint {
633 message: "full services have no ephemeral root startup sweep report".to_owned(),
634 })?;
635 health.install_ephemeral_sweep_report(report)
636}
637
638#[cfg(test)]
639mod tests {
640 use std::net::SocketAddr;
641
642 use super::{ChannelCluster, ClusterConfig, SharedReadinessState, StartStages, start_cluster};
643 use crate::ServerError;
644 use crate::health::{ClusterReadiness, ReadinessCondition, ReadinessState, readiness_check};
645 use crate::server::connection::services::LiminalConnectionServices;
646
647 #[test]
650 fn start_stages_read_as_lengths_and_ends() {
651 let stages = StartStages {
652 began: std::time::Instant::now(),
653 marks: vec![("config", 3), ("health", 4), ("services", 2_104)],
654 };
655 assert_eq!(
656 stages.to_string(),
657 "config 3 (3), health 1 (4), services 2100 (2104)"
658 );
659 }
660
661 fn unclustered_channel_cluster() -> Result<ChannelCluster, ServerError> {
665 Ok(LiminalConnectionServices::empty()?
666 .channel_cluster()
667 .clone())
668 }
669
670 fn clustered_but_unmet_readiness() -> SharedReadinessState {
671 SharedReadinessState::new(ReadinessState::new(
672 true,
673 true,
674 ClusterReadiness::Configured {
675 membership_established: false,
676 },
677 ))
678 }
679
680 fn sample_cluster_config() -> Result<ClusterConfig, Box<dyn std::error::Error>> {
681 let listen_address: SocketAddr = "127.0.0.1:0".parse()?;
682 Ok(ClusterConfig {
683 node_name: "node-under-test@127.0.0.1".to_owned(),
684 listen_address,
685 seed_nodes: Vec::new(),
686 cookie: "runtime-test-cookie".to_owned(),
687 })
688 }
689
690 #[test]
691 fn failed_cluster_start_leaves_membership_unestablished()
692 -> Result<(), Box<dyn std::error::Error>> {
693 let readiness = clustered_but_unmet_readiness();
694 let channel_cluster = unclustered_channel_cluster()?;
695 let config = sample_cluster_config()?;
696
697 let result = start_cluster(&channel_cluster, &config, &readiness);
700 assert!(
701 result.is_err(),
702 "start_cluster must fail without a distribution resolver"
703 );
704
705 let status = readiness_check(&readiness.snapshot());
707 assert!(
708 !status.ready,
709 "readiness must remain not-ready after a failed start"
710 );
711 assert!(
712 status
713 .unmet_conditions
714 .contains(&ReadinessCondition::ClusterMembershipEstablished),
715 "cluster membership gate must stay unmet after a failed start"
716 );
717
718 Ok(())
719 }
720}