use std::path::Path;
use std::sync::Arc;
use crate::ServerError;
use crate::cluster::{self, ClusterHandle};
use crate::config::file::load_config;
use crate::config::types::{ClusterConfig, ServiceProfile};
use crate::health::{ReadinessState, SharedReadinessState, start_health_server};
use crate::server::connection::ConnectionSupervisor;
use crate::server::connection::services::{
ChannelCluster, LiminalConnectionServices, build_connection_services,
};
use crate::server::listener::ServerListener;
use crate::server::shutdown::{ShutdownHandle, register_signal_handlers, run_shutdown_sequence};
pub fn run(config_path: &Path) -> Result<(), ServerError> {
if config_path.as_os_str().is_empty() {
return Err(ServerError::ConfigLoad {
message: "configuration path is empty".to_owned(),
});
}
let config = load_config(config_path)?;
crate::metrics::init();
let readiness = SharedReadinessState::new(ReadinessState::default());
let health_server = start_health_server(config.health_listen_address, readiness.clone())?;
let shutdown_handle = ShutdownHandle::new();
let signal_registration = register_signal_handlers(shutdown_handle.clone())?;
let auth_token = config
.auth
.as_ref()
.map(|auth| auth.token.clone().into_bytes());
let (connection_supervisor, cluster_handle) = match config.services.profile()? {
ServiceProfile::Full => {
let services = Arc::new(LiminalConnectionServices::from_config(&config)?);
let channel_cluster = services.channel_cluster().clone();
let connection_supervisor =
ConnectionSupervisor::with_services_and_auth(services, auth_token)?;
readiness.set_cluster_configured(config.cluster.is_some());
let cluster_handle = match config.cluster.as_ref() {
Some(cluster_config) => {
Some(start_cluster(&channel_cluster, cluster_config, &readiness)?)
}
None => None,
};
(connection_supervisor, cluster_handle)
}
ServiceProfile::WorkerFrontDoor => {
let services = build_connection_services(&config)?;
let connection_supervisor =
ConnectionSupervisor::with_services_and_auth(services, auth_token)?;
readiness.set_cluster_configured(false);
(connection_supervisor, None)
}
};
let mut listener = ServerListener::bind(&config, connection_supervisor)?;
readiness.set_config_loaded(true);
readiness.set_listener_bound(true);
tracing::debug!(
config_path = %config_path.display(),
listen_address = %config.listen_address,
health_listen_address = %health_server.local_addr(),
"liminal server configuration validated"
);
tracing::info!(
listen_address = %listener.local_addr(),
health_listen_address = %health_server.local_addr(),
"liminal server started"
);
shutdown_handle.wait();
readiness.set_listener_bound(false);
if let Some(mut cluster_handle) = cluster_handle {
cluster_handle.shutdown();
}
let supervisor = listener.supervisor();
let shutdown_result = run_shutdown_sequence(&mut listener, &supervisor, config.drain_timeout());
drop(signal_registration);
health_server.shutdown()?;
shutdown_result
}
fn start_cluster(
channel_cluster: &ChannelCluster,
cluster_config: &ClusterConfig,
readiness: &SharedReadinessState,
) -> Result<ClusterHandle, ServerError> {
let resolver = channel_cluster
.resolver()
.cloned()
.ok_or_else(|| ServerError::ClusterJoin {
message: "clustering configured but channel supervisor has no distribution resolver"
.to_owned(),
})?;
let scheduler = channel_cluster.supervisor().scheduler();
let supervisor = channel_cluster.supervisor().clone();
let readiness = readiness.clone();
cluster::start(
&scheduler,
resolver,
cluster_config,
move |sync| {
supervisor.install_observer(Arc::new(sync));
},
move || readiness.set_cluster_membership_established(true),
)
}
#[cfg(test)]
mod tests {
use std::net::SocketAddr;
use super::{ChannelCluster, ClusterConfig, SharedReadinessState, start_cluster};
use crate::ServerError;
use crate::health::{ClusterReadiness, ReadinessCondition, ReadinessState, readiness_check};
use crate::server::connection::services::LiminalConnectionServices;
fn unclustered_channel_cluster() -> Result<ChannelCluster, ServerError> {
Ok(LiminalConnectionServices::empty()?
.channel_cluster()
.clone())
}
fn clustered_but_unmet_readiness() -> SharedReadinessState {
SharedReadinessState::new(ReadinessState::new(
true,
true,
ClusterReadiness::Configured {
membership_established: false,
},
))
}
fn sample_cluster_config() -> Result<ClusterConfig, Box<dyn std::error::Error>> {
let listen_address: SocketAddr = "127.0.0.1:0".parse()?;
Ok(ClusterConfig {
node_name: "node-under-test@127.0.0.1".to_owned(),
listen_address,
seed_nodes: Vec::new(),
cookie: "runtime-test-cookie".to_owned(),
})
}
#[test]
fn failed_cluster_start_leaves_membership_unestablished()
-> Result<(), Box<dyn std::error::Error>> {
let readiness = clustered_but_unmet_readiness();
let channel_cluster = unclustered_channel_cluster()?;
let config = sample_cluster_config()?;
let result = start_cluster(&channel_cluster, &config, &readiness);
assert!(
result.is_err(),
"start_cluster must fail without a distribution resolver"
);
let status = readiness_check(&readiness.snapshot());
assert!(
!status.ready,
"readiness must remain not-ready after a failed start"
);
assert!(
status
.unmet_conditions
.contains(&ReadinessCondition::ClusterMembershipEstablished),
"cluster membership gate must stay unmet after a failed start"
);
Ok(())
}
}