Skip to content

Commit e4c1891

Browse files
fix(query-engine): decouple backend health endpoint (#687)
* fix(query-engine): decouple backend health endpoint * test(query-engine): allocate dynamic Prometheus mock ports
1 parent 2586400 commit e4c1891

10 files changed

Lines changed: 181 additions & 71 deletions

File tree

‎asap-query-engine/examples/engine_config.yaml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ http_server:
3636
backend:
3737
type: "prometheus"
3838
server: "http://localhost:9090" # used for forwarding and planner context
39+
# Backend-only startup health check; ASAPQuery keeps /api/v1/status/runtimeinfo.
40+
health_endpoint: "/-/ready"
3941
forward_unsupported_queries: false # when true, server must be reachable at startup
4042

4143
# ClickHouse — exposes an SQL-over-HTTP API.

‎asap-query-engine/src/engine_config.rs‎

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ pub enum BackendConfig {
100100
/// Prometheus server URL used for query forwarding and planner context.
101101
#[serde(default = "default_prometheus_server")]
102102
server: String,
103+
/// Backend health endpoint used only for the startup reachability check.
104+
/// This is independent from ASAPQuery's own runtime-info endpoint.
105+
#[serde(default = "default_prometheus_health_endpoint")]
106+
health_endpoint: String,
103107
/// When true, queries not answerable from sketches are forwarded to `server`.
104108
/// The server must be reachable at startup.
105109
#[serde(default)]
@@ -145,6 +149,7 @@ impl Default for BackendConfig {
145149
fn default() -> Self {
146150
BackendConfig::Prometheus {
147151
server: default_prometheus_server(),
152+
health_endpoint: default_prometheus_health_endpoint(),
148153
forward_unsupported_queries: false,
149154
fallback_timeout_secs: default_fallback_timeout_secs(),
150155
}
@@ -181,12 +186,48 @@ impl BackendConfig {
181186
} => *forward_unsupported_queries,
182187
}
183188
}
189+
190+
/// Return the URL used to check that the configured backend is reachable.
191+
pub fn health_check_url(&self) -> String {
192+
match self {
193+
BackendConfig::Prometheus {
194+
server,
195+
health_endpoint,
196+
..
197+
} => join_endpoint(server, health_endpoint),
198+
BackendConfig::Clickhouse { url, .. } => join_endpoint(url, "/ping"),
199+
BackendConfig::ElasticQuerydsl { url, .. } | BackendConfig::ElasticSql { url, .. } => {
200+
join_endpoint(url, "/_cluster/health")
201+
}
202+
}
203+
}
204+
205+
pub fn server_url(&self) -> &str {
206+
match self {
207+
BackendConfig::Prometheus { server, .. } => server,
208+
BackendConfig::Clickhouse { url, .. }
209+
| BackendConfig::ElasticQuerydsl { url, .. }
210+
| BackendConfig::ElasticSql { url, .. } => url,
211+
}
212+
}
213+
}
214+
215+
fn join_endpoint(server: &str, endpoint: &str) -> String {
216+
format!(
217+
"{}/{}",
218+
server.trim_end_matches('/'),
219+
endpoint.trim_start_matches('/')
220+
)
184221
}
185222

186223
fn default_prometheus_server() -> String {
187224
"http://localhost:9090".to_string()
188225
}
189226

227+
fn default_prometheus_health_endpoint() -> String {
228+
"/-/ready".to_string()
229+
}
230+
190231
fn default_fallback_timeout_secs() -> u64 {
191232
30
192233
}
@@ -538,6 +579,10 @@ output_dir: "./output"
538579
assert!(matches!(config.backend, BackendConfig::Prometheus { .. }));
539580
assert_eq!(config.backend.query_language(), QueryLanguage::promql);
540581
assert!(!config.backend.forward_unsupported_queries());
582+
assert_eq!(
583+
config.backend.health_check_url(),
584+
"http://localhost:9090/-/ready"
585+
);
541586
}
542587

543588
#[test]
@@ -649,6 +694,30 @@ backend:
649694
assert!(config.backend.forward_unsupported_queries());
650695
}
651696

697+
#[test]
698+
fn backend_prometheus_uses_configured_health_endpoint() {
699+
// VictoriaMetrics does not implement Prometheus's runtime-info path;
700+
// its /health endpoint must be used for the backend startup check.
701+
let yaml = r#"
702+
streaming_engine: "precompute"
703+
ingest:
704+
type: "http_remote_write"
705+
port: 9090
706+
output_dir: "./output"
707+
backend:
708+
type: "prometheus"
709+
server: "http://victoriametrics:8428/"
710+
health_endpoint: "/health"
711+
forward_unsupported_queries: true
712+
"#;
713+
let config: EngineConfig = Figment::new().merge(Yaml::string(yaml)).extract().unwrap();
714+
715+
assert_eq!(
716+
config.backend.health_check_url(),
717+
"http://victoriametrics:8428/health"
718+
);
719+
}
720+
652721
#[test]
653722
fn check_config_rejects_query_tracker_with_non_prometheus_backend() {
654723
let yaml = r#"

‎asap-query-engine/src/main.rs‎

Lines changed: 60 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,7 @@ async fn main() -> Result<()> {
315315
server,
316316
forward_unsupported_queries,
317317
fallback_timeout_secs,
318+
..
318319
} => AdapterConfig::prometheus_promql(
319320
server.clone(),
320321
*forward_unsupported_queries,
@@ -353,41 +354,11 @@ async fn main() -> Result<()> {
353354

354355
if config.backend.forward_unsupported_queries() {
355356
let client = reqwest::Client::new();
356-
let (health_url, backend_label) = match &config.backend {
357-
BackendConfig::Prometheus { server, .. } => (
358-
format!("{}/api/v1/status/runtimeinfo", server.trim_end_matches('/')),
359-
server.clone(),
360-
),
361-
BackendConfig::Clickhouse { url, .. } => {
362-
(format!("{}/ping", url.trim_end_matches('/')), url.clone())
363-
}
364-
BackendConfig::ElasticQuerydsl { url, .. } | BackendConfig::ElasticSql { url, .. } => (
365-
format!("{}/_cluster/health", url.trim_end_matches('/')),
366-
url.clone(),
367-
),
368-
};
369-
match client
370-
.get(&health_url)
371-
.timeout(std::time::Duration::from_secs(5))
372-
.send()
373-
.await
374-
{
375-
Ok(resp) if resp.status().is_success() => {
376-
info!("Backend reachable at {}", backend_label);
377-
}
378-
Ok(resp) => {
379-
error!(
380-
"Backend at {} returned HTTP {} — cannot start",
381-
backend_label,
382-
resp.status()
383-
);
384-
std::process::exit(1);
385-
}
386-
Err(e) => {
387-
error!("Cannot reach backend at {}: {}", backend_label, e);
388-
std::process::exit(1);
389-
}
357+
if let Err(message) = check_backend_health(&client, &config.backend).await {
358+
error!("{}", message);
359+
std::process::exit(1);
390360
}
361+
info!("Backend reachable at {}", config.backend.server_url());
391362
}
392363

393364
let query_tracker = if config.query_tracker.enabled {
@@ -503,6 +474,29 @@ async fn main() -> Result<()> {
503474
Ok(())
504475
}
505476

477+
async fn check_backend_health(
478+
client: &reqwest::Client,
479+
backend: &BackendConfig,
480+
) -> std::result::Result<(), String> {
481+
let health_url = backend.health_check_url();
482+
let backend_label = backend.server_url();
483+
484+
match client
485+
.get(&health_url)
486+
.timeout(std::time::Duration::from_secs(5))
487+
.send()
488+
.await
489+
{
490+
Ok(resp) if resp.status().is_success() => Ok(()),
491+
Ok(resp) => Err(format!(
492+
"Backend at {} returned HTTP {} — cannot start",
493+
backend_label,
494+
resp.status()
495+
)),
496+
Err(e) => Err(format!("Cannot reach backend at {}: {}", backend_label, e)),
497+
}
498+
}
499+
506500
/// Periodic memory diagnostics logger — runs every 30 seconds.
507501
async fn spawn_memory_diagnostics(
508502
store: Arc<SimpleMapStore>,
@@ -596,3 +590,35 @@ fn setup_logging(
596590
info!("Logs will be written to: {}/query_engine.log", output_dir);
597591
Ok(guard)
598592
}
593+
594+
#[cfg(test)]
595+
mod tests {
596+
use super::{check_backend_health, BackendConfig};
597+
use axum::{routing::get, Router};
598+
use tokio::net::TcpListener;
599+
600+
#[tokio::test]
601+
async fn backend_health_check_uses_configured_endpoint_and_reports_failure() {
602+
let app = Router::new().route(
603+
"/health",
604+
get(|| async { axum::http::StatusCode::SERVICE_UNAVAILABLE }),
605+
);
606+
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
607+
let address = listener.local_addr().unwrap();
608+
tokio::spawn(async move {
609+
axum::serve(listener, app).await.unwrap();
610+
});
611+
612+
let backend = BackendConfig::Prometheus {
613+
server: format!("http://{}", address),
614+
health_endpoint: "/health".to_string(),
615+
forward_unsupported_queries: true,
616+
fallback_timeout_secs: 30,
617+
};
618+
let error = check_backend_health(&reqwest::Client::new(), &backend)
619+
.await
620+
.unwrap_err();
621+
622+
assert!(error.contains("returned HTTP 503"));
623+
}
624+
}

‎asap-query-engine/src/tests/prometheus_forwarding_tests.rs‎

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use tokio::net::TcpListener;
1111
use tokio::time::{sleep, Duration};
1212

1313
/// Mock Prometheus server for testing
14-
async fn start_mock_prometheus_server(port: u16) -> Result<(), Box<dyn std::error::Error>> {
14+
async fn start_mock_prometheus_server() -> Result<u16, Box<dyn std::error::Error>> {
1515
use axum::{extract::Query, response::Json, routing::get, Router};
1616
use serde_json::json;
1717
use std::collections::HashMap;
@@ -88,15 +88,16 @@ async fn start_mock_prometheus_server(port: u16) -> Result<(), Box<dyn std::erro
8888
.route("/api/v1/query", get(mock_query_handler))
8989
.route("/api/v1/query_range", get(mock_range_query_handler));
9090

91-
let listener = TcpListener::bind(format!("127.0.0.1:{port}")).await?;
91+
let listener = TcpListener::bind("127.0.0.1:0").await?;
92+
let port = listener.local_addr()?.port();
9293

9394
tokio::spawn(async move {
9495
axum::serve(listener, app).await.unwrap();
9596
});
9697

9798
// Give the server time to start
9899
sleep(Duration::from_millis(100)).await;
99-
Ok(())
100+
Ok(port)
100101
}
101102

102103
async fn setup_test_server(prometheus_port: u16) -> (HttpServer, u16) {
@@ -137,8 +138,7 @@ async fn setup_test_server(prometheus_port: u16) -> (HttpServer, u16) {
137138
#[tokio::test]
138139
async fn test_prometheus_forwarding_instant_query() {
139140
// Start mock Prometheus server
140-
let prometheus_port = 19090;
141-
start_mock_prometheus_server(prometheus_port).await.unwrap();
141+
let prometheus_port = start_mock_prometheus_server().await.unwrap();
142142

143143
// Start our HTTP server with forwarding enabled
144144
let (_server, server_port) = setup_test_server(prometheus_port).await;
@@ -168,8 +168,7 @@ async fn test_prometheus_forwarding_instant_query() {
168168
#[tokio::test]
169169
async fn test_prometheus_forwarding_error_handling() {
170170
// Start mock Prometheus server
171-
let prometheus_port = 19092;
172-
start_mock_prometheus_server(prometheus_port).await.unwrap();
171+
let prometheus_port = start_mock_prometheus_server().await.unwrap();
173172

174173
// Start our HTTP server with forwarding enabled
175174
let (_server, server_port) = setup_test_server(prometheus_port).await;
@@ -311,8 +310,7 @@ async fn test_prometheus_server_unreachable() {
311310

312311
#[tokio::test]
313312
async fn test_prometheus_forwarding_range_query() {
314-
let prometheus_port = 19094;
315-
start_mock_prometheus_server(prometheus_port).await.unwrap();
313+
let prometheus_port = start_mock_prometheus_server().await.unwrap();
316314

317315
let (_server, server_port) = setup_test_server(prometheus_port).await;
318316

‎asap-tools/experiments/config/config.yaml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ cluster_data_directory: "/data/cluster_traces" # Path to directory containing G
8787
# Backend configuration for the query engine (aligned with BackendConfig in asap-query-engine/src/engine_config.rs)
8888
backend:
8989
type: "prometheus" # choices: ["prometheus", "clickhouse", "elastic_querydsl", "elastic_sql"]
90-
# prometheus: server URL is built at runtime from the Prometheus service (no extra fields needed here)
90+
# prometheus: server and health_endpoint are built at runtime from the monitoring service
9191
# clickhouse: url (e.g. "http://ch-host:8123"), database (e.g. "default")
9292
# elastic_querydsl / elastic_sql: url (e.g. "http://es-host:9200"), index (e.g. "metrics-*")
9393

‎asap-tools/experiments/experiment_run_e2e.py‎

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
KafkaService,
1313
FlinkService,
1414
QueryEngineRustService,
15+
resolve_backend_config,
1516
ExporterServiceFactory,
1617
PrometheusKafkaAdapterService,
1718
ArroyoService,
@@ -484,22 +485,15 @@ def main(cfg: DictConfig):
484485

485486
# in case we want to run query engine manually
486487
if not cfg.flow.replace_query_engine_with_dumb_consumer:
487-
# Get prometheus port from prometheus service
488-
prometheus_port = prometheus_service.get_query_endpoint_port()
489488
# Get http port from query engine service
490489
http_port = query_engine_service.get_http_port()
491490

492-
# Build a fully resolved BackendConfig dict. For the prometheus
493-
# backend the server URL depends on the runtime node IP, so we
494-
# fill it in here rather than in config.yaml.
495-
backend_config = dict(args.backend)
496-
if backend_config["type"] == "prometheus":
497-
prometheus_host = provider.get_node_ip(args.node_offset)
498-
backend_config["server"] = (
499-
f"http://{prometheus_host}:{prometheus_port}"
500-
)
501-
backend_config["forward_unsupported_queries"] = (
502-
args.forward_unsupported_queries
491+
backend_config = resolve_backend_config(
492+
args.backend,
493+
prometheus_service,
494+
provider,
495+
args.node_offset,
496+
args.forward_unsupported_queries,
503497
)
504498

505499
query_engine_service.start(

‎asap-tools/experiments/experiment_run_grafana_demo.py‎

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from experiment_utils.services import (
1212
KafkaService,
1313
QueryEngineRustService,
14+
resolve_backend_config,
1415
ExporterServiceFactory,
1516
ArroyoService,
1617
ArroyoThroughputMonitor,
@@ -447,21 +448,16 @@ def main(cfg: DictConfig):
447448
)
448449
# in case we want to run query engine manually
449450
if not cfg.flow.replace_query_engine_with_dumb_consumer:
450-
# Get prometheus port from prometheus service
451-
prometheus_port = prometheus_service.get_query_endpoint_port()
452451
# Get http port from query engine service
453452
http_port = query_engine_service.get_http_port()
454453

455-
# Build a fully resolved BackendConfig dict. For the prometheus
456-
# backend the server URL depends on the runtime node IP, so we
457-
# fill it in here rather than in config.yaml.
458-
backend_config = dict(args.backend)
459-
if backend_config["type"] == "prometheus":
460-
prometheus_host = provider.get_node_ip(args.node_offset)
461-
backend_config["server"] = f"http://{prometheus_host}:{prometheus_port}"
462454
# forward_unsupported_queries is forced True for the Grafana demo (line 63)
463-
backend_config["forward_unsupported_queries"] = (
464-
args.forward_unsupported_queries
455+
backend_config = resolve_backend_config(
456+
args.backend,
457+
prometheus_service,
458+
provider,
459+
args.node_offset,
460+
args.forward_unsupported_queries,
465461
)
466462

467463
query_engine_service.start(

‎asap-tools/experiments/experiment_utils/services/__init__.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from .flink import FlinkService
1111
from .query_engine import (
1212
QueryEngineRustService,
13+
resolve_backend_config,
1314
)
1415
from .monitoring import MonitoringService
1516
from .fake_exporters import (
@@ -131,6 +132,7 @@ def create_prometheus_service(cfg, provider, num_nodes: int, node_offset: int):
131132
"KafkaService",
132133
"FlinkService",
133134
"QueryEngineRustService",
135+
"resolve_backend_config",
134136
"MonitoringService",
135137
"ExporterServiceFactory",
136138
"PythonExporterService",

0 commit comments

Comments
 (0)