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