Skip to main content

liminal_server/server/
runtime.rs

1use crate::ServerError;
2use crate::auth_pass::PassVerifier;
3use crate::cluster::ClusterHandle;
4use crate::config::file::load_config;
5use crate::config::types::ServiceProfile;
6use crate::health::{ReadinessState, SharedReadinessState, start_health_server};
7use crate::server::connection::services::{LiminalConnectionServices, build_connection_services};
8use crate::server::connection::{BoundWebSocket, WebSocketListener};
9use crate::server::connection::{ConnectionSupervisor, LysInstall, start_context_watch};
10use crate::server::listener::{BoundAddresses, ServerListener};
11use crate::server::self_streams::owner::PublisherRuntime;
12use crate::server::shutdown::{ShutdownHandle, register_signal_handlers};
13use crate::services::production::ServiceBindings;
14
15use std::path::Path;
16use std::sync::Arc;
17
18#[path = "runtime_links.rs"]
19pub(in crate::server) mod runtime_links;
20use runtime_links::ConfiguredLinks;
21#[path = "link_udp_endpoint.rs"]
22pub mod link_udp_endpoint;
23
24#[cfg(feature = "test-support")]
25pub use runtime_links::test_support as link_test_support;
26
27fn configured_authentication(
28    config: &crate::config::types::ServerConfig,
29) -> Result<(Option<Vec<u8>>, Option<PassVerifier>), ServerError> {
30    let auth_token = config
31        .auth
32        .as_ref()
33        .map(|auth| auth.token.clone().into_bytes());
34    let pass_verifier = config
35        .auth
36        .as_ref()
37        .map(PassVerifier::from_auth)
38        .transpose()?
39        .flatten();
40    Ok((auth_token, pass_verifier))
41}
42
43/// Rewrites every participant conversation log under the configured
44/// persistence path into the current row grammar, offline.
45///
46/// The store is opened exclusively, so this refuses while a server has it
47/// open. Every conversation is replayed once, rewritten into its next log
48/// generation, proved to replay to the same state, and only then switched.
49///
50/// # Errors
51///
52/// Returns [`ServerError`] when the configuration names no persistence path
53/// or no `[participant]` section, the store cannot be opened, or a
54/// conversation refuses the migration (named in the error).
55pub fn run_participant_log_migration(
56    config_path: &Path,
57) -> Result<crate::server::participant::ParticipantLogMigration, ServerError> {
58    let config = load_config(config_path)?;
59    let Some(persistence_path) = config.persistence_path.as_deref() else {
60        return Err(ServerError::ConfigValidation {
61            message: "the participant log migration needs persistence_path; an ephemeral store has no logs to migrate".to_owned(),
62        });
63    };
64    let Some(participant) = config.participant.as_ref() else {
65        return Err(ServerError::ConfigValidation {
66            message: "the participant log migration needs the [participant] section".to_owned(),
67        });
68    };
69    let store = crate::server::connection::services::open_persistent_store(persistence_path)?;
70    Ok(crate::server::participant::migrate_participant_logs(
71        &store,
72        participant,
73    )?)
74}
75
76/// Starts the server deployment wrapper for the supplied configuration path.
77///
78/// # Errors
79///
80/// Returns [`ServerError`] when a later server lifecycle phase fails.
81pub fn run(config_path: &Path) -> Result<(), ServerError> {
82    let mut stages = StartStages::new();
83    let config = load_config(config_path)?;
84    stages.mark("config");
85
86    // Enable metrics for this process before the health server accepts scrapes,
87    // so `/metrics` renders the server families. Standalone liminal library users
88    // never call this, so the registry gate stays off for them.
89    crate::metrics::init();
90
91    let readiness = SharedReadinessState::new(ReadinessState::default());
92    let health_server = start_health_server(config.health_listen_address, readiness.clone())?;
93    stages.mark("health");
94    let shutdown_handle = ShutdownHandle::new();
95    let signal_registration = register_signal_handlers(shutdown_handle.clone())?;
96
97    // The configured [auth] token must ride along here: these call sites build
98    // services themselves (full mode reaches the shared channel cluster first;
99    // the worker front door builds no cluster at all) and so cannot use
100    // `from_config`, which is the only other place the token is wired.
101    let (auth_token, pass_verifier) = configured_authentication(&config)?;
102    stages.mark("auth");
103    // SRV-041 R3 and SRV-044 R2: the wire addresses and the WebSocket
104    // address bind before Lys is asked and the store opens; a client
105    // connecting during the build waits in the listen backlog until
106    // accepting starts below, and is never refused for it.
107    let bound = crate::server::boot_steps::timed("listener_bind", || bind_wire(&config))?;
108    // ACCESS-003 R1 FOUND: the production server built its supervisor by a
109    // path that installed no Lys, so `[auth.lys]` never reached a connection,
110    // and it started with no tokio runtime, so a live ask had nothing to run
111    // on. Both are built here, once, and the Lys install takes the same path
112    // for every profile.
113    let (lys_ask, lys, mut machine_issue) = runtime_links::lys(&config)?;
114    stages.mark("lys");
115    let FullProfile {
116        connection_supervisor,
117        cluster_handle,
118        mut links,
119        services: typed_services,
120        context_watch,
121        mut self_stream_publisher,
122        spine,
123    } = start_profile(
124        &config,
125        (auth_token, pass_verifier),
126        (lys, lys_ask.as_ref()),
127        &shutdown_handle,
128        &health_server,
129        &readiness,
130        &mut stages,
131    )?;
132
133    // P0 #56 R4: readiness now reports whether the server can ADMIT, not just
134    // whether it finished starting. It is installed here rather than at
135    // `SharedReadinessState::new` above because the health endpoint binds before
136    // the supervisor exists — liveness has to be answerable while the rest of
137    // the server is still being built — so the authority that owns the answer
138    // is not available until now.
139    // SRV-028: installed before any connection is admitted; its processes
140    // stop when the guard drops.
141    let (services, mut grant_stream) = bootstrap::lys_owners(
142        &config,
143        &connection_supervisor,
144        lys_ask.as_ref(),
145        typed_services.as_ref(),
146        &health_server,
147    )?;
148    let (mut listener, mut websocket_listener) =
149        crate::server::boot_steps::timed("accept_start", || {
150            open_listeners(
151                bound,
152                connection_supervisor,
153                &readiness,
154                &mut stages,
155                spine.as_ref(),
156            )
157        })?;
158
159    // SRV-028 R5: the address and the live restart after the bind.
160    health_server.install_listen_addresses(listener.local_addresses())?;
161    services.listening(listener.local_addr())?;
162    machine_issue.begin()?;
163    links.start(&health_server.link_bridges(), &listener.supervisor())?;
164    stages.mark("services_registry");
165
166    tracing::debug!(
167        config_path = %config_path.display(),
168        listen_address = %config.listen_address,
169        health_listen_address = %health_server.local_addr(),
170        "liminal server configuration validated"
171    );
172
173    listening::report_started(&listener, &health_server, &stages);
174
175    shutdown_handle.wait();
176    readiness.set_listener_bound(false);
177    // SRV-028: before any connection drains, so a worker leaving with its
178    // connection keeps its service live for the next start.
179    services.shutdown_begun();
180
181    // Tear the cluster down before draining connections: stop accepting peer
182    // links and halt the membership poll loop. Each node shuts down independently
183    // (no cluster-wide coordinated shutdown — that boundary belongs to SRV-004).
184    if let Some(mut cluster_handle) = cluster_handle {
185        cluster_handle.shutdown();
186    }
187
188    let supervisor = listener.supervisor();
189    health_server.release_drafts()?;
190    drop(typed_services);
191    let shutdown_result = runtime_links::shutdown(
192        &supervisor,
193        config.drain_timeout(),
194        || crate::server::shutdown::stop_accepting(&mut listener, websocket_listener.as_mut()),
195        (&mut links, || {
196            let grants = crate::auth_membership::grant_stream::finish_configured(&mut grant_stream);
197            let publisher = finish_self_stream_publisher(&mut self_stream_publisher);
198            grants.and(publisher)
199        }),
200    );
201    let participant_fatal = supervisor.participant_service_fatal();
202    let start_failure = supervisor.incarnation_start_failure();
203    let publisher_shutdown = finish_self_stream_publisher(&mut self_stream_publisher);
204    // The watch and the pass issue stop before the Lys ask runtime they use goes.
205    drop(context_watch);
206    let issue_finish = machine_issue.finish();
207    drop(lys_ask);
208    drop(websocket_listener);
209    drop(signal_registration);
210    // The health endpoint is torn down before the shutdown-sequence result is
211    // raised: the teardown has to happen either way.
212    bootstrap::run_answer(
213        start_failure,
214        raise_in_precedence(
215            health_server.shutdown(),
216            raise_in_precedence(
217                publisher_shutdown,
218                raise_in_precedence(issue_finish, shutdown_result),
219            ),
220        ),
221        participant_fatal,
222    )
223}
224
225/// What the full profile starts: the connection supervisor over the
226/// production services, and the cluster, context watch and self-stream
227/// publisher beside it when configured.
228struct FullProfile {
229    services: Option<Arc<LiminalConnectionServices>>,
230    links: runtime_links::ConfiguredLinks,
231    connection_supervisor: ConnectionSupervisor,
232    /// The spine store opened held (DUR-021), released after the listeners bind.
233    spine: Option<Arc<haematite::EventStore>>,
234    cluster_handle: Option<ClusterHandle>,
235    context_watch: Option<crate::services::context_watch::ContextWatch>,
236    self_stream_publisher: Option<PublisherRuntime>,
237}
238
239fn start_profile(
240    config: &crate::config::types::ServerConfig,
241    (auth_token, pass_verifier): (Option<Vec<u8>>, Option<PassVerifier>),
242    (lys, lys_ask): (Option<LysInstall>, Option<&tokio::runtime::Runtime>),
243    shutdown: &ShutdownHandle,
244    health: &crate::health::HealthServerHandle,
245    readiness: &SharedReadinessState,
246    stages: &mut StartStages,
247) -> Result<FullProfile, ServerError> {
248    match config.services.profile()? {
249        ServiceProfile::Full => start_full_profile(
250            config,
251            (auth_token, pass_verifier),
252            (lys, lys_ask),
253            shutdown,
254            health,
255            readiness,
256            stages,
257        ),
258        ServiceProfile::WorkerFrontDoor => {
259            let services = build_connection_services(config)?;
260            let connection_supervisor = ConnectionSupervisor::with_lys_and_fatal_shutdown(
261                services,
262                auth_token,
263                pass_verifier,
264                lys,
265                config.limits,
266                shutdown.clone(),
267            )?;
268            readiness.set_cluster_configured(false);
269            stages.mark("supervisor");
270            Ok(FullProfile {
271                services: None,
272                links: ConfiguredLinks::default(),
273                connection_supervisor,
274                spine: None,
275                cluster_handle: None,
276                context_watch: None,
277                self_stream_publisher: None,
278            })
279        }
280    }
281}
282
283/// Starts the full profile: the production connection services, the health
284/// records they publish, the Lys install with its service bindings, the
285/// connection supervisor, the context watch, the self-stream publisher and
286/// clustering when configured.
287fn start_full_profile(
288    config: &crate::config::types::ServerConfig,
289    (auth_token, pass_verifier): (Option<Vec<u8>>, Option<PassVerifier>),
290    (lys, lys_ask): (Option<LysInstall>, Option<&tokio::runtime::Runtime>),
291    shutdown_handle: &ShutdownHandle,
292    health_server: &crate::health::HealthServerHandle,
293    readiness: &SharedReadinessState,
294    stages: &mut StartStages,
295) -> Result<FullProfile, ServerError> {
296    let services = Arc::new(LiminalConnectionServices::from_config_with_runtime(
297        config,
298        lys_ask.map(|runtime| runtime.handle().clone()),
299    )?);
300    stages.mark("services");
301    // DUR-021 R4: the health endpoint reads the spine's reclaim status on each
302    // request and offers the one operator route that quiesces it.
303    let spine = services.durable_store().reclaim_spine();
304    if let Some(spine) = &spine {
305        health_server.install_spine(spine);
306    }
307    install_ephemeral_sweep_report(&services, health_server)?;
308    // Publish the participant's refused-load record onto the health
309    // endpoint. It happens HERE and not at `start_health_server`
310    // because the endpoint binds before the participant exists —
311    // liveness has to be answerable while the rest of the server is
312    // still being built. Construction restores no conversation
313    // (DUR-020): the record fills as conversations restore, on first
314    // touch or in the warm, and is complete once the warm has finished.
315    // The worker-front-
316    // door profile configures no participant and installs nothing, and
317    // the route reports that rather than an empty refusal set.
318    if let Some(record) = services.unloadable_conversation_record() {
319        health_server.install_unloadable_record(record);
320    }
321    // R18 amendment A7 (§0.18): the operator credential-re-issue
322    // authority, published on the same line of reasoning and at the
323    // same moment. A profile with no participant installs nothing and
324    // the route says so.
325    if let Some(reissuer) = services.credential_reissuer() {
326        health_server.install_credential_reissuer(reissuer);
327    }
328    let channel_cluster = services.channel_cluster().clone();
329    let signals_owner = Arc::clone(&services);
330    let lys = lys
331        .map(|install| configured_service_bindings(install, config, &services, lys_ask))
332        .transpose()?;
333    let bindings = lys.as_ref().and_then(|install| install.services.clone());
334    let links = runtime_links::prepare(config, lys.as_ref(), &signals_owner)?;
335    let connection_supervisor = ConnectionSupervisor::with_lys_and_fatal_shutdown(
336        services,
337        auth_token,
338        pass_verifier,
339        lys,
340        config.limits,
341        shutdown_handle.clone(),
342    )?;
343    stages.mark("supervisor");
344    let mut context_watch = None;
345    if let Some(bindings) = bindings.as_deref() {
346        context_watch = start_context_watch(signals_owner.as_ref(), bindings, &config.limits)?;
347        stages.mark("services");
348    }
349
350    let self_stream_publisher = start_self_stream_publisher(
351        &signals_owner,
352        &connection_supervisor,
353        config,
354        shutdown_handle,
355    )?;
356    stages.mark("incarnation");
357
358    // SRV-005: start clustering on the channel-supervisor scheduler when a
359    // [cluster] section is configured. The returned handle owns the inbound
360    // distribution listener and the membership poll loop; it must outlive the
361    // server and is torn down in the shutdown sequence below.
362    let cluster_handle =
363        start_configured_cluster(&channel_cluster, config.cluster.as_ref(), readiness)?;
364    stages.mark("cluster");
365    Ok(FullProfile {
366        services: Some(signals_owner),
367        links,
368        connection_supervisor,
369        spine,
370        cluster_handle,
371        context_watch,
372        self_stream_publisher,
373    })
374}
375
376/// Binds the wire listener and, when configured, its WebSocket sibling.
377/// Readiness reports the listeners bound once both are.
378/// Builds the ACCESS-003 service bindings `[auth.lys.services]` configures
379/// into the Lys install: made as this machine, on the Lys ask runtime,
380/// publishing through the server's own connection services.
381fn configured_service_bindings(
382    mut install: LysInstall,
383    config: &crate::config::types::ServerConfig,
384    services: &Arc<LiminalConnectionServices>,
385    lys_ask: Option<&tokio::runtime::Runtime>,
386) -> Result<LysInstall, ServerError> {
387    let Some(lys) = config.auth.as_ref().and_then(|auth| auth.lys.as_ref()) else {
388        return Ok(install);
389    };
390    let Some(runtime) = lys_ask else {
391        return Err(ServerError::ConfigValidation {
392            message: "auth.lys.services needs the Lys ask runtime; none was started".to_owned(),
393        });
394    };
395    let now = std::time::SystemTime::now()
396        .duration_since(std::time::UNIX_EPOCH)
397        .map_or(0, |elapsed| elapsed.as_secs());
398    install.services =
399        ServiceBindings::configure(lys, Arc::clone(services), runtime.handle().clone(), now)
400            .map_err(|error| ServerError::ConfigValidation {
401                message: format!("auth.lys.services: {error}"),
402            })?
403            .map(Arc::new);
404    Ok(install)
405}
406
407/// Binds the listeners and, once they are bound, starts the release of the
408/// spine's held reclaim (DUR-021 R1 and R3) on its own thread, with what each
409/// shard resumed on the log: the start does not wait for a shard to finish
410/// its reclaim step to answer.
411fn open_listeners(
412    bound: BoundWire,
413    supervisor: ConnectionSupervisor,
414    readiness: &SharedReadinessState,
415    stages: &mut StartStages,
416    spine: Option<&Arc<haematite::EventStore>>,
417) -> Result<(ServerListener, Option<WebSocketListener>), ServerError> {
418    readiness.track_admission(supervisor.admission_readiness());
419    let listeners = bind_listeners(bound, supervisor, readiness)?;
420    stages.mark("listeners");
421    crate::server::boot_steps::finished("time_to_listening", stages.began);
422    if let Some(spine) = spine {
423        let release =
424            crate::server::connection::reclaim_placement::release_after_bind(spine, stages.began)?;
425        listeners.0.supervisor().retain_reclaim_release(release)?;
426    }
427    Ok(listeners)
428}
429
430/// The wire addresses and, when `[websocket]` is configured, the WebSocket
431/// address: bound and not yet accepting (SRV-044 R2).
432#[derive(Debug)]
433struct BoundWire {
434    addresses: BoundAddresses,
435    websocket: Option<BoundWebSocket>,
436}
437
438/// Binds every address the server listens on, in one step before Lys is
439/// asked and the store opens. LP-WS-TRANSPORT R1.1: the WebSocket acceptor is
440/// explicit opt-in; absent `[websocket]` binds nothing, no HTTP surface at all.
441fn bind_wire(config: &crate::config::types::ServerConfig) -> Result<BoundWire, ServerError> {
442    let addresses = BoundAddresses::bind(config)?;
443    let websocket = config
444        .websocket
445        .as_ref()
446        .map(BoundWebSocket::bind)
447        .transpose()?;
448    Ok(BoundWire {
449        addresses,
450        websocket,
451    })
452}
453
454fn bind_listeners(
455    bound: BoundWire,
456    supervisor: ConnectionSupervisor,
457    readiness: &SharedReadinessState,
458) -> Result<(ServerListener, Option<WebSocketListener>), ServerError> {
459    let listener = ServerListener::accept_on(bound.addresses, supervisor)?;
460    let websocket_listener = bound
461        .websocket
462        .map(|websocket| WebSocketListener::accept_on(websocket, listener.supervisor()))
463        .transpose()?;
464    readiness.set_config_loaded(true);
465    readiness.set_listener_bound(true);
466    Ok((listener, websocket_listener))
467}
468
469#[cfg(test)]
470#[path = "runtime/bind_wire_tests.rs"]
471mod bind_wire_tests;
472
473#[path = "runtime/start_stages.rs"]
474mod start_stages;
475use start_stages::StartStages;
476
477mod bootstrap;
478#[cfg(any(test, feature = "test-support"))]
479pub(in crate::server) use bootstrap::bind_self_streams;
480pub(in crate::server) use bootstrap::start_self_stream_publisher;
481use bootstrap::{
482    finish_self_stream_publisher, install_ephemeral_sweep_report, raise_in_precedence,
483    start_configured_cluster,
484};
485#[cfg(test)]
486#[path = "runtime/tests.rs"]
487mod tests;
488
489mod listening;