diff --git a/config/config.md b/config/config.md
index a1c91a8af8..11e846ac76 100644
--- a/config/config.md
+++ b/config/config.md
@@ -37,7 +37,6 @@
| `http.body_limit` | String | `64MB` | HTTP request body limit.
The following units are supported: `B`, `KB`, `KiB`, `MB`, `MiB`, `GB`, `GiB`, `TB`, `TiB`, `PB`, `PiB`.
Set to 0 to disable limit. |
| `http.enable_cors` | Bool | `true` | HTTP CORS support, it's turned on by default
This allows browser to access http APIs without CORS restrictions |
| `http.cors_allowed_origins` | Array | Unset | Customize allowed origins for HTTP CORS. |
-| `http.experimental_enable_explain_analyze_stream` | Bool | `true` | Experimental: enable POST /v1/sql/analyze/stream for streaming EXPLAIN ANALYZE VERBOSE metrics. |
| `http.enable_api_server` | Bool | `false` | Whether to start the dedicated public HTTP **API** server. This server serves
only the `v1` interfaces plus the dashboard, and shares every other `[http]`
option with the main server. It is disabled by default; set to `true` to enable. |
| `http.api_server_addr` | String | `127.0.0.1:4006` | The address to bind the dedicated HTTP API server, in the same form as `addr`.
Defaults to `127.0.0.1:4006`. |
| `grpc` | -- | -- | The gRPC server options. |
@@ -274,7 +273,6 @@
| `http.body_limit` | String | `64MB` | HTTP request body limit.
The following units are supported: `B`, `KB`, `KiB`, `MB`, `MiB`, `GB`, `GiB`, `TB`, `TiB`, `PB`, `PiB`.
Set to 0 to disable limit. |
| `http.enable_cors` | Bool | `true` | HTTP CORS support, it's turned on by default
This allows browser to access http APIs without CORS restrictions |
| `http.cors_allowed_origins` | Array | Unset | Customize allowed origins for HTTP CORS. |
-| `http.experimental_enable_explain_analyze_stream` | Bool | `true` | Experimental: enable POST /v1/sql/analyze/stream for streaming EXPLAIN ANALYZE VERBOSE metrics. |
| `http.enable_api_server` | Bool | `false` | Whether to start the dedicated public HTTP **API** server. This server serves
only the `v1` interfaces plus the dashboard, and shares every other `[http]`
option with the main server. It is disabled by default; set to `true` to enable. |
| `http.api_server_addr` | String | `127.0.0.1:4006` | The address to bind the dedicated HTTP API server, in the same form as `addr`.
Defaults to `127.0.0.1:4006`. |
| `grpc` | -- | -- | The gRPC server options. |
diff --git a/config/frontend.example.toml b/config/frontend.example.toml
index 048a736055..9175fa598c 100644
--- a/config/frontend.example.toml
+++ b/config/frontend.example.toml
@@ -65,8 +65,6 @@ enable_cors = true
## Customize allowed origins for HTTP CORS.
## @toml2docs:none-default
cors_allowed_origins = ["https://example.com"]
-## Experimental: enable POST /v1/sql/analyze/stream for streaming EXPLAIN ANALYZE VERBOSE metrics.
-experimental_enable_explain_analyze_stream = true
## Whether to start the dedicated public HTTP **API** server. This server serves
## only the `v1` interfaces plus the dashboard, and shares every other `[http]`
diff --git a/config/standalone.example.toml b/config/standalone.example.toml
index 1026ddc925..5458e0ed83 100644
--- a/config/standalone.example.toml
+++ b/config/standalone.example.toml
@@ -91,8 +91,6 @@ enable_cors = true
## @toml2docs:none-default
cors_allowed_origins = ["https://example.com"]
-## Experimental: enable POST /v1/sql/analyze/stream for streaming EXPLAIN ANALYZE VERBOSE metrics.
-experimental_enable_explain_analyze_stream = true
## Whether to start the dedicated public HTTP **API** server. This server serves
## only the `v1` interfaces plus the dashboard, and shares every other `[http]`
diff --git a/src/client/src/region.rs b/src/client/src/region.rs
index cd155810ea..1ef40c4d25 100644
--- a/src/client/src/region.rs
+++ b/src/client/src/region.rs
@@ -607,7 +607,6 @@ mod test {
vec![Arc::new(Int32Vector::from_slice([1])) as VectorRef],
)
.unwrap();
-
let mut recordbatches = recordbatches_from_flight_message_stream(
"test-peer".to_string(),
stream::iter(vec![
@@ -679,6 +678,35 @@ mod test {
assert!(recordbatches.next().await.is_none());
}
+ #[tokio::test]
+ async fn test_record_batch_stream_captures_final_metrics_after_record_batch() {
+ let schema = test_schema();
+ let batch = RecordBatch::new(
+ schema.clone(),
+ vec![Arc::new(Int32Vector::from_slice([1])) as VectorRef],
+ )
+ .unwrap();
+ let final_metrics = serde_json::to_string(&RecordBatchMetrics {
+ elapsed_compute: 99,
+ ..Default::default()
+ })
+ .unwrap();
+ let mut recordbatches = recordbatches_from_flight_message_stream(
+ "test-peer".to_string(),
+ stream::iter(vec![
+ Ok(FlightMessage::Schema(schema.arrow_schema().clone())),
+ Ok(FlightMessage::RecordBatch(batch.into_df_record_batch())),
+ Ok(FlightMessage::Metrics(final_metrics)),
+ ]),
+ )
+ .await
+ .unwrap();
+
+ assert_eq!(recordbatches.next().await.unwrap().unwrap().num_rows(), 1);
+ assert!(recordbatches.next().await.is_none());
+ assert_eq!(recordbatches.metrics().unwrap().elapsed_compute, 99);
+ }
+
#[tokio::test]
async fn test_record_batch_stream_exposes_error_after_pre_batch_metrics() {
let schema = test_schema();
diff --git a/src/common/recordbatch/src/adapter.rs b/src/common/recordbatch/src/adapter.rs
index eeac94904b..ca9619c013 100644
--- a/src/common/recordbatch/src/adapter.rs
+++ b/src/common/recordbatch/src/adapter.rs
@@ -327,7 +327,9 @@ impl RecordBatchStreamAdapter {
query_load_region_id: Option,
) -> RecordBatchMetrics {
if explain_verbose {
- collect_full_metrics(df_plan, explain_verbose, query_load_region_id)
+ // Verbose in-progress snapshots have the same complete topology as
+ // the final snapshot, but use compact (Default) plan formatting.
+ collect_full_metrics(df_plan, false, query_load_region_id)
} else {
collect_lightweight_query_load_metrics(df_plan, query_load_region_id)
}
@@ -479,13 +481,17 @@ impl RecordBatchStream for RecordBatchStreamAdapter {
match &self.metrics_2 {
Metrics::Unresolved(df_plan) => {
if self.explain_verbose {
- Some(self.collect_plan_metrics(df_plan))
+ Some(Self::collect_partial_metrics(
+ df_plan.as_ref(),
+ true,
+ self.query_load_region_id,
+ ))
} else {
None
}
}
Metrics::PartialResolved(df_plan, metrics) => Some(if self.explain_verbose {
- self.collect_plan_metrics(df_plan)
+ Self::collect_partial_metrics(df_plan.as_ref(), true, self.query_load_region_id)
} else {
metrics.clone()
}),
@@ -954,6 +960,7 @@ mod test {
struct TestMetricsExec {
properties: Arc,
metrics: ExecutionPlanMetricsSet,
+ format_plan: bool,
}
impl TestMetricsExec {
@@ -961,7 +968,19 @@ mod test {
Self::with_output_bytes(schema, &[24])
}
+ fn new_without_plan_formatting(schema: DfSchemaRef) -> Self {
+ Self::with_output_bytes_and_formatting(schema, &[24], false)
+ }
+
fn with_output_bytes(schema: DfSchemaRef, output_bytes_by_partition: &[usize]) -> Self {
+ Self::with_output_bytes_and_formatting(schema, output_bytes_by_partition, true)
+ }
+
+ fn with_output_bytes_and_formatting(
+ schema: DfSchemaRef,
+ output_bytes_by_partition: &[usize],
+ format_plan: bool,
+ ) -> Self {
let metrics = ExecutionPlanMetricsSet::new();
let elapsed_compute = MetricBuilder::new(&metrics).elapsed_compute(0);
elapsed_compute.add_duration(Duration::from_nanos(42));
@@ -978,13 +997,22 @@ mod test {
Boundedness::Bounded,
)),
metrics,
+ format_plan,
}
}
}
impl DisplayAs for TestMetricsExec {
- fn fmt_as(&self, _t: DisplayFormatType, _f: &mut std::fmt::Formatter) -> std::fmt::Result {
- panic!("non-verbose lightweight partial metrics must not format the plan")
+ fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ assert!(
+ self.format_plan,
+ "non-verbose lightweight partial metrics must not format the plan"
+ );
+ write!(f, "RegionScanExec")?;
+ if matches!(t, DisplayFormatType::Verbose) {
+ write!(f, ": files=[file-1.parquet]")?;
+ }
+ Ok(())
}
}
@@ -1070,7 +1098,9 @@ mod test {
futures::stream::iter(vec![Ok(batch1), Ok(batch2)]),
),
);
- let plan = Arc::new(TestMetricsExec::new(schema.arrow_schema().clone()));
+ let plan = Arc::new(TestMetricsExec::new_without_plan_formatting(
+ schema.arrow_schema().clone(),
+ ));
let mut adapter = RecordBatchStreamAdapter::try_new(df_stream).unwrap();
adapter.set_metrics2(plan);
@@ -1087,12 +1117,49 @@ mod test {
assert_eq!(metrics.plan_metrics.len(), 1);
assert_eq!(metrics.plan_metrics[0].plan, REGION_SCAN_EXEC_NAME);
assert_eq!(metrics.plan_metrics[0].plan_name, REGION_SCAN_EXEC_NAME);
- assert_eq!(
- metrics.plan_metrics[0].metrics,
- vec![("output_bytes".to_string(), 24)]
+ assert!(
+ metrics.plan_metrics[0]
+ .metrics
+ .iter()
+ .any(|(name, value)| name == "output_bytes" && *value == 24)
);
}
+ #[tokio::test]
+ async fn test_record_batch_stream_adapter_uses_compact_partial_and_verbose_final_metrics() {
+ let schema = Arc::new(Schema::new(vec![ColumnSchema::new(
+ "a",
+ ConcreteDataType::int32_datatype(),
+ false,
+ )]));
+ let batch = RecordBatch::new(
+ schema.clone(),
+ vec![Arc::new(Int32Vector::from_slice([1])) as _],
+ )
+ .unwrap()
+ .into_df_record_batch();
+ let df_stream = Box::pin(
+ datafusion::physical_plan::stream::RecordBatchStreamAdapter::new(
+ schema.arrow_schema().clone(),
+ futures::stream::iter(vec![Ok(batch)]),
+ ),
+ );
+ let plan = Arc::new(TestMetricsExec::new(schema.arrow_schema().clone()));
+ let mut adapter = RecordBatchStreamAdapter::try_new(df_stream).unwrap();
+ adapter.set_metrics2(plan);
+ adapter.set_explain_verbose(true);
+
+ adapter.next().await.unwrap().unwrap();
+ let partial = adapter.metrics().unwrap();
+ assert_eq!(partial.plan_metrics.len(), 1);
+ assert_eq!(partial.plan_metrics[0].plan.trim_end(), "RegionScanExec");
+ assert!(!partial.plan_metrics[0].plan.contains("files"));
+
+ assert!(adapter.next().await.is_none());
+ let final_metrics = adapter.metrics().unwrap();
+ assert!(final_metrics.plan_metrics[0].plan.contains("files"));
+ }
+
#[test]
fn test_record_batch_stream_adapter_reuses_partial_query_stats_on_drop() {
let schema = Arc::new(Schema::new(vec![ColumnSchema::new(
diff --git a/src/frontend/src/instance.rs b/src/frontend/src/instance.rs
index 90650014e6..dd89a1f3f9 100644
--- a/src/frontend/src/instance.rs
+++ b/src/frontend/src/instance.rs
@@ -231,31 +231,37 @@ fn parse_stmt(sql: &str, dialect: &(dyn Dialect + Send + Sync)) -> Result bool {
matches!(stmt, Statement::Explain(explain) if explain.analyze && explain.verbose)
+ || matches!(stmt, Statement::Tql(Tql::Analyze(analyze)) if analyze.is_verbose)
}
fn validate_analyze_stream_statement(stmt: &mut Statement) -> Result<()> {
- let Statement::Explain(explain) = stmt else {
- return InvalidSqlSnafu {
- err_msg: "only EXPLAIN ANALYZE VERBOSE statement is supported",
+ let (is_verbose, format) = match stmt {
+ Statement::Explain(explain) => (explain.analyze && explain.verbose, &mut explain.format),
+ Statement::Tql(Tql::Analyze(analyze)) => (analyze.is_verbose, &mut analyze.format),
+ _ => {
+ return InvalidSqlSnafu {
+ err_msg: "only EXPLAIN ANALYZE VERBOSE or TQL ANALYZE VERBOSE statement is supported",
+ }
+ .fail();
}
- .fail();
};
+
ensure!(
- explain.analyze && explain.verbose,
+ is_verbose,
InvalidSqlSnafu {
- err_msg: "statement must be EXPLAIN ANALYZE VERBOSE"
+ err_msg: "statement must be EXPLAIN ANALYZE VERBOSE or TQL ANALYZE VERBOSE"
}
);
- match explain.format {
+ match format {
None | Some(AnalyzeFormat::JSON) => {
// Keep explicit FORMAT JSON accepted, but pass JSON through
- // QueryContext.explain_format instead of the statement to avoid the
- // planner's current `EXPLAIN VERBOSE with FORMAT` limitation.
- explain.format = None;
+ // QueryContext.explain_format instead of the statement to avoid
+ // the planner's current `EXPLAIN VERBOSE with FORMAT` limitation.
+ *format = None;
Ok(())
}
Some(_) => InvalidSqlSnafu {
- err_msg: "only FORMAT JSON is supported for analyze stream",
+ err_msg: "only FORMAT JSON is supported for EXPLAIN ANALYZE VERBOSE or TQL ANALYZE VERBOSE",
}
.fail(),
}
@@ -711,7 +717,7 @@ impl Instance {
ensure!(
stmts.len() == 1,
InvalidSqlSnafu {
- err_msg: "only single EXPLAIN ANALYZE VERBOSE statement is supported"
+ err_msg: "only a single EXPLAIN ANALYZE VERBOSE or TQL ANALYZE VERBOSE statement is supported"
}
);
let mut stmt = stmts.remove(0);
@@ -1877,6 +1883,9 @@ mod tests {
"explain analyze select 1",
"explain analyze verbose format text select 1",
"explain analyze verbose format graphviz select 1",
+ "TQL ANALYZE (0, 10, '5s') physical_metric",
+ "TQL EXPLAIN VERBOSE (0, 10, '5s') physical_metric",
+ "TQL ANALYZE VERBOSE FORMAT TEXT (0, 10, '5s') physical_metric",
] {
let mut stmts = parse_test_sql(sql);
assert!(
@@ -1888,16 +1897,19 @@ mod tests {
for sql in [
"explain analyze verbose select 1",
"explain analyze verbose format json select 1",
+ "TQL ANALYZE VERBOSE (0, 10, '5s') physical_metric",
+ "TQL ANALYZE VERBOSE FORMAT JSON (0, 10, '5s') physical_metric",
] {
let mut stmts = parse_test_sql(sql);
assert!(
validate_analyze_stream_statement(&mut stmts[0]).is_ok(),
"{sql}"
);
- let Statement::Explain(explain) = &stmts[0] else {
- unreachable!();
- };
- assert!(explain.format.is_none());
+ match &stmts[0] {
+ Statement::Explain(explain) => assert!(explain.format.is_none()),
+ Statement::Tql(Tql::Analyze(analyze)) => assert!(analyze.format.is_none()),
+ _ => unreachable!(),
+ }
}
assert_eq!(
@@ -1908,11 +1920,16 @@ mod tests {
assert!(is_explain_analyze_verbose(
&parse_test_sql("explain analyze verbose select 1")[0]
));
+ assert!(is_explain_analyze_verbose(
+ &parse_test_sql("TQL ANALYZE VERBOSE (0, 10, '5s') physical_metric")[0]
+ ));
for sql in [
"select 1",
"explain select 1",
"explain analyze select 1",
"explain verbose select 1",
+ "TQL ANALYZE (0, 10, '5s') physical_metric",
+ "TQL EXPLAIN VERBOSE (0, 10, '5s') physical_metric",
] {
assert!(
!is_explain_analyze_verbose(&parse_test_sql(sql)[0]),
diff --git a/src/query/src/analyze.rs b/src/query/src/analyze.rs
index 2ab3ec5176..dec73e4093 100644
--- a/src/query/src/analyze.rs
+++ b/src/query/src/analyze.rs
@@ -332,6 +332,8 @@ struct JsonMetrics {
// other metrics
metrics: HashMap,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ memory_usage: Option,
children: Vec,
}
@@ -339,12 +341,14 @@ impl JsonMetrics {
fn from_record_batch_metrics(record_batch_metrics: RecordBatchMetrics) -> Self {
let mut layers: HashMap> = HashMap::default();
+ let memory_usage = record_batch_metrics.memory_usage;
for plan_metrics in record_batch_metrics.plan_metrics.into_iter().rev() {
let (level, mut metrics) = Self::from_plan_metrics(plan_metrics);
if let Some(next_layer) = layers.remove(&(level + 1)) {
metrics.children = next_layer;
}
if level == 0 {
+ metrics.memory_usage = Some(memory_usage);
return metrics;
}
layers.entry(level).or_default().push(metrics);
@@ -382,6 +386,7 @@ impl JsonMetrics {
output_rows,
elapsed_compute,
metrics: other_metrics,
+ memory_usage: None,
children: vec![],
},
)
@@ -498,4 +503,32 @@ mod tests {
assert_eq!(metrics.name, "FilterExec");
assert_eq!(metrics.param, "predicate");
}
+
+ #[test]
+ fn qbs_analyze_json_includes_memory_usage_only_at_root() {
+ let metrics = JsonMetrics::from_record_batch_metrics(RecordBatchMetrics {
+ memory_usage: 42,
+ plan_metrics: vec![
+ PlanMetrics {
+ plan: "RootExec".to_string(),
+ plan_name: "RootExec".to_string(),
+ level: 0,
+ metrics: vec![("mem_used".to_string(), 24)],
+ },
+ PlanMetrics {
+ plan: "ChildExec".to_string(),
+ plan_name: "ChildExec".to_string(),
+ level: 1,
+ metrics: vec![("mem_used".to_string(), 18)],
+ },
+ ],
+ ..Default::default()
+ });
+ let value = serde_json::to_value(metrics).unwrap();
+
+ assert_eq!(value["memory_usage"], 42);
+ assert!(value["children"][0].get("memory_usage").is_none());
+ assert_eq!(value["metrics"]["mem_used"], 24);
+ assert_eq!(value["children"][0]["metrics"]["mem_used"], 18);
+ }
}
diff --git a/src/query/src/dist_plan/merge_scan.rs b/src/query/src/dist_plan/merge_scan.rs
index 8d3580ea45..c91fa6b402 100644
--- a/src/query/src/dist_plan/merge_scan.rs
+++ b/src/query/src/dist_plan/merge_scan.rs
@@ -754,6 +754,19 @@ impl MergeScanExec {
stream.next().instrument(region_span.clone()).await
};
let Some(batch) = batch else {
+ // The remote Flight stream publishes its terminal metrics
+ // immediately after EOF. Capture them before leaving the
+ // loop so the final verbose snapshot is not lost.
+ if let Some(metrics) = stream.metrics() {
+ let load = region_scan_load(&metrics);
+ let (c, s) = parse_catalog_and_schema_from_db_string(&dbname);
+ let value = read_meter!(c, s, load, current_channel as u8);
+ metric.record_greptime_exec_cost(value as usize);
+ sub_stage_metrics_moved
+ .lock()
+ .unwrap()
+ .insert(region_id, metrics);
+ }
break;
};
let poll_elapsed = poll_timer.elapsed();
@@ -823,18 +836,6 @@ impl MergeScanExec {
);
}
- // process metrics after all data is drained.
- if let Some(metrics) = stream.metrics() {
- let load = region_scan_load(&metrics);
- let (c, s) = parse_catalog_and_schema_from_db_string(&dbname);
- let value = read_meter!(c, s, load, current_channel as u8);
- metric.record_greptime_exec_cost(value as usize);
-
- // record metrics from sub sgates
- let mut sub_stage_metrics = sub_stage_metrics_moved.lock().unwrap();
- sub_stage_metrics.insert(region_id, metrics);
- }
-
MERGE_SCAN_POLL_ELAPSED.observe(poll_duration.as_secs_f64());
}
diff --git a/src/servers/src/grpc/flight/stream.rs b/src/servers/src/grpc/flight/stream.rs
index b740f12d02..6d8b458a1d 100644
--- a/src/servers/src/grpc/flight/stream.rs
+++ b/src/servers/src/grpc/flight/stream.rs
@@ -700,6 +700,17 @@ mod test {
}
other => panic!("expected record batch after pending metrics, got {other:?}"),
}
+
+ drop(tx);
+ let final_metrics_data = tokio::time::timeout(Duration::from_secs(2), stream.next())
+ .await
+ .unwrap()
+ .unwrap()
+ .unwrap();
+ assert!(matches!(
+ decoder.try_decode(&final_metrics_data).unwrap().unwrap(),
+ FlightMessage::Metrics(_)
+ ));
}
#[tokio::test]
diff --git a/src/servers/src/http.rs b/src/servers/src/http.rs
index b82ad4633a..311ed48d91 100644
--- a/src/servers/src/http.rs
+++ b/src/servers/src/http.rs
@@ -253,8 +253,6 @@ pub struct HttpOptions {
pub enable_cors: bool,
- pub experimental_enable_explain_analyze_stream: bool,
-
/// Whether to start the dedicated public HTTP **API** server, which serves
/// only the `v1` interfaces plus the dashboard. It shares every other
/// `[http]` option with the main server and only differs by its bound
@@ -274,7 +272,6 @@ impl Default for HttpOptions {
body_limit: DEFAULT_BODY_LIMIT,
cors_allowed_origins: Vec::new(),
enable_cors: true,
- experimental_enable_explain_analyze_stream: true,
enable_api_server: false,
api_server_addr: format!("127.0.0.1:{}", DEFAULT_HTTP_API_ADDR_PORT),
}
@@ -601,7 +598,6 @@ impl From for HttpResponse {
#[derive(Clone)]
pub struct ApiState {
pub sql_handler: ServerSqlQueryHandlerRef,
- pub experimental_enable_explain_analyze_stream: bool,
}
#[derive(Clone)]
@@ -638,12 +634,7 @@ impl HttpServerBuilder {
}
pub fn with_sql_handler(self, sql_handler: ServerSqlQueryHandlerRef) -> Self {
- let sql_router = HttpServer::route_sql(ApiState {
- sql_handler,
- experimental_enable_explain_analyze_stream: self
- .options
- .experimental_enable_explain_analyze_stream,
- });
+ let sql_router = HttpServer::route_sql(ApiState { sql_handler });
Self {
router: self
@@ -1315,7 +1306,7 @@ impl HttpServer {
}
fn route_sql(api_state: ApiState) -> Router {
- let mut router = Router::new()
+ Router::new()
.route("/sql", routing::get(handler::sql).post(handler::sql))
.route(
"/sql/parse",
@@ -1328,16 +1319,12 @@ impl HttpServer {
.route(
"/promql",
routing::get(handler::promql).post(handler::promql),
- );
-
- if api_state.experimental_enable_explain_analyze_stream {
- router = router.route(
+ )
+ .route(
"/sql/analyze/stream",
routing::post(handler::sql_analyze_stream),
- );
- }
-
- router.with_state(api_state)
+ )
+ .with_state(api_state)
}
fn route_logs(log_handler: LogQueryHandlerRef) -> Router {
@@ -1652,31 +1639,6 @@ mod test {
server.build(app).unwrap()
}
- #[tokio::test]
- pub async fn test_analyze_stream_route_config_gate() {
- let (tx, _rx) = mpsc::channel(100);
- let options = HttpOptions {
- experimental_enable_explain_analyze_stream: false,
- ..Default::default()
- };
- let app = make_test_app_custom(tx, options);
- let client = TestClient::new(app).await;
- let res = client
- .post("/v1/sql/analyze/stream?sql=EXPLAIN%20ANALYZE%20VERBOSE%20SELECT%201")
- .send()
- .await;
- assert_eq!(res.status(), StatusCode::NOT_FOUND);
-
- let (tx, _rx) = mpsc::channel(100);
- let app = make_test_app_custom(tx, HttpOptions::default());
- let client = TestClient::new(app).await;
- let res = client
- .post("/v1/sql/analyze/stream?sql=EXPLAIN%20ANALYZE%20VERBOSE%20SELECT%201")
- .send()
- .await;
- assert_ne!(res.status(), StatusCode::NOT_FOUND);
- }
-
fn make_split_builder() -> HttpServerBuilder {
let (tx, _rx) = mpsc::channel(100);
let instance = Arc::new(DummyInstance { _tx: tx });
@@ -1987,6 +1949,36 @@ mod test {
assert_eq!(Duration::from_secs(0), default.timeout)
}
+ #[tokio::test]
+ async fn test_http_options_legacy_analyze_stream_config_is_ignored() {
+ let options: HttpOptions = serde_json::from_value(serde_json::json!({
+ "addr": "127.0.0.1:4000",
+ "timeout": "0s",
+ "body_limit": "64MiB",
+ "cors_allowed_origins": [],
+ "enable_cors": true,
+ "experimental_enable_explain_analyze_stream": false,
+ "enable_api_server": false,
+ "api_server_addr": "127.0.0.1:4006"
+ }))
+ .unwrap();
+ let serialized = serde_json::to_string(&options).unwrap();
+ assert!(!serialized.contains("experimental_enable_explain_analyze_stream"));
+
+ let (tx, _rx) = mpsc::channel(100);
+ let app = make_test_app_custom(tx, options);
+ let client = TestClient::new(app).await;
+ let response = client
+ .post("/v1/sql/analyze/stream")
+ .form(&handler::SqlQuery {
+ sql: Some("EXPLAIN ANALYZE VERBOSE SELECT 1".to_string()),
+ ..Default::default()
+ })
+ .send()
+ .await;
+ assert_ne!(response.status(), StatusCode::NOT_FOUND);
+ }
+
#[tokio::test]
async fn test_http_server_request_timeout() {
common_telemetry::init_default_ut_logging();
diff --git a/src/servers/src/http/handler.rs b/src/servers/src/http/handler.rs
index f92f2413c7..ee2c3fd414 100644
--- a/src/servers/src/http/handler.rs
+++ b/src/servers/src/http/handler.rs
@@ -13,7 +13,9 @@
// limitations under the License.
use std::collections::HashMap;
+use std::panic::AssertUnwindSafe;
use std::sync::Arc;
+use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant};
use axum::extract::rejection::FormRejection;
@@ -26,11 +28,10 @@ use common_error::ext::ErrorExt;
use common_error::status_code::StatusCode;
use common_plugins::GREPTIME_EXEC_WRITE_COST;
use common_query::{Output, OutputData};
-use common_recordbatch::{RecordBatch, SendableRecordBatchStream, util};
+use common_recordbatch::util;
use common_telemetry::tracing;
use datafusion::physical_plan::ExecutionPlan;
-use datatypes::schema::SchemaRef;
-use futures::StreamExt;
+use futures::{FutureExt, StreamExt};
use query::parser::{DEFAULT_LOOKBACK_STRING, PromQuery};
use serde::{Deserialize, Serialize};
use serde_json::Value;
@@ -39,6 +40,7 @@ use snafu::ResultExt;
use sql::dialect::GreptimeDbDialect;
use sql::parser::{ParseOptions, ParserContext};
use sql::statements::statement::Statement;
+use tokio::sync::{Notify, watch};
use crate::error::{FailedToParseQuerySnafu, InvalidQuerySnafu, Result};
use crate::http::header::collect_plan_metrics;
@@ -100,15 +102,32 @@ struct AnalyzeStreamPayload {
code: Option,
}
-struct AnalyzeStreamState {
- stream: SendableRecordBatchStream,
- schema: SchemaRef,
- plan: Option>,
- batches: Vec,
- seq: u64,
- start: Instant,
- requested_interval_ms: u64,
- current_interval_ms: u64,
+#[derive(Clone, Debug)]
+#[doc(hidden)]
+pub struct AnalyzeStreamMessage {
+ pub event_name: &'static str,
+ pub payload: String,
+}
+
+struct AnalyzeStreamWorkerGuard {
+ cancel: watch::Sender,
+ handle: common_runtime::JoinHandle<()>,
+}
+
+impl Drop for AnalyzeStreamWorkerGuard {
+ fn drop(&mut self) {
+ let _ = self.cancel.send(true);
+ self.handle.abort();
+ }
+}
+
+struct AnalyzeStreamBodyState {
+ latest_metrics: watch::Receiver