liminal_server/server/
runtime.rs1use std::path::Path;
2use std::sync::Arc;
3
4use crate::ServerError;
5use crate::cluster::{self, ClusterHandle};
6use crate::config::file::load_config;
7use crate::config::types::{ClusterConfig, ServiceProfile};
8use crate::health::{ReadinessState, SharedReadinessState, start_health_server};
9use crate::server::connection::ConnectionSupervisor;
10use crate::server::connection::WebSocketListener;
11use crate::server::connection::services::{
12 ChannelCluster, LiminalConnectionServices, build_connection_services,
13};
14use crate::server::listener::ServerListener;
15use crate::server::shutdown::{ShutdownHandle, register_signal_handlers, run_shutdown_sequence};
16
17pub fn run(config_path: &Path) -> Result<(), ServerError> {
23 if config_path.as_os_str().is_empty() {
24 return Err(ServerError::ConfigLoad {
25 message: "configuration path is empty".to_owned(),
26 });
27 }
28
29 let config = load_config(config_path)?;
30
31 crate::metrics::init();
35
36 let readiness = SharedReadinessState::new(ReadinessState::default());
37 let health_server = start_health_server(config.health_listen_address, readiness.clone())?;
38 let shutdown_handle = ShutdownHandle::new();
39 let signal_registration = register_signal_handlers(shutdown_handle.clone())?;
40
41 let auth_token = config
46 .auth
47 .as_ref()
48 .map(|auth| auth.token.clone().into_bytes());
49
50 let (connection_supervisor, cluster_handle) = match config.services.profile()? {
58 ServiceProfile::Full => {
59 let services = Arc::new(LiminalConnectionServices::from_config(&config)?);
60 let channel_cluster = services.channel_cluster().clone();
61 let connection_supervisor = ConnectionSupervisor::with_services_auth_and_limits(
62 services,
63 auth_token,
64 config.limits,
65 )?;
66
67 readiness.set_cluster_configured(config.cluster.is_some());
72 let cluster_handle = match config.cluster.as_ref() {
73 Some(cluster_config) => {
74 Some(start_cluster(&channel_cluster, cluster_config, &readiness)?)
75 }
76 None => None,
77 };
78 (connection_supervisor, cluster_handle)
79 }
80 ServiceProfile::WorkerFrontDoor => {
81 let services = build_connection_services(&config)?;
82 let connection_supervisor = ConnectionSupervisor::with_services_auth_and_limits(
83 services,
84 auth_token,
85 config.limits,
86 )?;
87 readiness.set_cluster_configured(false);
88 (connection_supervisor, None)
89 }
90 };
91
92 let mut listener = ServerListener::bind(&config, connection_supervisor)?;
93 let mut websocket_listener = match config.websocket.as_ref() {
98 Some(websocket_config) => Some(WebSocketListener::bind(
99 websocket_config,
100 listener.supervisor(),
101 )?),
102 None => None,
103 };
104 readiness.set_config_loaded(true);
105 readiness.set_listener_bound(true);
106
107 tracing::debug!(
108 config_path = %config_path.display(),
109 listen_address = %config.listen_address,
110 health_listen_address = %health_server.local_addr(),
111 "liminal server configuration validated"
112 );
113
114 tracing::info!(
115 listen_address = %listener.local_addr(),
116 health_listen_address = %health_server.local_addr(),
117 "liminal server started"
118 );
119
120 shutdown_handle.wait();
121 readiness.set_listener_bound(false);
122
123 if let Some(mut cluster_handle) = cluster_handle {
127 cluster_handle.shutdown();
128 }
129
130 let supervisor = listener.supervisor();
131 let shutdown_result = run_shutdown_sequence(
132 &mut listener,
133 websocket_listener.as_mut(),
134 &supervisor,
135 config.drain_timeout(),
136 );
137 drop(websocket_listener);
138 drop(signal_registration);
139 health_server.shutdown()?;
140 shutdown_result
141}
142
143fn start_cluster(
155 channel_cluster: &ChannelCluster,
156 cluster_config: &ClusterConfig,
157 readiness: &SharedReadinessState,
158) -> Result<ClusterHandle, ServerError> {
159 let resolver = channel_cluster
160 .resolver()
161 .cloned()
162 .ok_or_else(|| ServerError::ClusterJoin {
163 message: "clustering configured but channel supervisor has no distribution resolver"
164 .to_owned(),
165 })?;
166 let scheduler = channel_cluster.supervisor().scheduler();
167 let supervisor = channel_cluster.supervisor().clone();
168 let readiness = readiness.clone();
169 cluster::start(
170 &scheduler,
171 resolver,
172 cluster_config,
173 move |sync| {
174 supervisor.install_observer(Arc::new(sync));
175 },
176 move || readiness.set_cluster_membership_established(true),
177 )
178}
179
180#[cfg(test)]
181mod tests {
182 use std::net::SocketAddr;
183
184 use super::{ChannelCluster, ClusterConfig, SharedReadinessState, start_cluster};
185 use crate::ServerError;
186 use crate::health::{ClusterReadiness, ReadinessCondition, ReadinessState, readiness_check};
187 use crate::server::connection::services::LiminalConnectionServices;
188
189 fn unclustered_channel_cluster() -> Result<ChannelCluster, ServerError> {
193 Ok(LiminalConnectionServices::empty()?
194 .channel_cluster()
195 .clone())
196 }
197
198 fn clustered_but_unmet_readiness() -> SharedReadinessState {
199 SharedReadinessState::new(ReadinessState::new(
200 true,
201 true,
202 ClusterReadiness::Configured {
203 membership_established: false,
204 },
205 ))
206 }
207
208 fn sample_cluster_config() -> Result<ClusterConfig, Box<dyn std::error::Error>> {
209 let listen_address: SocketAddr = "127.0.0.1:0".parse()?;
210 Ok(ClusterConfig {
211 node_name: "node-under-test@127.0.0.1".to_owned(),
212 listen_address,
213 seed_nodes: Vec::new(),
214 cookie: "runtime-test-cookie".to_owned(),
215 })
216 }
217
218 #[test]
219 fn failed_cluster_start_leaves_membership_unestablished()
220 -> Result<(), Box<dyn std::error::Error>> {
221 let readiness = clustered_but_unmet_readiness();
222 let channel_cluster = unclustered_channel_cluster()?;
223 let config = sample_cluster_config()?;
224
225 let result = start_cluster(&channel_cluster, &config, &readiness);
228 assert!(
229 result.is_err(),
230 "start_cluster must fail without a distribution resolver"
231 );
232
233 let status = readiness_check(&readiness.snapshot());
235 assert!(
236 !status.ready,
237 "readiness must remain not-ready after a failed start"
238 );
239 assert!(
240 status
241 .unmet_conditions
242 .contains(&ReadinessCondition::ClusterMembershipEstablished),
243 "cluster membership gate must stay unmet after a failed start"
244 );
245
246 Ok(())
247 }
248}