mod auth;
mod command;
mod handlers;
mod query;
mod response;
mod state;
mod trace;
pub use auth::{CommandTokenVerifier, HttpCommandAuth, RequestValidationDetails};
pub use state::{
CommandCapabilityPolicy, CommandCapabilityPolicyError, HttpApiConfig, RuntimeStaticInfo,
SyncLogView, SyncLogViewError,
};
#[cfg(test)]
use crate::command_contract::{CommandRequest, CommandResponse, CommandResponseEvent};
use crate::ApiRouter;
use appcore_core::{RuntimeController, RuntimeOperationalMode};
use axum::extract::DefaultBodyLimit;
use axum::http::StatusCode;
use axum::routing::{get, post};
use axum::Router;
use command::command_handler;
use handlers::{
diagnostics_handler, health_handler, private_status_handler, public_status_handler,
status_handler,
};
use parking_lot::Mutex;
use query::query_handler;
use state::HttpState;
use std::io;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Duration;
pub struct RuntimeHttpHost {
config: HttpApiConfig,
router: Router,
}
impl RuntimeHttpHost {
pub fn new(config: HttpApiConfig, static_info: RuntimeStaticInfo) -> Self {
Self::with_runtime_state(config, static_info, RuntimeHttpStateParts::default())
}
pub fn with_controller(
config: HttpApiConfig,
static_info: RuntimeStaticInfo,
controller: Arc<Mutex<RuntimeController>>,
) -> Self {
Self::with_runtime_state(
config,
static_info,
RuntimeHttpStateParts {
controller: Some(controller),
..RuntimeHttpStateParts::default()
},
)
}
pub fn with_runtime_state_and_auth(
config: HttpApiConfig,
static_info: RuntimeStaticInfo,
controller: Arc<Mutex<RuntimeController>>,
sync_log: Option<Arc<dyn SyncLogView>>,
tick_counter: Option<Arc<AtomicU64>>,
auth: HttpCommandAuth,
) -> Self {
Self::with_runtime_state(
config,
static_info,
RuntimeHttpStateParts {
controller: Some(controller),
sync_log,
tick_counter,
auth,
..RuntimeHttpStateParts::default()
},
)
}
pub fn with_runtime_state_auth_and_operation_mode(
config: HttpApiConfig,
static_info: RuntimeStaticInfo,
controller: Arc<Mutex<RuntimeController>>,
sync_log: Option<Arc<dyn SyncLogView>>,
tick_counter: Option<Arc<AtomicU64>>,
auth: HttpCommandAuth,
operation_mode: Arc<Mutex<appcore_core::RuntimeOperationalMode>>,
) -> Self {
Self::with_runtime_state(
config,
static_info,
RuntimeHttpStateParts {
controller: Some(controller),
sync_log,
tick_counter,
operation_mode: Some(operation_mode),
auth,
..RuntimeHttpStateParts::default()
},
)
}
pub fn with_runtime_state_auth_and_app_queries(
config: HttpApiConfig,
static_info: RuntimeStaticInfo,
controller: Arc<Mutex<RuntimeController>>,
sync_log: Option<Arc<dyn SyncLogView>>,
tick_counter: Option<Arc<AtomicU64>>,
auth: HttpCommandAuth,
app_query_router: Arc<Mutex<ApiRouter>>,
) -> Self {
Self::with_runtime_state(
config,
static_info,
RuntimeHttpStateParts {
controller: Some(controller),
sync_log,
tick_counter,
app_query_router: Some(app_query_router),
auth,
..RuntimeHttpStateParts::default()
},
)
}
pub fn with_state_parts(
config: HttpApiConfig,
static_info: RuntimeStaticInfo,
parts: RuntimeHttpStateParts,
) -> Self {
Self::with_runtime_state(config, static_info, parts)
}
fn with_runtime_state(
config: HttpApiConfig,
static_info: RuntimeStaticInfo,
parts: RuntimeHttpStateParts,
) -> Self {
if let Some(router) = &parts.app_query_router {
router.lock().freeze_queries();
}
let state = HttpState {
static_info,
controller: parts.controller,
app_query_router: parts.app_query_router,
sync_log: parts.sync_log,
tick_counter: parts.tick_counter,
operation_mode: parts.operation_mode,
command_policy: parts.command_policy,
supervisor: parts.supervisor,
auth: parts.auth,
max_payload_bytes: config.max_payload_bytes,
clock: Arc::new(appcore_core::SystemClock::new()),
};
let router = Router::new()
.route("/v1/health", get(health_handler))
.route("/v1/status", get(status_handler))
.route("/v1/status/public", get(public_status_handler))
.route("/v1/status/private", get(private_status_handler))
.route("/v1/diagnostics", get(diagnostics_handler))
.route("/v1/command", post(command_handler))
.route("/v1/query", post(query_handler))
.route("/health", get(update_required_handler))
.route("/status", get(update_required_handler))
.route("/status/public", get(update_required_handler))
.route("/status/private", get(update_required_handler))
.route("/diagnostics", get(update_required_handler))
.route("/command", post(update_required_handler))
.route("/query", post(update_required_handler))
.layer(DefaultBodyLimit::max(config.max_payload_bytes))
.with_state(state);
Self { config, router }
}
pub fn config(&self) -> &HttpApiConfig {
&self.config
}
pub fn router(&self) -> Router {
self.router.clone()
}
pub fn run_until_shutdown(&self, shutdown: Arc<AtomicBool>) -> io::Result<()> {
if !self.config.enabled {
return Ok(());
}
let address = format!("{}:{}", self.config.host, self.config.port);
let router = self.router();
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(io::Error::other)?;
runtime.block_on(async move {
let listener = tokio::net::TcpListener::bind(address).await?;
axum::serve(listener, router)
.with_graceful_shutdown(wait_for_shutdown(shutdown))
.await
})
}
}
async fn update_required_handler() -> (StatusCode, &'static str) {
(
StatusCode::UPGRADE_REQUIRED,
"NO MORE SUPPORTED PLEASE UPDATE",
)
}
#[derive(Default)]
pub struct RuntimeHttpStateParts {
pub controller: Option<Arc<Mutex<RuntimeController>>>,
pub sync_log: Option<Arc<dyn SyncLogView>>,
pub tick_counter: Option<Arc<AtomicU64>>,
pub app_query_router: Option<Arc<Mutex<ApiRouter>>>,
pub operation_mode: Option<Arc<Mutex<RuntimeOperationalMode>>>,
pub command_policy: Option<Arc<dyn CommandCapabilityPolicy>>,
pub supervisor: Option<appcore_supervisor::Supervisor>,
pub auth: HttpCommandAuth,
}
async fn wait_for_shutdown(shutdown: Arc<AtomicBool>) {
loop {
if shutdown.load(Ordering::SeqCst) {
break;
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
#[cfg(test)]
mod http_tests;