transport: complete M0–M7
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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user