Repository navigation
feat(query-engine): answer RQE expression trees on POST /api/v1/rqe_query #889
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
fa57681
d8681d5
2138a02
7c77405
c25fd46
f8ae11f
b80eaf4
3236e88
1b07c83
7ef4f7c
33ddb10
4a4e9da
a791ac5
60a7844
995f16a
2f18617
08b50c6
c2064d2
c2da7dc
292f30d
f051622
bd683bc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,8 +14,15 @@ use std::time::Instant; | |
| use tokio::net::TcpListener; | ||
| use tracing::{debug, info, warn}; | ||
|
|
||
| use crate::drivers::query::adapters::{create_http_adapter, AdapterConfig, HttpProtocolAdapter}; | ||
| use crate::engines::{QueryExecutionError, SimpleEngine}; | ||
| use crate::drivers::query::adapters::{ | ||
| create_http_adapter, AdapterConfig, HttpProtocolAdapter, PrometheusResponse, | ||
| }; | ||
| use crate::engines::{QueryExecutionError, RqeQueryError, SimpleEngine}; | ||
| use crate::utils::http::convert_query_result_to_prometheus; | ||
| use asap_types::rqe_expr::RqeExpr; | ||
|
|
||
| /// Answers an RQE expression tree, whatever the adapter's query language. | ||
| pub const RQE_QUERY_ENDPOINT: &str = "/api/v1/rqe_query"; | ||
| use crate::query_tracker::QueryTracker; | ||
| use crate::stores::Store; | ||
|
|
||
|
|
@@ -106,6 +113,7 @@ impl HttpServer { | |
| .route(range_query_endpoint, post(handle_range_query_post)) | ||
| .route(runtime_info_path, get(handle_runtime_info)) | ||
| .route(runtime_info_path, post(handle_runtime_info)) | ||
| .route(RQE_QUERY_ENDPOINT, post(handle_rqe_query)) | ||
| .route("/metrics", get(handle_metrics)) | ||
| .with_state(app_state); | ||
|
|
||
|
|
@@ -143,6 +151,7 @@ impl HttpServer { | |
| .route(range_query_endpoint, get(handle_range_query)) | ||
| .route(range_query_endpoint, post(handle_range_query_post)) | ||
| .route(runtime_info_path, get(handle_runtime_info)) | ||
| .route(RQE_QUERY_ENDPOINT, post(handle_rqe_query)) | ||
| .with_state(app_state); | ||
|
|
||
| let listener = TcpListener::bind("127.0.0.1:0").await?; | ||
|
|
@@ -445,6 +454,75 @@ async fn handle_instant_query_post( | |
| result | ||
| } | ||
|
|
||
| /// Body of an RQE query: the tree and its evaluation time in unix seconds. | ||
| #[derive(serde::Deserialize)] | ||
| #[serde(deny_unknown_fields)] | ||
| struct RqeQueryRequest { | ||
| expr: RqeExpr, | ||
| time: f64, | ||
| } | ||
|
|
||
| /// A Prometheus-format error response with an explicit HTTP status. | ||
| fn prometheus_error(status: StatusCode, error_type: &str, error: &str) -> Response { | ||
| (status, Json(PrometheusResponse::error(error_type, error))).into_response() | ||
| } | ||
|
|
||
| /// `POST /api/v1/rqe_query`: answers with Prometheus instant-vector JSON. Never | ||
| /// forwards to a fallback backend: an invalid request is a 400, a failed | ||
| /// execution a 500, and a window without data yet an empty result. With | ||
| /// request handling disabled it answers 500 `internal`, as the instant route | ||
| /// does when it has no fallback either. | ||
| async fn handle_rqe_query(State(state): State<AppState>, body: Bytes) -> Response { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Failure scenario: With the query tracker enabled for passive auto-discovery, rqe-frontend workloads are invisible to it, so auto-planning never sees them. (from automated /code-review; unverified)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Declining. The tracker discovers PromQL strings for passive auto-planning, and RQE trees can't be planned that way; trees come from saved plans. |
||
| if !state.config.handle_http_requests { | ||
| debug!("HTTP request handling is disabled; RQE queries have no fallback"); | ||
| return prometheus_error( | ||
| StatusCode::INTERNAL_SERVER_ERROR, | ||
| "internal", | ||
| "RQE query handling is disabled", | ||
| ); | ||
| } | ||
| let start_time = Instant::now(); | ||
| let request: RqeQueryRequest = match serde_json::from_slice(&body) { | ||
| Ok(request) => request, | ||
| Err(error) => { | ||
| return prometheus_error( | ||
| StatusCode::BAD_REQUEST, | ||
| "bad_data", | ||
| &format!("invalid RQE query body: {error}"), | ||
| ); | ||
| } | ||
| }; | ||
| match state | ||
| .query_engine | ||
| .handle_rqe_query(&request.expr, request.time) | ||
| { | ||
| Ok((labels, result)) => { | ||
| info!( | ||
| "rqe_query time={} destination=asap total_latency_ms={:.2}", | ||
| request.time, | ||
| start_time.elapsed().as_secs_f64() * 1000.0 | ||
| ); | ||
| match convert_query_result_to_prometheus(&result, &labels) { | ||
| Ok(data) => Json(PrometheusResponse::success(data)).into_response(), | ||
| Err(error) => { | ||
| prometheus_error(StatusCode::INTERNAL_SERVER_ERROR, "internal", error) | ||
| } | ||
| } | ||
| } | ||
| Err(RqeQueryError::BadRequest(error)) => { | ||
| prometheus_error(StatusCode::BAD_REQUEST, "bad_data", &error) | ||
| } | ||
| Err(RqeQueryError::Execution(error)) => { | ||
| warn!(error = %error, "RQE query execution failed"); | ||
| prometheus_error( | ||
| StatusCode::INTERNAL_SERVER_ERROR, | ||
| "internal", | ||
| &error.to_string(), | ||
| ) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| async fn handle_runtime_info( | ||
| State(state): State<AppState>, | ||
| headers: axum::http::HeaderMap, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -51,7 +51,7 @@ fn detect_scalar_arm<'a>( | |
| /// two arms don't share the same label set — mirroring DataFusion's | ||
| /// `build_binary_vector_plan`, which fails to resolve a join column that | ||
| /// only exists on one side. | ||
| fn combine_vector_vector( | ||
| pub(super) fn combine_vector_vector( | ||
| lhs_results: Vec<InstantVectorElement>, | ||
| lhs_labels: &[String], | ||
| rhs_results: Vec<InstantVectorElement>, | ||
|
|
@@ -83,7 +83,7 @@ fn combine_vector_vector( | |
| /// Scalar combiner for one binary-expr level (instant query): applies | ||
| /// `op(scalar, value)` or `op(value, scalar)` per `scalar_on_left` to every | ||
| /// element of the vector arm's results. | ||
| fn combine_scalar( | ||
| pub(super) fn combine_scalar( | ||
| vector_results: Vec<InstantVectorElement>, | ||
| scalar: f64, | ||
| op: &promql_parser::parser::token::TokenType, | ||
|
|
@@ -119,14 +119,14 @@ impl SimpleEngine { | |
| /// boundary, unconditionally — mirroring SQL's `align_end_timestamp_sql`. | ||
| /// A no-op in the common case (already-aligned timestamps), a safety net | ||
| /// otherwise, for every PromQL query shape, not just a subset. | ||
| fn align_end_timestamp_promql(&self, end_timestamp: u64) -> u64 { | ||
| pub(super) fn align_end_timestamp_promql(&self, end_timestamp: u64) -> u64 { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. RQE leaves reuse
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in a791ac5: the warning is now language-neutral ("Query end timestamp …"). |
||
| let interval_ms = self.data_ingestion_interval_ms; | ||
| if end_timestamp.is_multiple_of(interval_ms) { | ||
| return end_timestamp; | ||
| } | ||
| let aligned = (end_timestamp / interval_ms) * interval_ms; | ||
| warn!( | ||
| "PromQL query end timestamp {} is not aligned with data ingestion interval of {} ms; \ | ||
| "Query end timestamp {} is not aligned with data ingestion interval of {} ms; \ | ||
| aligning down to {}.", | ||
| end_timestamp, interval_ms, aligned | ||
| ); | ||
|
|
@@ -286,7 +286,7 @@ impl SimpleEngine { | |
| let match_result = self.find_matching_controller_pattern(arm_ast, planned_subquery)?; | ||
|
|
||
| let agg_info = self | ||
| .get_aggregation_id_info(query_config) | ||
| .get_aggregation_id_info(&query_config.aggregations) | ||
| .map_err(|e| { | ||
| warn!("{}", e); | ||
| e | ||
|
|
@@ -1323,7 +1323,7 @@ impl SimpleEngine { | |
|
|
||
| // Resolve aggregation: try pre-configured query_configs first, fall back to capability matching. | ||
| let agg_info: AggregationIdInfo = if let Some(config) = self.find_query_config(query) { | ||
| self.get_aggregation_id_info(&config) | ||
| self.get_aggregation_id_info(&config.aggregations) | ||
| .map_err(|e| { | ||
| warn!("{}", e); | ||
| e | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
handle_rqe_queryskips thestate.config.handle_http_requestscheck thatprocess_query_requestapplies to every other query route.Failure scenario: With handle_http_requests=false, POST /api/v1/rqe_query still evaluates RQE trees against the local store, bypassing the disable switch for this route only.
(from automated /code-review; unverified)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in c2064d2: with
handle_http_requestsoff, the route returns aninternalerror ('RQE query handling is disabled'), as the instant route does when it has no fallback.