Skip to main content

appcore_api/
http.rs

1// =============================================================================
2//        #######
3//     ###       ###     F: http.rs
4//    ##   ## ##   ##    P: AppCore-Runtime
5//         ## ##
6//                       C: 2026/05/31 13:38:42 by dnettoRaw
7//    ##   ## ##   ##    U: 2026/08/02 13:24:05 by dnettoRaw
8//      ###########      S: 1.0.1-rc.8
9// =============================================================================
10
11//! Minimal HTTP host for runtime health, status, and command ingress.
12
13mod auth;
14mod command;
15mod connection;
16mod handlers;
17mod ingress;
18mod query;
19mod reload;
20mod reload_generation;
21mod response;
22mod state;
23mod trace;
24
25pub use auth::{
26    CommandTokenVerifier, HttpCommandAuth, RequestPayloadRef, RequestValidationDetails,
27    RequestValidationDetailsRef,
28};
29pub use reload::{
30    HttpReloadPhase, HttpReloadPolicy, HttpReloadSnapshot, PreparedRuntimeHttpGeneration,
31    ReloadableRuntimeHttpHost, RuntimeHttpReloadError,
32};
33pub use reload_generation::{HttpRoutingGenerationSnapshot, HttpRoutingGenerationsSnapshot};
34pub use state::{
35    CommandCapabilityPolicy, CommandCapabilityPolicyError, HttpApiConfig, RuntimeStaticInfo,
36    SyncLogView, SyncLogViewError,
37};
38
39#[cfg(test)]
40use crate::command_contract::{CommandRequest, CommandResponse, CommandResponseEvent};
41use crate::ApiRouter;
42use appcore_core::{RuntimeController, RuntimeOperationalMode};
43use axum::extract::DefaultBodyLimit;
44use axum::http::StatusCode;
45use axum::routing::{get, post};
46use axum::Router;
47use command::command_handler;
48use handlers::{
49    diagnostics_handler, health_handler, private_status_handler, public_status_handler,
50    status_handler,
51};
52use parking_lot::Mutex;
53use query::query_handler;
54use state::HttpState;
55use std::io;
56use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
57use std::sync::{Arc, LazyLock};
58use std::time::Duration;
59use tokio::sync::{OwnedSemaphorePermit, Semaphore};
60
61const HTTP_MAX_BLOCKING_TASKS: usize = 16;
62const HTTP_THREAD_STACK_BYTES: usize = 1024 * 1024;
63// appcore-norm: allow(global-state) reason: process-wide gate bounds all embedded HTTP blocking dispatch
64static HTTP_BLOCKING_SLOTS: LazyLock<Arc<Semaphore>> =
65    LazyLock::new(|| Arc::new(Semaphore::new(HTTP_MAX_BLOCKING_TASKS)));
66
67/// Embedded HTTP host for stable Runtime health, status, command, and query routes.
68pub struct RuntimeHttpHost {
69    config: HttpApiConfig,
70    router: Router,
71}
72
73impl RuntimeHttpHost {
74    /// Creates a host exposing only static operational information.
75    pub fn new(config: HttpApiConfig, static_info: RuntimeStaticInfo) -> Self {
76        Self::with_runtime_state(config, static_info, RuntimeHttpStateParts::default())
77    }
78
79    /// Creates a host connected to a Runtime controller.
80    pub fn with_controller(
81        config: HttpApiConfig,
82        static_info: RuntimeStaticInfo,
83        controller: Arc<Mutex<RuntimeController>>,
84    ) -> Self {
85        Self::with_runtime_state(
86            config,
87            static_info,
88            RuntimeHttpStateParts {
89                controller: Some(controller),
90                ..RuntimeHttpStateParts::default()
91            },
92        )
93    }
94
95    /// Creates a host with controller, sync status, tick counter, and token policy.
96    pub fn with_runtime_state_and_auth(
97        config: HttpApiConfig,
98        static_info: RuntimeStaticInfo,
99        controller: Arc<Mutex<RuntimeController>>,
100        sync_log: Option<Arc<dyn SyncLogView>>,
101        tick_counter: Option<Arc<AtomicU64>>,
102        auth: HttpCommandAuth,
103    ) -> Self {
104        Self::with_runtime_state(
105            config,
106            static_info,
107            RuntimeHttpStateParts {
108                controller: Some(controller),
109                sync_log,
110                tick_counter,
111                auth,
112                ..RuntimeHttpStateParts::default()
113            },
114        )
115    }
116
117    /// Creates an authenticated host with a live operational-mode source.
118    pub fn with_runtime_state_auth_and_operation_mode(
119        config: HttpApiConfig,
120        static_info: RuntimeStaticInfo,
121        controller: Arc<Mutex<RuntimeController>>,
122        sync_log: Option<Arc<dyn SyncLogView>>,
123        tick_counter: Option<Arc<AtomicU64>>,
124        auth: HttpCommandAuth,
125        operation_mode: Arc<Mutex<appcore_core::RuntimeOperationalMode>>,
126    ) -> Self {
127        Self::with_runtime_state(
128            config,
129            static_info,
130            RuntimeHttpStateParts {
131                controller: Some(controller),
132                sync_log,
133                tick_counter,
134                operation_mode: Some(operation_mode),
135                auth,
136                ..RuntimeHttpStateParts::default()
137            },
138        )
139    }
140
141    /// Creates an authenticated host with application query routing.
142    pub fn with_runtime_state_auth_and_app_queries(
143        config: HttpApiConfig,
144        static_info: RuntimeStaticInfo,
145        controller: Arc<Mutex<RuntimeController>>,
146        sync_log: Option<Arc<dyn SyncLogView>>,
147        tick_counter: Option<Arc<AtomicU64>>,
148        auth: HttpCommandAuth,
149        app_query_router: Arc<Mutex<ApiRouter>>,
150    ) -> Self {
151        Self::with_runtime_state(
152            config,
153            static_info,
154            RuntimeHttpStateParts {
155                controller: Some(controller),
156                sync_log,
157                tick_counter,
158                app_query_router: Some(app_query_router),
159                auth,
160                ..RuntimeHttpStateParts::default()
161            },
162        )
163    }
164
165    /// Creates a host from the complete set of optional shared state parts.
166    pub fn with_state_parts(
167        config: HttpApiConfig,
168        static_info: RuntimeStaticInfo,
169        parts: RuntimeHttpStateParts,
170    ) -> Self {
171        Self::with_runtime_state(config, static_info, parts)
172    }
173
174    fn with_runtime_state(
175        config: HttpApiConfig,
176        static_info: RuntimeStaticInfo,
177        parts: RuntimeHttpStateParts,
178    ) -> Self {
179        if let Some(router) = &parts.app_query_router {
180            router.lock().freeze_queries();
181        }
182        let state = HttpState {
183            static_info: Arc::new(static_info),
184            controller: parts.controller,
185            app_query_router: parts.app_query_router,
186            sync_log: parts.sync_log,
187            tick_counter: parts.tick_counter,
188            operation_mode: parts.operation_mode,
189            command_policy: parts.command_policy,
190            supervisor: parts.supervisor,
191            auth: parts.auth,
192            max_payload_bytes: config.max_payload_bytes,
193            clock: Arc::new(appcore_core::SystemClock::new()),
194        };
195        let ingress_routes = Router::new()
196            .route("/v1/command", post(command_handler))
197            .route("/v1/query", post(query_handler))
198            .route_layer(axum::middleware::from_fn_with_state(
199                ingress::Ingress::new(config.max_payload_bytes),
200                ingress::admit,
201            ));
202        let router = Router::new()
203            .merge(ingress_routes)
204            .route("/v1/health", get(health_handler))
205            .route("/v1/status", get(status_handler))
206            .route("/v1/status/public", get(public_status_handler))
207            .route("/v1/status/private", get(private_status_handler))
208            .route("/v1/diagnostics", get(diagnostics_handler))
209            .route("/health", get(update_required_handler))
210            .route("/status", get(update_required_handler))
211            .route("/status/public", get(update_required_handler))
212            .route("/status/private", get(update_required_handler))
213            .route("/diagnostics", get(update_required_handler))
214            .route("/command", post(update_required_handler))
215            .route("/query", post(update_required_handler))
216            .layer(DefaultBodyLimit::max(config.max_payload_bytes))
217            .with_state(state);
218        Self { config, router }
219    }
220
221    /// Returns immutable listener and payload-limit configuration.
222    pub fn config(&self) -> &HttpApiConfig {
223        &self.config
224    }
225
226    /// Returns an Axum router suitable for embedding in another listener.
227    pub fn router(&self) -> Router {
228        self.router.clone()
229    }
230
231    /// Runs the configured listener until `shutdown` becomes true.
232    pub fn run_until_shutdown(&self, shutdown: Arc<AtomicBool>) -> io::Result<()> {
233        if !self.config.enabled {
234            return Ok(());
235        }
236        let address = format!("{}:{}", self.config.host, self.config.port);
237        let router = self.router();
238        let runtime = build_http_runtime()?;
239        runtime.block_on(async move {
240            let listener = tokio::net::TcpListener::bind(address).await?;
241            axum::serve(connection::with_read_timeout(listener), router)
242                .with_graceful_shutdown(wait_for_shutdown(shutdown))
243                .await
244        })
245    }
246}
247
248fn build_http_runtime() -> io::Result<tokio::runtime::Runtime> {
249    tokio::runtime::Builder::new_current_thread()
250        .max_blocking_threads(HTTP_MAX_BLOCKING_TASKS)
251        .thread_stack_size(HTTP_THREAD_STACK_BYTES)
252        .thread_name("appcore-api-blocking")
253        .thread_keep_alive(Duration::from_secs(5))
254        .enable_all()
255        .build()
256        .map_err(io::Error::other)
257}
258
259fn try_acquire_blocking_slot() -> Option<OwnedSemaphorePermit> {
260    Arc::clone(&HTTP_BLOCKING_SLOTS).try_acquire_owned().ok()
261}
262
263async fn update_required_handler() -> (StatusCode, &'static str) {
264    (
265        StatusCode::UPGRADE_REQUIRED,
266        "NO MORE SUPPORTED PLEASE UPDATE",
267    )
268}
269
270#[derive(Default)]
271/// Optional shared state used to compose a [`RuntimeHttpHost`].
272pub struct RuntimeHttpStateParts {
273    /// Runtime controller used by command and operational query routes.
274    pub controller: Option<Arc<Mutex<RuntimeController>>>,
275    /// Read-only synchronization-log view used by status routes.
276    pub sync_log: Option<Arc<dyn SyncLogView>>,
277    /// Runtime tick counter exposed through diagnostics.
278    pub tick_counter: Option<Arc<AtomicU64>>,
279    /// Application-owned query router.
280    pub app_query_router: Option<Arc<Mutex<ApiRouter>>>,
281    /// Live operational mode used to gate writes.
282    pub operation_mode: Option<Arc<Mutex<RuntimeOperationalMode>>>,
283    /// Capability and leadership authorization policy for commands.
284    pub command_policy: Option<Arc<dyn CommandCapabilityPolicy>>,
285    /// Managed-service supervisor exposed through private diagnostics.
286    pub supervisor: Option<appcore_supervisor::Supervisor>,
287    /// Bearer-token authorization configuration.
288    pub auth: HttpCommandAuth,
289}
290
291async fn wait_for_shutdown(shutdown: Arc<AtomicBool>) {
292    loop {
293        if shutdown.load(Ordering::SeqCst) {
294            break;
295        }
296        tokio::time::sleep(Duration::from_millis(100)).await;
297    }
298}
299
300#[cfg(test)]
301mod http_tests;
302#[cfg(test)]
303mod reload_tests;