1use std::fmt;
2use std::path::Path;
3use std::sync::Arc;
4use std::time::Instant;
5
6use crate::ServerError;
7use crate::auth_pass::PassVerifier;
8use crate::cluster::{self, ClusterHandle};
9use crate::config::file::load_config;
10use crate::config::types::{ClusterConfig, ServiceProfile};
11use crate::health::{ReadinessState, SharedReadinessState, start_health_server};
12use crate::server::connection::WebSocketListener;
13use crate::server::connection::services::{
14 ChannelCluster, LiminalConnectionServices, build_connection_services,
15};
16use crate::server::connection::{
17 ConnectionSupervisor, LysInstall, configured_lys, lys_ask_runtime, start_context_watch,
18};
19use crate::server::listener::ServerListener;
20use crate::server::self_streams::cost::ServerMetadata;
21use crate::server::self_streams::owner::PublisherRuntime;
22use crate::server::self_streams::sink::SelfStreams;
23use crate::server::shutdown::{
24 ShutdownHandle, register_signal_handlers, run_shutdown_sequence_before_flush,
25};
26use crate::services::production::ServiceBindings;
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 = config
113 .auth
114 .as_ref()
115 .map(|auth| {
116 configured_lys(
117 auth,
118 lys_ask.as_ref().map(|runtime| runtime.handle().clone()),
119 )
120 })
121 .transpose()?
122 .flatten();
123 stages.mark("lys");
124 let mut context_watch = None;
125
126 let mut self_stream_publisher = None;
134 let (connection_supervisor, cluster_handle) = match config.services.profile()? {
135 ServiceProfile::Full => {
136 let full = start_full_profile(
137 &config,
138 (auth_token, pass_verifier),
139 (lys, lys_ask.as_ref()),
140 &shutdown_handle,
141 &health_server,
142 &readiness,
143 &mut stages,
144 )?;
145 context_watch = full.context_watch;
146 self_stream_publisher = full.self_stream_publisher;
147 (full.connection_supervisor, full.cluster_handle)
148 }
149 ServiceProfile::WorkerFrontDoor => {
150 let services = build_connection_services(&config)?;
151 let connection_supervisor = ConnectionSupervisor::with_lys_and_fatal_shutdown(
155 services,
156 auth_token,
157 pass_verifier,
158 lys,
159 config.limits,
160 shutdown_handle.clone(),
161 )?;
162 readiness.set_cluster_configured(false);
163 stages.mark("supervisor");
164 (connection_supervisor, None)
165 }
166 };
167
168 readiness.track_admission(connection_supervisor.admission_readiness());
175
176 let (mut listener, mut websocket_listener) =
177 bind_listeners(&config, connection_supervisor, &readiness)?;
178 stages.mark("listeners");
179
180 tracing::debug!(
181 config_path = %config_path.display(),
182 listen_address = %config.listen_address,
183 health_listen_address = %health_server.local_addr(),
184 "liminal server configuration validated"
185 );
186
187 tracing::info!(
188 listen_address = %listener.local_addr(),
189 health_listen_address = %health_server.local_addr(),
190 start_stages = %stages,
191 "liminal server started"
192 );
193
194 shutdown_handle.wait();
195 readiness.set_listener_bound(false);
196
197 if let Some(mut cluster_handle) = cluster_handle {
201 cluster_handle.shutdown();
202 }
203
204 let supervisor = listener.supervisor();
205 let shutdown_result = run_shutdown_sequence_before_flush(
206 &mut listener,
207 websocket_listener.as_mut(),
208 &supervisor,
209 config.drain_timeout(),
210 || finish_self_stream_publisher(&mut self_stream_publisher),
211 );
212 let participant_fatal = supervisor.participant_service_fatal();
213 let publisher_shutdown = finish_self_stream_publisher(&mut self_stream_publisher);
214 drop(context_watch);
216 drop(lys_ask);
217 drop(websocket_listener);
218 drop(signal_registration);
219 raise_in_precedence(
222 health_server.shutdown(),
223 raise_in_precedence(publisher_shutdown, shutdown_result),
224 )?;
225 participant_fatal?.map_or(Ok(()), |fatal| {
226 Err(ServerError::ParticipantServiceFatal { fatal })
227 })
228}
229
230struct FullProfile {
234 connection_supervisor: ConnectionSupervisor,
235 cluster_handle: Option<ClusterHandle>,
236 context_watch: Option<crate::services::context_watch::ContextWatch>,
237 self_stream_publisher: Option<PublisherRuntime>,
238}
239
240fn start_full_profile(
245 config: &crate::config::types::ServerConfig,
246 (auth_token, pass_verifier): (Option<Vec<u8>>, Option<PassVerifier>),
247 (lys, lys_ask): (Option<LysInstall>, Option<&tokio::runtime::Runtime>),
248 shutdown_handle: &ShutdownHandle,
249 health_server: &crate::health::HealthServerHandle,
250 readiness: &SharedReadinessState,
251 stages: &mut StartStages,
252) -> Result<FullProfile, ServerError> {
253 let services = Arc::new(LiminalConnectionServices::from_config(config)?);
254 stages.mark("services");
255 install_ephemeral_sweep_report(&services, health_server)?;
256 if let Some(record) = services.unloadable_conversation_record() {
266 health_server.install_unloadable_record(record);
267 }
268 if let Some(reissuer) = services.credential_reissuer() {
273 health_server.install_credential_reissuer(reissuer);
274 }
275 let channel_cluster = services.channel_cluster().clone();
276 let signals_owner = Arc::clone(&services);
277 let lys = lys
278 .map(|install| configured_service_bindings(install, config, &services, lys_ask))
279 .transpose()?;
280 let bindings = lys.as_ref().and_then(|install| install.services.clone());
281 let connection_supervisor = ConnectionSupervisor::with_lys_and_fatal_shutdown(
282 services,
283 auth_token,
284 pass_verifier,
285 lys,
286 config.limits,
287 shutdown_handle.clone(),
288 )?;
289 stages.mark("supervisor");
290 let mut context_watch = None;
291 if let Some(bindings) = bindings.as_deref() {
292 context_watch = start_context_watch(signals_owner.as_ref(), bindings, &config.limits)?;
293 stages.mark("services");
294 }
295
296 let self_stream_publisher = start_self_stream_publisher(
297 &signals_owner,
298 &connection_supervisor,
299 config,
300 shutdown_handle,
301 )?;
302 stages.mark("incarnation");
303
304 let cluster_handle =
309 start_configured_cluster(&channel_cluster, config.cluster.as_ref(), readiness)?;
310 stages.mark("cluster");
311 Ok(FullProfile {
312 connection_supervisor,
313 cluster_handle,
314 context_watch,
315 self_stream_publisher,
316 })
317}
318
319fn configured_service_bindings(
325 mut install: LysInstall,
326 config: &crate::config::types::ServerConfig,
327 services: &Arc<LiminalConnectionServices>,
328 lys_ask: Option<&tokio::runtime::Runtime>,
329) -> Result<LysInstall, ServerError> {
330 let Some(lys) = config.auth.as_ref().and_then(|auth| auth.lys.as_ref()) else {
331 return Ok(install);
332 };
333 let Some(runtime) = lys_ask else {
334 return Err(ServerError::ConfigValidation {
335 message: "auth.lys.services needs the Lys ask runtime; none was started".to_owned(),
336 });
337 };
338 let now = std::time::SystemTime::now()
339 .duration_since(std::time::UNIX_EPOCH)
340 .map_or(0, |elapsed| elapsed.as_secs());
341 install.services =
342 ServiceBindings::configure(lys, Arc::clone(services), runtime.handle().clone(), now)
343 .map_err(|error| ServerError::ConfigValidation {
344 message: format!("auth.lys.services: {error}"),
345 })?
346 .map(Arc::new);
347 Ok(install)
348}
349
350fn bind_listeners(
351 config: &crate::config::types::ServerConfig,
352 supervisor: ConnectionSupervisor,
353 readiness: &SharedReadinessState,
354) -> Result<(ServerListener, Option<WebSocketListener>), ServerError> {
355 let listener = ServerListener::bind(config, supervisor)?;
356 let websocket_listener = match config.websocket.as_ref() {
361 Some(websocket_config) => Some(WebSocketListener::bind(
362 websocket_config,
363 listener.supervisor(),
364 )?),
365 None => None,
366 };
367 readiness.set_config_loaded(true);
368 readiness.set_listener_bound(true);
369 Ok((listener, websocket_listener))
370}
371
372struct StartStages {
375 began: Instant,
376 marks: Vec<(&'static str, u128)>,
377}
378
379impl StartStages {
380 fn new() -> Self {
381 Self {
382 began: Instant::now(),
383 marks: Vec::with_capacity(8),
384 }
385 }
386
387 fn mark(&mut self, name: &'static str) {
389 self.marks.push((name, self.began.elapsed().as_millis()));
390 }
391}
392
393impl fmt::Display for StartStages {
396 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
397 let mut before = 0;
398 for (index, (name, at)) in self.marks.iter().enumerate() {
399 if index > 0 {
400 formatter.write_str(", ")?;
401 }
402 write!(formatter, "{name} {} ({at})", at.saturating_sub(before))?;
403 before = *at;
404 }
405 Ok(())
406 }
407}
408
409fn validate_config_path(config_path: &Path) -> Result<(), ServerError> {
410 if config_path.as_os_str().is_empty() {
411 return Err(ServerError::ConfigLoad {
412 message: "configuration path is empty".to_owned(),
413 });
414 }
415 Ok(())
416}
417
418pub(in crate::server) fn start_self_stream_publisher(
419 services: &Arc<LiminalConnectionServices>,
420 supervisor: &ConnectionSupervisor,
421 config: &crate::config::types::ServerConfig,
422 shutdown: &ShutdownHandle,
423) -> Result<Option<PublisherRuntime>, ServerError> {
424 bind_self_streams(services, supervisor, config, shutdown)?
425 .map(|streams| {
426 PublisherRuntime::start(services.signals().wake(), streams, shutdown.clone())
427 })
428 .transpose()
429}
430
431fn finish_self_stream_publisher(
432 publisher: &mut Option<PublisherRuntime>,
433) -> Result<(), ServerError> {
434 publisher.as_mut().map_or(Ok(()), PublisherRuntime::finish)
435}
436
437fn start_configured_cluster(
438 channels: &ChannelCluster,
439 config: Option<&ClusterConfig>,
440 readiness: &SharedReadinessState,
441) -> Result<Option<ClusterHandle>, ServerError> {
442 readiness.set_cluster_configured(config.is_some());
443 config
444 .map(|config| start_cluster(channels, config, readiness))
445 .transpose()
446}
447
448pub(in crate::server) fn bind_self_streams(
450 services: &Arc<LiminalConnectionServices>,
451 supervisor: &ConnectionSupervisor,
452 config: &crate::config::types::ServerConfig,
453 shutdown: &ShutdownHandle,
454) -> Result<Option<Arc<SelfStreams>>, ServerError> {
455 let signals = services.signals();
456 supervisor.install_signals(signals.clone())?;
457 let Some(incarnation) = supervisor.current_server_incarnation()? else {
458 crate::metrics::self_streams_disabled();
459 tracing::warn!(
460 "SELF_STREAMS_REQUIRE_PARTICIPANT: self streams disabled because no participant is configured"
461 );
462 crate::metrics::console_streams_disabled();
466 tracing::warn!(
467 "CONSOLE_STREAMS_REQUIRE_PARTICIPANT: console streams disabled because no participant is configured"
468 );
469 return Ok(None);
470 };
471 services.install_self_stream_channels()?;
472 services
474 .console()
475 .incarnation_started(std::time::Instant::now())
476 .map_err(|error| ServerError::ListenerAccept {
477 message: format!("console hub: {error}"),
478 })?;
479 let metadata = ServerMetadata::from_config(config, incarnation).map_err(|error| {
480 ServerError::ConfigValidation {
481 message: error.to_string(),
482 }
483 })?;
484 let streams = Arc::new(SelfStreams::new(
485 metadata,
486 services,
487 signals.wake(),
488 shutdown.clone(),
489 config.services.self_streams,
490 ));
491 signals.install(Arc::clone(&streams))?;
492 services.install_console_channels()?;
493 signals.enable_console()?;
494 let wake = signals.wake();
495 wake.install_source(services, &streams)?;
496 let counts = services.store_counts();
497 let names = ["durable", "ephemeral-dedup"];
498 let configured = names
499 .get(..counts.store_count())
500 .ok_or_else(|| SelfStreams::failure("unexpected number of counted stores"))?;
501 counts.install_observer(configured, wake).map_err(|error| {
502 SelfStreams::failure(&format!("store count observer installation: {error}"))
503 })?;
504 Ok(Some(streams))
505}
506
507fn raise_in_precedence(
512 health_shutdown: Result<(), ServerError>,
513 shutdown_sequence: Result<(), ServerError>,
514) -> Result<(), ServerError> {
515 if let Err(error) = health_shutdown {
516 if let Err(sequence_error) = &shutdown_sequence {
517 tracing::error!(
518 %sequence_error,
519 "shutdown sequence failed; the health endpoint shutdown failure takes the exit code"
520 );
521 }
522 return Err(error);
523 }
524 shutdown_sequence
525}
526
527fn start_cluster(
539 channel_cluster: &ChannelCluster,
540 cluster_config: &ClusterConfig,
541 readiness: &SharedReadinessState,
542) -> Result<ClusterHandle, ServerError> {
543 let resolver = channel_cluster
544 .resolver()
545 .cloned()
546 .ok_or_else(|| ServerError::ClusterJoin {
547 message: "clustering configured but channel supervisor has no distribution resolver"
548 .to_owned(),
549 })?;
550 let scheduler = channel_cluster.supervisor().scheduler();
551 let observer = Arc::clone(channel_cluster.observer());
552 let readiness = readiness.clone();
553 cluster::start(
554 &scheduler,
555 resolver,
556 cluster_config,
557 move |sync| {
558 let peers = Arc::clone(&observer);
561 if !sync.set_peer_listener(Arc::new(move |name, present| peers.peer(name, present))) {
562 tracing::error!("cluster peer listener was already installed");
563 }
564 if let Err(error) = observer.set_cluster(Arc::new(sync)) {
567 tracing::error!(%error, "cluster observer was not installed");
568 }
569 },
570 move || readiness.set_cluster_membership_established(true),
571 )
572}
573
574fn install_ephemeral_sweep_report(
575 services: &LiminalConnectionServices,
576 health: &crate::health::HealthServerHandle,
577) -> Result<(), ServerError> {
578 let report = services
579 .ephemeral_sweep_counts()
580 .ok_or_else(|| ServerError::HealthEndpoint {
581 message: "full services have no ephemeral root startup sweep report".to_owned(),
582 })?;
583 health.install_ephemeral_sweep_report(report)
584}
585
586#[cfg(test)]
587mod tests {
588 use std::net::SocketAddr;
589
590 use super::{ChannelCluster, ClusterConfig, SharedReadinessState, StartStages, start_cluster};
591 use crate::ServerError;
592 use crate::health::{ClusterReadiness, ReadinessCondition, ReadinessState, readiness_check};
593 use crate::server::connection::services::LiminalConnectionServices;
594
595 #[test]
598 fn start_stages_read_as_lengths_and_ends() {
599 let stages = StartStages {
600 began: std::time::Instant::now(),
601 marks: vec![("config", 3), ("health", 4), ("services", 2_104)],
602 };
603 assert_eq!(
604 stages.to_string(),
605 "config 3 (3), health 1 (4), services 2100 (2104)"
606 );
607 }
608
609 fn unclustered_channel_cluster() -> Result<ChannelCluster, ServerError> {
613 Ok(LiminalConnectionServices::empty()?
614 .channel_cluster()
615 .clone())
616 }
617
618 fn clustered_but_unmet_readiness() -> SharedReadinessState {
619 SharedReadinessState::new(ReadinessState::new(
620 true,
621 true,
622 ClusterReadiness::Configured {
623 membership_established: false,
624 },
625 ))
626 }
627
628 fn sample_cluster_config() -> Result<ClusterConfig, Box<dyn std::error::Error>> {
629 let listen_address: SocketAddr = "127.0.0.1:0".parse()?;
630 Ok(ClusterConfig {
631 node_name: "node-under-test@127.0.0.1".to_owned(),
632 listen_address,
633 seed_nodes: Vec::new(),
634 cookie: "runtime-test-cookie".to_owned(),
635 })
636 }
637
638 #[test]
639 fn failed_cluster_start_leaves_membership_unestablished()
640 -> Result<(), Box<dyn std::error::Error>> {
641 let readiness = clustered_but_unmet_readiness();
642 let channel_cluster = unclustered_channel_cluster()?;
643 let config = sample_cluster_config()?;
644
645 let result = start_cluster(&channel_cluster, &config, &readiness);
648 assert!(
649 result.is_err(),
650 "start_cluster must fail without a distribution resolver"
651 );
652
653 let status = readiness_check(&readiness.snapshot());
655 assert!(
656 !status.ready,
657 "readiness must remain not-ready after a failed start"
658 );
659 assert!(
660 status
661 .unmet_conditions
662 .contains(&ReadinessCondition::ClusterMembershipEstablished),
663 "cluster membership gate must stay unmet after a failed start"
664 );
665
666 Ok(())
667 }
668}