2026-03-21 12:57:09 +08:00
|
|
|
use std::path::Path;
|
2026-03-24 15:12:56 +08:00
|
|
|
use std::sync::Arc;
|
2026-03-21 12:57:09 +08:00
|
|
|
|
2026-04-03 14:59:58 +08:00
|
|
|
use aether_contracts::ExecutionPlan;
|
2026-03-24 15:12:56 +08:00
|
|
|
use aether_runtime::{
|
|
|
|
|
maybe_hold_axum_response_permit, prometheus_response, service_up_sample, AdmissionPermit,
|
2026-05-08 00:18:12 +08:00
|
|
|
ConcurrencyError, ConcurrencyGate, ConcurrencySnapshot, MetricKind, MetricLabel, MetricSample,
|
2026-03-21 12:57:09 +08:00
|
|
|
};
|
2026-05-08 00:18:12 +08:00
|
|
|
use aether_runtime_state::{RuntimeSemaphore, RuntimeSemaphoreError, RuntimeSemaphoreSnapshot};
|
2026-03-24 15:12:56 +08:00
|
|
|
use axum::body::{to_bytes, Body};
|
|
|
|
|
use axum::extract::{Request, State};
|
2026-03-21 12:57:09 +08:00
|
|
|
use axum::http::StatusCode;
|
|
|
|
|
use axum::response::{IntoResponse, Response};
|
|
|
|
|
use axum::routing::{get, post};
|
|
|
|
|
use axum::{Json, Router};
|
|
|
|
|
use serde_json::json;
|
2026-04-03 14:59:58 +08:00
|
|
|
use thiserror::Error;
|
2026-03-21 12:57:09 +08:00
|
|
|
|
2026-04-05 20:23:16 +08:00
|
|
|
use crate::execution_runtime::{
|
2026-04-03 14:59:58 +08:00
|
|
|
build_direct_execution_frame_stream, DirectSyncExecutionRuntime, ExecutionRuntimeTransportError,
|
|
|
|
|
};
|
2026-04-22 21:10:16 +08:00
|
|
|
use crate::middleware;
|
2026-04-03 14:59:58 +08:00
|
|
|
|
|
|
|
|
const EXECUTION_RUNTIME_COMPONENT: &str = "aether-gateway-execution-runtime";
|
|
|
|
|
const REQUEST_GATE_NAME: &str = "execution_runtime_requests";
|
|
|
|
|
const DISTRIBUTED_REQUEST_GATE_NAME: &str = "execution_runtime_requests_distributed";
|
2026-03-21 12:57:09 +08:00
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Default)]
|
2026-04-03 14:59:58 +08:00
|
|
|
struct ExecutionRuntimeAppState {
|
|
|
|
|
execution_runtime: DirectSyncExecutionRuntime,
|
2026-03-24 15:12:56 +08:00
|
|
|
request_gate: Option<Arc<ConcurrencyGate>>,
|
2026-05-08 00:18:12 +08:00
|
|
|
distributed_request_gate: Option<Arc<RuntimeSemaphore>>,
|
2026-03-24 15:12:56 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-03 14:59:58 +08:00
|
|
|
impl ExecutionRuntimeAppState {
|
2026-03-24 15:12:56 +08:00
|
|
|
fn with_request_concurrency_limit(limit: Option<usize>) -> Self {
|
|
|
|
|
Self {
|
2026-04-03 14:59:58 +08:00
|
|
|
execution_runtime: DirectSyncExecutionRuntime::new(),
|
2026-03-24 15:12:56 +08:00
|
|
|
request_gate: limit
|
|
|
|
|
.filter(|limit| *limit > 0)
|
2026-04-03 14:59:58 +08:00
|
|
|
.map(|limit| Arc::new(ConcurrencyGate::new(REQUEST_GATE_NAME, limit))),
|
2026-03-24 15:12:56 +08:00
|
|
|
distributed_request_gate: None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-08 00:18:12 +08:00
|
|
|
fn with_distributed_request_gate(mut self, gate: RuntimeSemaphore) -> Self {
|
2026-03-24 15:12:56 +08:00
|
|
|
self.distributed_request_gate = Some(Arc::new(gate));
|
|
|
|
|
self
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn request_concurrency_snapshot(&self) -> Option<ConcurrencySnapshot> {
|
|
|
|
|
self.request_gate.as_ref().map(|gate| gate.snapshot())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn distributed_request_concurrency_snapshot(
|
|
|
|
|
&self,
|
2026-05-08 00:18:12 +08:00
|
|
|
) -> Result<Option<RuntimeSemaphoreSnapshot>, RuntimeSemaphoreError> {
|
2026-03-24 15:12:56 +08:00
|
|
|
match self.distributed_request_gate.as_ref() {
|
|
|
|
|
Some(gate) => gate.snapshot().await.map(Some),
|
|
|
|
|
None => Ok(None),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn metric_samples(&self) -> Vec<MetricSample> {
|
2026-04-03 14:59:58 +08:00
|
|
|
let mut samples = vec![service_up_sample(EXECUTION_RUNTIME_COMPONENT)];
|
2026-03-24 15:12:56 +08:00
|
|
|
if let Some(snapshot) = self.request_concurrency_snapshot() {
|
2026-04-03 14:59:58 +08:00
|
|
|
samples.extend(snapshot.to_metric_samples(REQUEST_GATE_NAME));
|
2026-03-24 15:12:56 +08:00
|
|
|
}
|
|
|
|
|
if let Some(gate) = self.distributed_request_gate.as_ref() {
|
|
|
|
|
match gate.snapshot().await {
|
|
|
|
|
Ok(snapshot) => {
|
2026-04-03 14:59:58 +08:00
|
|
|
samples.extend(snapshot.to_metric_samples(DISTRIBUTED_REQUEST_GATE_NAME));
|
2026-03-24 15:12:56 +08:00
|
|
|
}
|
|
|
|
|
Err(_) => samples.push(
|
|
|
|
|
MetricSample::new(
|
|
|
|
|
"concurrency_unavailable",
|
|
|
|
|
"Whether the distributed concurrency gate is currently unavailable.",
|
|
|
|
|
MetricKind::Gauge,
|
|
|
|
|
1,
|
|
|
|
|
)
|
|
|
|
|
.with_labels(vec![MetricLabel::new(
|
|
|
|
|
"gate",
|
2026-04-03 14:59:58 +08:00
|
|
|
DISTRIBUTED_REQUEST_GATE_NAME,
|
2026-03-24 15:12:56 +08:00
|
|
|
)]),
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
samples
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn try_acquire_request_permit(
|
|
|
|
|
&self,
|
|
|
|
|
) -> Result<Option<AdmissionPermit>, RequestAdmissionError> {
|
|
|
|
|
let local = self
|
|
|
|
|
.request_gate
|
|
|
|
|
.as_ref()
|
|
|
|
|
.map(|gate| gate.try_acquire())
|
|
|
|
|
.transpose()
|
|
|
|
|
.map_err(RequestAdmissionError::Local)?;
|
|
|
|
|
let distributed = match self.distributed_request_gate.as_ref() {
|
|
|
|
|
Some(gate) => Some(
|
|
|
|
|
gate.try_acquire()
|
|
|
|
|
.await
|
|
|
|
|
.map_err(RequestAdmissionError::Distributed)?,
|
|
|
|
|
),
|
|
|
|
|
None => None,
|
|
|
|
|
};
|
|
|
|
|
Ok(AdmissionPermit::from_parts(local, distributed))
|
|
|
|
|
}
|
2026-03-21 12:57:09 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-03 14:59:58 +08:00
|
|
|
pub fn build_execution_runtime_router() -> Router {
|
|
|
|
|
build_execution_runtime_router_with_request_concurrency_limit(None)
|
2026-03-21 12:57:09 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-03 14:59:58 +08:00
|
|
|
pub fn build_execution_runtime_router_with_request_concurrency_limit(
|
|
|
|
|
limit: Option<usize>,
|
|
|
|
|
) -> Router {
|
|
|
|
|
build_execution_runtime_router_with_request_gates(limit, None)
|
2026-03-24 15:12:56 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-03 14:59:58 +08:00
|
|
|
pub fn build_execution_runtime_router_with_request_gates(
|
2026-03-24 15:12:56 +08:00
|
|
|
limit: Option<usize>,
|
2026-05-08 00:18:12 +08:00
|
|
|
distributed_gate: Option<RuntimeSemaphore>,
|
2026-03-24 15:12:56 +08:00
|
|
|
) -> Router {
|
|
|
|
|
let state = match distributed_gate {
|
2026-04-03 14:59:58 +08:00
|
|
|
Some(gate) => ExecutionRuntimeAppState::with_request_concurrency_limit(limit)
|
|
|
|
|
.with_distributed_request_gate(gate),
|
|
|
|
|
None => ExecutionRuntimeAppState::with_request_concurrency_limit(limit),
|
2026-03-24 15:12:56 +08:00
|
|
|
};
|
2026-04-22 21:10:16 +08:00
|
|
|
middleware::apply_cf_header_stripping(
|
|
|
|
|
Router::new()
|
|
|
|
|
.route("/health", get(health))
|
|
|
|
|
.route("/metrics", get(metrics))
|
|
|
|
|
.route("/v1/execute/sync", post(execute_sync))
|
|
|
|
|
.route("/v1/execute/stream", post(execute_stream))
|
|
|
|
|
.with_state(state),
|
|
|
|
|
)
|
2026-03-24 15:12:56 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-03 14:59:58 +08:00
|
|
|
pub async fn serve_execution_runtime_tcp(
|
2026-03-24 15:12:56 +08:00
|
|
|
bind: &str,
|
|
|
|
|
max_in_flight_requests: Option<usize>,
|
2026-05-08 00:18:12 +08:00
|
|
|
distributed_request_gate: Option<RuntimeSemaphore>,
|
2026-03-24 15:12:56 +08:00
|
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
2026-03-21 12:57:09 +08:00
|
|
|
let listener = tokio::net::TcpListener::bind(bind).await?;
|
2026-03-24 15:12:56 +08:00
|
|
|
axum::serve(
|
|
|
|
|
listener,
|
2026-04-03 14:59:58 +08:00
|
|
|
build_execution_runtime_router_with_request_gates(
|
|
|
|
|
max_in_flight_requests,
|
|
|
|
|
distributed_request_gate,
|
|
|
|
|
),
|
2026-03-24 15:12:56 +08:00
|
|
|
)
|
|
|
|
|
.await?;
|
2026-03-21 12:57:09 +08:00
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-14 14:39:22 +08:00
|
|
|
#[cfg(unix)]
|
2026-04-03 14:59:58 +08:00
|
|
|
pub async fn serve_execution_runtime_unix(
|
2026-03-24 15:12:56 +08:00
|
|
|
socket_path: &Path,
|
|
|
|
|
max_in_flight_requests: Option<usize>,
|
2026-05-08 00:18:12 +08:00
|
|
|
distributed_request_gate: Option<RuntimeSemaphore>,
|
2026-03-24 15:12:56 +08:00
|
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
2026-03-21 12:57:09 +08:00
|
|
|
if let Some(parent) = socket_path.parent() {
|
|
|
|
|
std::fs::create_dir_all(parent)?;
|
|
|
|
|
}
|
|
|
|
|
if socket_path.exists() {
|
|
|
|
|
std::fs::remove_file(socket_path)?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let listener = tokio::net::UnixListener::bind(socket_path)?;
|
2026-03-24 15:12:56 +08:00
|
|
|
axum::serve(
|
|
|
|
|
listener,
|
2026-04-03 14:59:58 +08:00
|
|
|
build_execution_runtime_router_with_request_gates(
|
|
|
|
|
max_in_flight_requests,
|
|
|
|
|
distributed_request_gate,
|
|
|
|
|
),
|
2026-03-24 15:12:56 +08:00
|
|
|
)
|
|
|
|
|
.await?;
|
2026-03-21 12:57:09 +08:00
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-14 14:39:22 +08:00
|
|
|
#[cfg(not(unix))]
|
|
|
|
|
pub async fn serve_execution_runtime_unix(
|
|
|
|
|
_socket_path: &Path,
|
|
|
|
|
_max_in_flight_requests: Option<usize>,
|
|
|
|
|
_distributed_request_gate: Option<RuntimeSemaphore>,
|
|
|
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
|
|
|
Err(std::io::Error::new(
|
|
|
|
|
std::io::ErrorKind::Unsupported,
|
|
|
|
|
"Unix sockets are not supported on this platform",
|
|
|
|
|
)
|
|
|
|
|
.into())
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-03 14:59:58 +08:00
|
|
|
async fn health(State(state): State<ExecutionRuntimeAppState>) -> impl IntoResponse {
|
2026-03-24 15:12:56 +08:00
|
|
|
let request_concurrency = state.request_concurrency_snapshot().map(|snapshot| {
|
|
|
|
|
json!({
|
|
|
|
|
"limit": snapshot.limit,
|
|
|
|
|
"in_flight": snapshot.in_flight,
|
|
|
|
|
"available_permits": snapshot.available_permits,
|
|
|
|
|
"high_watermark": snapshot.high_watermark,
|
|
|
|
|
"rejected": snapshot.rejected,
|
|
|
|
|
})
|
|
|
|
|
});
|
|
|
|
|
let distributed_request_concurrency = state
|
|
|
|
|
.distributed_request_concurrency_snapshot()
|
|
|
|
|
.await
|
|
|
|
|
.ok()
|
|
|
|
|
.flatten()
|
|
|
|
|
.map(|snapshot| {
|
|
|
|
|
json!({
|
|
|
|
|
"limit": snapshot.limit,
|
|
|
|
|
"in_flight": snapshot.in_flight,
|
|
|
|
|
"available_permits": snapshot.available_permits,
|
|
|
|
|
"high_watermark": snapshot.high_watermark,
|
|
|
|
|
"rejected": snapshot.rejected,
|
|
|
|
|
})
|
|
|
|
|
});
|
|
|
|
|
Json(json!({
|
|
|
|
|
"status": "ok",
|
2026-04-03 14:59:58 +08:00
|
|
|
"component": EXECUTION_RUNTIME_COMPONENT,
|
2026-03-24 15:12:56 +08:00
|
|
|
"request_concurrency": request_concurrency,
|
|
|
|
|
"distributed_request_concurrency": distributed_request_concurrency,
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-03 14:59:58 +08:00
|
|
|
async fn metrics(State(state): State<ExecutionRuntimeAppState>) -> Response {
|
2026-03-24 15:12:56 +08:00
|
|
|
prometheus_response(&state.metric_samples().await)
|
2026-03-21 12:57:09 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn execute_sync(
|
2026-04-03 14:59:58 +08:00
|
|
|
State(state): State<ExecutionRuntimeAppState>,
|
2026-03-24 15:12:56 +08:00
|
|
|
request: Request,
|
2026-04-03 14:59:58 +08:00
|
|
|
) -> Result<Response, ExecutionRuntimeAppError> {
|
2026-03-24 15:12:56 +08:00
|
|
|
let request_permit = acquire_request_permit(&state).await?;
|
|
|
|
|
let plan = parse_request_json::<ExecutionPlan>(request).await?;
|
2026-04-03 14:59:58 +08:00
|
|
|
let result = state
|
|
|
|
|
.execution_runtime
|
2026-04-21 16:19:07 +08:00
|
|
|
.execute_sync(&plan)
|
2026-04-03 14:59:58 +08:00
|
|
|
.await
|
|
|
|
|
.map_err(|err| ExecutionRuntimeAppError(ExecutionRuntimeServerError::Transport(err)))?;
|
2026-03-24 15:12:56 +08:00
|
|
|
Ok(maybe_hold_axum_response_permit(
|
|
|
|
|
Json(result).into_response(),
|
|
|
|
|
request_permit,
|
|
|
|
|
))
|
2026-03-21 12:57:09 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn execute_stream(
|
2026-04-03 14:59:58 +08:00
|
|
|
State(state): State<ExecutionRuntimeAppState>,
|
2026-03-24 15:12:56 +08:00
|
|
|
request: Request,
|
2026-04-03 14:59:58 +08:00
|
|
|
) -> Result<Response, ExecutionRuntimeAppError> {
|
2026-03-24 15:12:56 +08:00
|
|
|
let request_permit = acquire_request_permit(&state).await?;
|
|
|
|
|
let plan = parse_request_json::<ExecutionPlan>(request).await?;
|
2026-03-21 12:57:09 +08:00
|
|
|
let execution = state
|
2026-04-03 14:59:58 +08:00
|
|
|
.execution_runtime
|
2026-04-21 16:19:07 +08:00
|
|
|
.execute_stream(&plan)
|
2026-03-21 12:57:09 +08:00
|
|
|
.await
|
2026-04-03 14:59:58 +08:00
|
|
|
.map_err(|err| ExecutionRuntimeAppError(ExecutionRuntimeServerError::Transport(err)))?;
|
2026-03-21 12:57:09 +08:00
|
|
|
|
2026-04-03 14:59:58 +08:00
|
|
|
let mut response = Response::new(Body::from_stream(build_direct_execution_frame_stream(
|
|
|
|
|
execution,
|
|
|
|
|
)));
|
2026-03-21 12:57:09 +08:00
|
|
|
*response.status_mut() = StatusCode::OK;
|
|
|
|
|
response.headers_mut().insert(
|
|
|
|
|
axum::http::header::CONTENT_TYPE,
|
|
|
|
|
axum::http::HeaderValue::from_static("application/x-ndjson"),
|
|
|
|
|
);
|
2026-03-24 15:12:56 +08:00
|
|
|
Ok(maybe_hold_axum_response_permit(response, request_permit))
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-03 14:59:58 +08:00
|
|
|
async fn acquire_request_permit(
|
|
|
|
|
state: &ExecutionRuntimeAppState,
|
|
|
|
|
) -> Result<Option<AdmissionPermit>, ExecutionRuntimeAppError> {
|
2026-03-24 15:12:56 +08:00
|
|
|
match state.try_acquire_request_permit().await {
|
|
|
|
|
Ok(permit) => Ok(permit),
|
|
|
|
|
Err(RequestAdmissionError::Local(ConcurrencyError::Saturated { gate, limit }))
|
2026-05-08 00:18:12 +08:00
|
|
|
| Err(RequestAdmissionError::Distributed(RuntimeSemaphoreError::Saturated {
|
2026-03-24 15:12:56 +08:00
|
|
|
gate,
|
|
|
|
|
limit,
|
|
|
|
|
}))
|
2026-05-08 00:18:12 +08:00
|
|
|
| Err(RequestAdmissionError::Distributed(RuntimeSemaphoreError::Unavailable {
|
2026-03-24 15:12:56 +08:00
|
|
|
gate,
|
|
|
|
|
limit,
|
|
|
|
|
..
|
2026-04-03 14:59:58 +08:00
|
|
|
})) => Err(ExecutionRuntimeAppError(
|
|
|
|
|
ExecutionRuntimeServerError::Overloaded { gate, limit },
|
|
|
|
|
)),
|
|
|
|
|
Err(RequestAdmissionError::Local(ConcurrencyError::Closed { gate })) => Err(
|
|
|
|
|
ExecutionRuntimeAppError(ExecutionRuntimeServerError::RequestRead(format!(
|
|
|
|
|
"execution runtime request concurrency gate {gate} is closed"
|
|
|
|
|
))),
|
|
|
|
|
),
|
2026-05-08 00:18:12 +08:00
|
|
|
Err(RequestAdmissionError::Distributed(RuntimeSemaphoreError::InvalidConfiguration(
|
|
|
|
|
message,
|
|
|
|
|
))) => Err(ExecutionRuntimeAppError(
|
2026-04-03 14:59:58 +08:00
|
|
|
ExecutionRuntimeServerError::RequestRead(message),
|
|
|
|
|
)),
|
2026-03-24 15:12:56 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
enum RequestAdmissionError {
|
|
|
|
|
Local(ConcurrencyError),
|
2026-05-08 00:18:12 +08:00
|
|
|
Distributed(RuntimeSemaphoreError),
|
2026-03-24 15:12:56 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-03 14:59:58 +08:00
|
|
|
async fn parse_request_json<T>(request: Request) -> Result<T, ExecutionRuntimeAppError>
|
2026-03-24 15:12:56 +08:00
|
|
|
where
|
|
|
|
|
T: serde::de::DeserializeOwned,
|
|
|
|
|
{
|
|
|
|
|
let body = to_bytes(request.into_body(), usize::MAX)
|
|
|
|
|
.await
|
2026-04-03 14:59:58 +08:00
|
|
|
.map_err(|err| {
|
|
|
|
|
ExecutionRuntimeAppError(ExecutionRuntimeServerError::RequestRead(err.to_string()))
|
|
|
|
|
})?;
|
|
|
|
|
serde_json::from_slice(&body).map_err(|err| {
|
|
|
|
|
ExecutionRuntimeAppError(ExecutionRuntimeServerError::InvalidRequestJson(err))
|
|
|
|
|
})
|
2026-03-24 15:12:56 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn build_overloaded_response(message: &str) -> Response {
|
|
|
|
|
(
|
|
|
|
|
StatusCode::SERVICE_UNAVAILABLE,
|
|
|
|
|
Json(json!({
|
|
|
|
|
"error": {
|
|
|
|
|
"type": "overloaded",
|
|
|
|
|
"message": message,
|
|
|
|
|
}
|
|
|
|
|
})),
|
|
|
|
|
)
|
|
|
|
|
.into_response()
|
2026-03-21 12:57:09 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-03 14:59:58 +08:00
|
|
|
#[derive(Debug, Error)]
|
|
|
|
|
enum ExecutionRuntimeServerError {
|
|
|
|
|
#[error("failed to read execution runtime request body: {0}")]
|
|
|
|
|
RequestRead(String),
|
|
|
|
|
#[error("execution runtime request body is not valid JSON: {0}")]
|
|
|
|
|
InvalidRequestJson(serde_json::Error),
|
|
|
|
|
#[error("execution runtime overloaded: gate {gate} saturated at {limit}")]
|
|
|
|
|
Overloaded { gate: &'static str, limit: usize },
|
|
|
|
|
#[error(transparent)]
|
|
|
|
|
Transport(#[from] ExecutionRuntimeTransportError),
|
|
|
|
|
}
|
2026-03-21 12:57:09 +08:00
|
|
|
|
2026-04-03 14:59:58 +08:00
|
|
|
#[derive(Debug)]
|
|
|
|
|
struct ExecutionRuntimeAppError(ExecutionRuntimeServerError);
|
|
|
|
|
|
|
|
|
|
impl IntoResponse for ExecutionRuntimeAppError {
|
2026-03-21 12:57:09 +08:00
|
|
|
fn into_response(self) -> Response {
|
|
|
|
|
let status_code = match self.0 {
|
2026-04-03 14:59:58 +08:00
|
|
|
ExecutionRuntimeServerError::RequestRead(_)
|
|
|
|
|
| ExecutionRuntimeServerError::InvalidRequestJson(_) => StatusCode::BAD_REQUEST,
|
|
|
|
|
ExecutionRuntimeServerError::Overloaded { .. } => {
|
2026-03-24 15:12:56 +08:00
|
|
|
return build_overloaded_response(&self.0.to_string());
|
|
|
|
|
}
|
2026-04-03 14:59:58 +08:00
|
|
|
ExecutionRuntimeServerError::Transport(
|
|
|
|
|
ExecutionRuntimeTransportError::StreamUnsupported
|
|
|
|
|
| ExecutionRuntimeTransportError::RequestBodyRequired
|
|
|
|
|
| ExecutionRuntimeTransportError::BodyDecode(_)
|
|
|
|
|
| ExecutionRuntimeTransportError::UnsupportedContentEncoding(_)
|
|
|
|
|
| ExecutionRuntimeTransportError::ProxyUnsupported
|
|
|
|
|
| ExecutionRuntimeTransportError::InvalidMethod(_)
|
|
|
|
|
| ExecutionRuntimeTransportError::InvalidHeaderName(_)
|
|
|
|
|
| ExecutionRuntimeTransportError::InvalidHeaderValue(_)
|
|
|
|
|
| ExecutionRuntimeTransportError::InvalidProxy(_)
|
2026-05-05 22:21:23 +08:00
|
|
|
| ExecutionRuntimeTransportError::UnsupportedTransportProfile(_)
|
2026-04-03 14:59:58 +08:00
|
|
|
| ExecutionRuntimeTransportError::BodyEncode(_),
|
|
|
|
|
) => StatusCode::BAD_REQUEST,
|
|
|
|
|
ExecutionRuntimeServerError::Transport(
|
|
|
|
|
ExecutionRuntimeTransportError::ClientBuild(_)
|
2026-05-16 19:23:23 +08:00
|
|
|
| ExecutionRuntimeTransportError::BrowserClientBuild(_)
|
|
|
|
|
| ExecutionRuntimeTransportError::BrowserBody(_)
|
2026-04-03 14:59:58 +08:00
|
|
|
| ExecutionRuntimeTransportError::UpstreamRequest(_)
|
|
|
|
|
| ExecutionRuntimeTransportError::RelayError(_)
|
|
|
|
|
| ExecutionRuntimeTransportError::InvalidJson(_),
|
|
|
|
|
) => StatusCode::BAD_GATEWAY,
|
2026-03-21 12:57:09 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
(
|
|
|
|
|
status_code,
|
|
|
|
|
Json(json!({
|
|
|
|
|
"error": self.0.to_string(),
|
|
|
|
|
})),
|
|
|
|
|
)
|
|
|
|
|
.into_response()
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-24 15:12:56 +08:00
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
2026-04-03 14:59:58 +08:00
|
|
|
use super::{
|
|
|
|
|
build_execution_runtime_router_with_request_concurrency_limit,
|
|
|
|
|
build_execution_runtime_router_with_request_gates, DISTRIBUTED_REQUEST_GATE_NAME,
|
|
|
|
|
};
|
2026-03-24 15:12:56 +08:00
|
|
|
use aether_contracts::{ExecutionPlan, ExecutionTimeouts, RequestBody};
|
2026-05-08 00:18:12 +08:00
|
|
|
use aether_runtime_state::{
|
|
|
|
|
MemoryRuntimeStateConfig, RuntimeSemaphore, RuntimeSemaphoreConfig, RuntimeState,
|
|
|
|
|
};
|
2026-03-24 15:12:56 +08:00
|
|
|
use axum::body::{Body, Bytes};
|
|
|
|
|
use axum::response::Response;
|
|
|
|
|
use axum::routing::any;
|
|
|
|
|
use axum::{extract::Request, Router};
|
|
|
|
|
use http::StatusCode;
|
|
|
|
|
use std::convert::Infallible;
|
|
|
|
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
|
2026-05-08 00:18:12 +08:00
|
|
|
fn distributed_gate(gate: &'static str, limit: usize) -> RuntimeSemaphore {
|
|
|
|
|
RuntimeState::memory(MemoryRuntimeStateConfig::default())
|
|
|
|
|
.semaphore(gate, limit, RuntimeSemaphoreConfig::default())
|
|
|
|
|
.expect("distributed semaphore")
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-24 15:12:56 +08:00
|
|
|
async fn start_server(app: Router) -> (String, tokio::task::JoinHandle<()>) {
|
2026-04-20 16:59:18 +08:00
|
|
|
let listener = crate::test_support::bind_loopback_listener()
|
2026-03-24 15:12:56 +08:00
|
|
|
.await
|
|
|
|
|
.expect("listener should bind");
|
|
|
|
|
let addr = listener.local_addr().expect("local addr should resolve");
|
|
|
|
|
let handle = tokio::spawn(async move {
|
|
|
|
|
axum::serve(listener, app).await.expect("server should run");
|
|
|
|
|
});
|
|
|
|
|
(format!("http://{addr}"), handle)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn stream_plan(url: String) -> ExecutionPlan {
|
|
|
|
|
ExecutionPlan {
|
|
|
|
|
request_id: "req-1".into(),
|
|
|
|
|
candidate_id: Some("cand-1".into()),
|
|
|
|
|
provider_name: Some("openai".into()),
|
|
|
|
|
provider_id: "prov-1".into(),
|
|
|
|
|
endpoint_id: "ep-1".into(),
|
|
|
|
|
key_id: "key-1".into(),
|
|
|
|
|
method: "GET".into(),
|
|
|
|
|
url,
|
|
|
|
|
headers: std::collections::BTreeMap::new(),
|
|
|
|
|
content_type: None,
|
|
|
|
|
content_encoding: None,
|
|
|
|
|
body: RequestBody {
|
|
|
|
|
json_body: None,
|
|
|
|
|
body_bytes_b64: None,
|
|
|
|
|
body_ref: None,
|
|
|
|
|
},
|
|
|
|
|
stream: true,
|
|
|
|
|
client_api_format: "openai:chat".into(),
|
|
|
|
|
provider_api_format: "openai:chat".into(),
|
|
|
|
|
model_name: Some("gpt-4.1".into()),
|
|
|
|
|
proxy: None,
|
2026-05-05 22:21:23 +08:00
|
|
|
transport_profile: None,
|
2026-03-24 15:12:56 +08:00
|
|
|
timeouts: Some(ExecutionTimeouts {
|
|
|
|
|
connect_ms: Some(5_000),
|
|
|
|
|
total_ms: Some(30_000),
|
|
|
|
|
..ExecutionTimeouts::default()
|
|
|
|
|
}),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
2026-04-03 14:59:58 +08:00
|
|
|
async fn execution_runtime_rejects_second_in_flight_stream_request_with_overload() {
|
2026-03-24 15:12:56 +08:00
|
|
|
let upstream_hits = Arc::new(AtomicUsize::new(0));
|
|
|
|
|
let upstream_hits_clone = Arc::clone(&upstream_hits);
|
|
|
|
|
let upstream = Router::new().route(
|
|
|
|
|
"/slow",
|
|
|
|
|
any(move |_request: Request| {
|
|
|
|
|
let upstream_hits = Arc::clone(&upstream_hits_clone);
|
|
|
|
|
async move {
|
|
|
|
|
upstream_hits.fetch_add(1, Ordering::SeqCst);
|
|
|
|
|
let stream = async_stream::stream! {
|
|
|
|
|
yield Ok::<_, Infallible>(Bytes::from_static(b"chunk-1"));
|
|
|
|
|
futures_util::future::pending::<()>().await;
|
|
|
|
|
};
|
|
|
|
|
Response::builder()
|
|
|
|
|
.status(StatusCode::OK)
|
|
|
|
|
.body(Body::from_stream(stream))
|
|
|
|
|
.expect("response should build")
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
let (upstream_url, upstream_handle) = start_server(upstream).await;
|
2026-04-03 14:59:58 +08:00
|
|
|
let runtime = build_execution_runtime_router_with_request_concurrency_limit(Some(1));
|
|
|
|
|
let (runtime_url, runtime_handle) = start_server(runtime).await;
|
2026-03-24 15:12:56 +08:00
|
|
|
|
|
|
|
|
let client = reqwest::Client::new();
|
|
|
|
|
let first_response = client
|
2026-04-03 14:59:58 +08:00
|
|
|
.post(format!("{runtime_url}/v1/execute/stream"))
|
2026-03-24 15:12:56 +08:00
|
|
|
.json(&stream_plan(format!("{upstream_url}/slow")))
|
|
|
|
|
.send()
|
|
|
|
|
.await
|
|
|
|
|
.expect("first request should succeed");
|
|
|
|
|
|
|
|
|
|
for _ in 0..50 {
|
|
|
|
|
if upstream_hits.load(Ordering::SeqCst) == 1 {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
|
|
|
|
}
|
|
|
|
|
assert_eq!(upstream_hits.load(Ordering::SeqCst), 1);
|
|
|
|
|
|
|
|
|
|
let second_response = client
|
2026-04-03 14:59:58 +08:00
|
|
|
.post(format!("{runtime_url}/v1/execute/stream"))
|
2026-03-24 15:12:56 +08:00
|
|
|
.json(&stream_plan(format!("{upstream_url}/slow")))
|
|
|
|
|
.send()
|
|
|
|
|
.await
|
|
|
|
|
.expect("second request should complete");
|
|
|
|
|
|
|
|
|
|
assert_eq!(second_response.status(), StatusCode::SERVICE_UNAVAILABLE);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
second_response
|
|
|
|
|
.json::<serde_json::Value>()
|
|
|
|
|
.await
|
|
|
|
|
.expect("json body should decode")["error"]["type"],
|
|
|
|
|
"overloaded"
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(upstream_hits.load(Ordering::SeqCst), 1);
|
|
|
|
|
|
|
|
|
|
drop(first_response);
|
2026-04-03 14:59:58 +08:00
|
|
|
runtime_handle.abort();
|
2026-03-24 15:12:56 +08:00
|
|
|
upstream_handle.abort();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
2026-04-03 14:59:58 +08:00
|
|
|
async fn execution_runtime_rejects_second_in_flight_stream_request_with_distributed_overload() {
|
2026-03-24 15:12:56 +08:00
|
|
|
let upstream_hits = Arc::new(AtomicUsize::new(0));
|
|
|
|
|
let upstream_hits_clone = Arc::clone(&upstream_hits);
|
|
|
|
|
let upstream = Router::new().route(
|
|
|
|
|
"/slow",
|
|
|
|
|
any(move |_request: Request| {
|
|
|
|
|
let upstream_hits = Arc::clone(&upstream_hits_clone);
|
|
|
|
|
async move {
|
|
|
|
|
upstream_hits.fetch_add(1, Ordering::SeqCst);
|
|
|
|
|
let stream = async_stream::stream! {
|
|
|
|
|
yield Ok::<_, Infallible>(Bytes::from_static(b"chunk-1"));
|
|
|
|
|
futures_util::future::pending::<()>().await;
|
|
|
|
|
};
|
|
|
|
|
Response::builder()
|
|
|
|
|
.status(StatusCode::OK)
|
|
|
|
|
.body(Body::from_stream(stream))
|
|
|
|
|
.expect("response should build")
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
let (upstream_url, upstream_handle) = start_server(upstream).await;
|
2026-05-08 00:18:12 +08:00
|
|
|
let distributed_gate = distributed_gate(DISTRIBUTED_REQUEST_GATE_NAME, 1);
|
2026-04-03 14:59:58 +08:00
|
|
|
let runtime_a =
|
|
|
|
|
build_execution_runtime_router_with_request_gates(None, Some(distributed_gate.clone()));
|
|
|
|
|
let runtime_b =
|
|
|
|
|
build_execution_runtime_router_with_request_gates(None, Some(distributed_gate));
|
|
|
|
|
let (runtime_a_url, runtime_a_handle) = start_server(runtime_a).await;
|
|
|
|
|
let (runtime_b_url, runtime_b_handle) = start_server(runtime_b).await;
|
2026-03-24 15:12:56 +08:00
|
|
|
|
|
|
|
|
let client = reqwest::Client::new();
|
|
|
|
|
let first_response = client
|
2026-04-03 14:59:58 +08:00
|
|
|
.post(format!("{runtime_a_url}/v1/execute/stream"))
|
2026-03-24 15:12:56 +08:00
|
|
|
.json(&stream_plan(format!("{upstream_url}/slow")))
|
|
|
|
|
.send()
|
|
|
|
|
.await
|
|
|
|
|
.expect("first request should succeed");
|
|
|
|
|
|
|
|
|
|
for _ in 0..50 {
|
|
|
|
|
if upstream_hits.load(Ordering::SeqCst) == 1 {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
|
|
|
|
}
|
|
|
|
|
assert_eq!(upstream_hits.load(Ordering::SeqCst), 1);
|
|
|
|
|
|
|
|
|
|
let second_response = client
|
2026-04-03 14:59:58 +08:00
|
|
|
.post(format!("{runtime_b_url}/v1/execute/stream"))
|
2026-03-24 15:12:56 +08:00
|
|
|
.json(&stream_plan(format!("{upstream_url}/slow")))
|
|
|
|
|
.send()
|
|
|
|
|
.await
|
|
|
|
|
.expect("second request should complete");
|
|
|
|
|
|
|
|
|
|
assert_eq!(second_response.status(), StatusCode::SERVICE_UNAVAILABLE);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
second_response
|
|
|
|
|
.json::<serde_json::Value>()
|
|
|
|
|
.await
|
|
|
|
|
.expect("json body should decode")["error"]["type"],
|
|
|
|
|
"overloaded"
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(upstream_hits.load(Ordering::SeqCst), 1);
|
|
|
|
|
|
|
|
|
|
drop(first_response);
|
2026-04-03 14:59:58 +08:00
|
|
|
runtime_a_handle.abort();
|
|
|
|
|
runtime_b_handle.abort();
|
2026-03-24 15:12:56 +08:00
|
|
|
upstream_handle.abort();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
2026-04-03 14:59:58 +08:00
|
|
|
async fn execution_runtime_exposes_request_concurrency_metrics() {
|
|
|
|
|
let runtime = build_execution_runtime_router_with_request_gates(
|
2026-03-24 15:12:56 +08:00
|
|
|
Some(4),
|
2026-05-08 00:18:12 +08:00
|
|
|
Some(distributed_gate(DISTRIBUTED_REQUEST_GATE_NAME, 6)),
|
2026-03-24 15:12:56 +08:00
|
|
|
);
|
2026-04-03 14:59:58 +08:00
|
|
|
let (runtime_url, runtime_handle) = start_server(runtime).await;
|
2026-03-24 15:12:56 +08:00
|
|
|
|
|
|
|
|
let response = reqwest::Client::new()
|
2026-04-03 14:59:58 +08:00
|
|
|
.get(format!("{runtime_url}/metrics"))
|
2026-03-24 15:12:56 +08:00
|
|
|
.send()
|
|
|
|
|
.await
|
|
|
|
|
.expect("request should succeed");
|
|
|
|
|
|
|
|
|
|
assert_eq!(response.status(), StatusCode::OK);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
response
|
|
|
|
|
.headers()
|
|
|
|
|
.get(http::header::CONTENT_TYPE)
|
|
|
|
|
.and_then(|value| value.to_str().ok()),
|
|
|
|
|
Some("text/plain; version=0.0.4; charset=utf-8")
|
|
|
|
|
);
|
|
|
|
|
let body = response.text().await.expect("body should read");
|
2026-04-03 14:59:58 +08:00
|
|
|
assert!(body.contains("service_up{service=\"aether-gateway-execution-runtime\"} 1"));
|
|
|
|
|
assert!(
|
|
|
|
|
body.contains("concurrency_available_permits{gate=\"execution_runtime_requests\"} 4")
|
|
|
|
|
);
|
|
|
|
|
assert!(body.contains(
|
|
|
|
|
"concurrency_available_permits{gate=\"execution_runtime_requests_distributed\"} 6"
|
|
|
|
|
));
|
2026-03-24 15:12:56 +08:00
|
|
|
|
2026-04-03 14:59:58 +08:00
|
|
|
runtime_handle.abort();
|
2026-03-24 15:12:56 +08:00
|
|
|
}
|
|
|
|
|
}
|