1mod 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;
63static HTTP_BLOCKING_SLOTS: LazyLock<Arc<Semaphore>> =
65 LazyLock::new(|| Arc::new(Semaphore::new(HTTP_MAX_BLOCKING_TASKS)));
66
67pub struct RuntimeHttpHost {
69 config: HttpApiConfig,
70 router: Router,
71}
72
73impl RuntimeHttpHost {
74 pub fn new(config: HttpApiConfig, static_info: RuntimeStaticInfo) -> Self {
76 Self::with_runtime_state(config, static_info, RuntimeHttpStateParts::default())
77 }
78
79 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 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 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 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 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 pub fn config(&self) -> &HttpApiConfig {
223 &self.config
224 }
225
226 pub fn router(&self) -> Router {
228 self.router.clone()
229 }
230
231 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)]
271pub struct RuntimeHttpStateParts {
273 pub controller: Option<Arc<Mutex<RuntimeController>>>,
275 pub sync_log: Option<Arc<dyn SyncLogView>>,
277 pub tick_counter: Option<Arc<AtomicU64>>,
279 pub app_query_router: Option<Arc<Mutex<ApiRouter>>>,
281 pub operation_mode: Option<Arc<Mutex<RuntimeOperationalMode>>>,
283 pub command_policy: Option<Arc<dyn CommandCapabilityPolicy>>,
285 pub supervisor: Option<appcore_supervisor::Supervisor>,
287 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;