Skip to main content

lean_ctx/core/ocla/
runtime.rs

1//! Self-contained lifecycle runtime for the OCLA REST API.
2
3use axum::serve;
4use tokio::{net::TcpListener, task::JoinHandle};
5use tokio_util::sync::CancellationToken;
6use tracing::{debug, warn};
7
8use crate::core::config::OclaConfig;
9
10use super::OclaResult;
11use super::wire_api::ocla_router;
12
13/// Owns the OCLA REST server task and its graceful-shutdown signal.
14pub struct OclaRuntime {
15    rest_handle: JoinHandle<()>,
16    cancel: CancellationToken,
17    rest_port: u16,
18}
19
20impl OclaRuntime {
21    /// Starts the OCLA REST API on an OS-assigned loopback port.
22    pub async fn start(config: &OclaConfig) -> OclaResult<Self> {
23        let _ = config;
24        let listener = TcpListener::bind("127.0.0.1:0").await.map_err(|error| {
25            super::OclaError::InvalidRequest(format!("failed to bind OCLA REST listener: {error}"))
26        })?;
27        let rest_port = listener
28            .local_addr()
29            .map_err(|error| {
30                super::OclaError::InvalidRequest(format!(
31                    "failed to inspect OCLA REST listener: {error}"
32                ))
33            })?
34            .port();
35        let cancel = CancellationToken::new();
36        let shutdown = cancel.clone();
37        let rest_handle = tokio::spawn(async move {
38            if let Err(error) = serve(listener, ocla_router())
39                .with_graceful_shutdown(shutdown.cancelled_owned())
40                .await
41            {
42                warn!(error = %error, "OCLA REST server stopped with an error");
43            }
44        });
45
46        Ok(Self {
47            rest_handle,
48            cancel,
49            rest_port,
50        })
51    }
52
53    /// Requests graceful shutdown and waits for the REST task to finish.
54    pub async fn stop(self) -> OclaResult<()> {
55        self.cancel.cancel();
56        self.rest_handle.await.map_err(|error| {
57            super::OclaError::InvalidRequest(format!("OCLA REST task failed: {error}"))
58        })?;
59        debug!("OCLA REST runtime stopped");
60        Ok(())
61    }
62
63    /// Returns the OS-assigned REST listener port.
64    #[must_use]
65    pub fn rest_port(&self) -> u16 {
66        self.rest_port
67    }
68
69    /// Returns whether the REST server task has not finished.
70    #[must_use]
71    pub fn is_running(&self) -> bool {
72        !self.rest_handle.is_finished()
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    #[tokio::test]
81    async fn start_and_stop_lifecycle() {
82        let runtime = OclaRuntime::start(&OclaConfig::default())
83            .await
84            .expect("runtime starts");
85        assert!(runtime.is_running());
86        runtime.stop().await.expect("runtime stops");
87    }
88
89    #[tokio::test]
90    async fn rest_port_is_nonzero() {
91        let runtime = OclaRuntime::start(&OclaConfig::default())
92            .await
93            .expect("runtime starts");
94        assert_ne!(runtime.rest_port(), 0);
95        runtime.stop().await.expect("runtime stops");
96    }
97
98    #[tokio::test]
99    async fn double_stop_is_idempotent() {
100        let runtime = OclaRuntime::start(&OclaConfig::default())
101            .await
102            .expect("runtime starts");
103        runtime.cancel.cancel();
104        runtime.cancel.cancel();
105        runtime.stop().await.expect("runtime stops");
106    }
107}