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