transport: complete M0–M7
Some checks failed
ci / rust (push) Failing after 2m21s
ci / ui (push) Failing after 28s
images / build-and-push (push) Failing after 18s

shared: add stream+consumer policy helpers; NATS context header builder

aggregate/runner/projection: centralize stream validation and header usage; set bounded consumer params

projection: add QueryService gRPC and wire into main; settings include PROJECTION_GRPC_ADDR

gateway: gRPC routing to Projection/Runner with deadlines; bounded read-only retries; pooled gRPC channels (bounded LRU+TTL); admin proxy forwards to gRPC; probes use concurrency limiter + TTL cache

runner: add RunnerAdmin gRPC server (drain, status, reload) and wire into main; settings include RUNNER_GRPC_ADDR

tests: add gateway authz for runner admin, projection tenant isolation, runner admin drain semantics

docs: update TRANSPORT_DEVELOPMENT_PLAN to reflect completed milestones and details
This commit is contained in:
2026-03-30 14:24:14 +03:00
parent 1ab112438b
commit 90c307016d
41 changed files with 2391 additions and 505 deletions

View File

@@ -144,6 +144,7 @@ async fn status(
async fn gates(
State(state): State<AppState>,
ctx: crate::RequestContext,
principal: Principal,
Query(q): Query<TenantQuery>,
) -> Result<Json<GatesResponse>, AuthzRejection> {
@@ -165,24 +166,33 @@ async fn gates(
.await
.ok();
let projection_ready = if let Some(ep) = projection_endpoint {
projection_gate_ready(&ep, &q.tenant_id)
.await
.unwrap_or(false)
} else {
false
let projection_fut = async {
if let Some(ep) = projection_endpoint {
projection_gate_ready(&ep, &q.tenant_id, &ctx)
.await
.unwrap_or(false)
} else {
false
}
};
let runner_ready = if let Some(ep) = runner_endpoint {
http_ready(&ep).await.unwrap_or(false)
} else {
false
let runner_fut = async {
if let Some(ep) = runner_endpoint {
http_ready(&ep, &ctx).await.unwrap_or(false)
} else {
false
}
};
let aggregate_ready = if let Some(ep) = aggregate_endpoint {
aggregate_ready(&ep).await.unwrap_or(false)
} else {
false
let aggregate_fut = async {
if let Some(ep) = aggregate_endpoint {
aggregate_ready(&ep, &ctx).await.unwrap_or(false)
} else {
false
}
};
let (projection_ready, runner_ready, aggregate_ready) =
tokio::join!(projection_fut, runner_fut, aggregate_fut);
Ok(Json(GatesResponse {
tenant_id: q.tenant_id,
aggregate_ready,
@@ -191,35 +201,49 @@ async fn gates(
}))
}
async fn http_ready(endpoint: &str) -> Result<bool, AuthzRejection> {
async fn http_ready(endpoint: &str, ctx: &crate::RequestContext) -> Result<bool, AuthzRejection> {
let url = format!("{}/ready", endpoint.trim_end_matches('/'));
let client = crate::upstream::http_client();
let resp = tokio::time::timeout(Duration::from_secs(2), client.get(url).send())
.await
.map_err(|_| AuthzRejection::Internal)?
.map_err(|_| AuthzRejection::Internal)?;
Ok(resp.status().is_success())
crate::upstream::probe_status_ok(
&url,
&[
(shared::HEADER_X_CORRELATION_ID, ctx.correlation_id.as_str()),
(shared::HEADER_TRACEPARENT, ctx.traceparent.as_str()),
],
Duration::from_secs(2),
Duration::from_millis(500),
)
.await
.map_err(|_| AuthzRejection::Internal)
}
async fn aggregate_ready(endpoint: &str) -> Result<bool, AuthzRejection> {
async fn aggregate_ready(
endpoint: &str,
ctx: &crate::RequestContext,
) -> Result<bool, AuthzRejection> {
if endpoint.contains(":50051") {
let http_ep = endpoint.replace(":50051", ":8080");
return http_ready(&http_ep).await;
return http_ready(&http_ep, ctx).await;
}
http_ready(endpoint).await
http_ready(endpoint, ctx).await
}
async fn projection_gate_ready(endpoint: &str, tenant_id: &str) -> Result<bool, AuthzRejection> {
async fn projection_gate_ready(
endpoint: &str,
tenant_id: &str,
ctx: &crate::RequestContext,
) -> Result<bool, AuthzRejection> {
let url = format!("{}/metrics", endpoint.trim_end_matches('/'));
let client = crate::upstream::http_client();
let resp = tokio::time::timeout(Duration::from_secs(2), client.get(url).send())
.await
.map_err(|_| AuthzRejection::Internal)?
.map_err(|_| AuthzRejection::Internal)?;
if !resp.status().is_success() {
return Ok(false);
}
let text = resp.text().await.map_err(|_| AuthzRejection::Internal)?;
let text = crate::upstream::probe_text(
&url,
&[
(shared::HEADER_X_CORRELATION_ID, ctx.correlation_id.as_str()),
(shared::HEADER_TRACEPARENT, ctx.traceparent.as_str()),
],
Duration::from_secs(2),
Duration::from_millis(250),
)
.await
.map_err(|_| AuthzRejection::Internal)?;
let ready = parse_prom_gauge(&text, "projection_ready").unwrap_or(0.0) >= 1.0;
if !ready {