diff --git a/config/config.md b/config/config.md
index a8d12c3557e..785d86e69df 100644
--- a/config/config.md
+++ b/config/config.md
@@ -33,7 +33,7 @@
| `runtime.experimental_workload_scheduler.sample_every_polls` | Integer | `16` | Number of polls between scheduler fairness samples. Must be greater than zero. |
| `http` | -- | -- | The HTTP server options. |
| `http.addr` | String | `127.0.0.1:4000` | The address to bind the HTTP server. |
-| `http.timeout` | String | `0s` | HTTP request timeout. Set to 0 to disable timeout.
When Prometheus pending-row batching is enabled, a nonzero timeout less than or equal to the
`prom_store.pending_rows_flush_interval` plus 1 second is adjusted to that value. |
+| `http.timeout` | String | `0s` | HTTP request timeout. Set to 0 to disable timeout.
When synchronous pending-row batching serves an enabled Prometheus or OTLP HTTP endpoint,
a nonzero timeout less than or equal to `prom_store.pending_rows_flush_interval` plus 1 second
is adjusted to that value. |
| `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. |
@@ -78,6 +78,7 @@
| `jaeger.enable` | Bool | `true` | Whether to enable Jaeger protocol in HTTP API. |
| `otlp` | -- | -- | OpenTelemetry protocol options. |
| `otlp.enable` | Bool | `true` | Whether to enable OpenTelemetry protocol in HTTP API. |
+| `otlp.enable_metrics_batching` | Bool | `false` | Enable pending-row batching for eligible OTLP metrics over HTTP and OTel Arrow.
Disabled by default. Requires Metric Engine, valid nonzero `prom_store.pending_rows_*` tuning,
and `PENDING_ROWS_BATCH_SYNC=true`; otherwise requests use the original Inserter path.
Legacy OTLP metric tables and ineligible converted request shapes, such as native histograms or
mixed scalar/histogram requests, fall back to the Inserter.
This startup option requires a restart. Before disabling it, drain pending rows and inflight
requests. A failed multi-region flush may partially write rows, so retries can create duplicates. |
| `otlp.experimental_enable_exponential_histogram` | Bool | `false` | Experimental: enable cumulative OTLP exponential histogram ingestion. |
| `otlp.trace_ingest_chunk_size` | Integer | `512` | Maximum spans per trace ingest chunk. Set to 0 to disable splitting. |
| `otlp.experimental_enable_resource_info` | Bool | `false` | Whether to synthesize the `greptime_otel_resource_info` table from OTLP metric
resource attributes, so metrics-only services reach the semantic graph. |
@@ -86,7 +87,7 @@
| `prom_store.with_metric_engine` | Bool | `true` | Whether to store the data from Prometheus remote write in metric engine. |
| `prom_store.prom_validation_mode` | String | `strict` | Whether to enable validation for Prometheus remote write requests.
Available options:
- strict: deny invalid UTF-8 strings (default).
- lossy: allow invalid UTF-8 strings, replace invalid characters with REPLACEMENT_CHARACTER(U+FFFD).
- unchecked: do not valid strings. |
| `prom_store.experimental_enable_prometheus_native_histogram` | Bool | `false` | Experimental: enable Prometheus remote write v2 native histogram ingestion. |
-| `prom_store.pending_rows_flush_interval` | String | `0s` | Interval to flush pending rows batcher.
Set to "0s" to disable batching mode in Prometheus Remote Write endpoint |
+| `prom_store.pending_rows_flush_interval` | String | `0s` | Interval to flush pending rows batcher.
Set to "0s" to disable batching for Prometheus Remote Write and force OTLP to use its Inserter. |
| `prom_store.max_batch_rows` | Integer | `100000` | Max rows per pending batch before triggering a flush. |
| `prom_store.max_concurrent_flushes` | Integer | `256` | Max number of concurrent batch flushes. |
| `prom_store.worker_channel_capacity` | Integer | `65526` | Capacity of the pending batch worker channel. |
@@ -272,7 +273,7 @@
| `runtime.compact_rt_max_blocking_threads` | Integer | `4` | The maximum number of blocking threads for compact operations.
Defaults to max(num_cpus / 2, 2). |
| `http` | -- | -- | The HTTP server options. |
| `http.addr` | String | `127.0.0.1:4000` | The address to bind the HTTP server. |
-| `http.timeout` | String | `0s` | HTTP request timeout. Set to 0 to disable timeout.
When Prometheus pending-row batching is enabled, a nonzero timeout less than or equal to the
`prom_store.pending_rows_flush_interval` plus 1 second is adjusted to that value. |
+| `http.timeout` | String | `0s` | HTTP request timeout. Set to 0 to disable timeout.
When synchronous pending-row batching serves an enabled Prometheus or OTLP HTTP endpoint,
a nonzero timeout less than or equal to `prom_store.pending_rows_flush_interval` plus 1 second
is adjusted to that value. |
| `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. |
@@ -329,6 +330,7 @@
| `jaeger.enable` | Bool | `true` | Whether to enable Jaeger protocol in HTTP API. |
| `otlp` | -- | -- | OpenTelemetry protocol options. |
| `otlp.enable` | Bool | `true` | Whether to enable OpenTelemetry protocol in HTTP API. |
+| `otlp.enable_metrics_batching` | Bool | `false` | Enable pending-row batching for eligible OTLP metrics over HTTP and OTel Arrow.
Disabled by default. Requires Metric Engine, valid nonzero `prom_store.pending_rows_*` tuning,
and `PENDING_ROWS_BATCH_SYNC=true`; otherwise requests use the original Inserter path.
Legacy OTLP metric tables and ineligible converted request shapes, such as native histograms or
mixed scalar/histogram requests, fall back to the Inserter.
This startup option requires a restart. Before disabling it, drain pending rows and inflight
requests. A failed multi-region flush may partially write rows, so retries can create duplicates. |
| `otlp.experimental_enable_exponential_histogram` | Bool | `false` | Experimental: enable cumulative OTLP exponential histogram ingestion. |
| `otlp.trace_ingest_chunk_size` | Integer | `512` | Maximum spans per trace ingest chunk. Set to 0 to disable splitting. |
| `otlp.experimental_enable_resource_info` | Bool | `false` | Whether to synthesize the `greptime_otel_resource_info` table from OTLP metric
resource attributes, so metrics-only services reach the semantic graph. |
@@ -337,7 +339,7 @@
| `prom_store.with_metric_engine` | Bool | `true` | Whether to store the data from Prometheus remote write in metric engine. |
| `prom_store.prom_validation_mode` | String | `strict` | Whether to enable validation for Prometheus remote write requests.
Available options:
- strict: deny invalid UTF-8 strings (default).
- lossy: allow invalid UTF-8 strings, replace invalid characters with REPLACEMENT_CHARACTER(U+FFFD).
- unchecked: do not valid strings. |
| `prom_store.experimental_enable_prometheus_native_histogram` | Bool | `false` | Experimental: enable Prometheus remote write v2 native histogram ingestion. |
-| `prom_store.pending_rows_flush_interval` | String | `0s` | Interval to flush pending rows batcher.
Set to "0s" to disable batching mode in Prometheus Remote Write endpoint |
+| `prom_store.pending_rows_flush_interval` | String | `0s` | Interval to flush pending rows batcher.
Set to "0s" to disable batching for Prometheus Remote Write and force OTLP to use its Inserter. |
| `prom_store.max_batch_rows` | Integer | `100000` | Max rows per pending batch before triggering a flush. |
| `prom_store.max_concurrent_flushes` | Integer | `256` | Max number of concurrent batch flushes. |
| `prom_store.worker_channel_capacity` | Integer | `65526` | Capacity of the pending batch worker channel. |
diff --git a/config/frontend.example.toml b/config/frontend.example.toml
index 7c6862916e4..80e18e7831d 100644
--- a/config/frontend.example.toml
+++ b/config/frontend.example.toml
@@ -52,8 +52,9 @@ default_column_prefix = "greptime"
## The address to bind the HTTP server.
addr = "127.0.0.1:4000"
## HTTP request timeout. Set to 0 to disable timeout.
-## When Prometheus pending-row batching is enabled, a nonzero timeout less than or equal to the
-## `prom_store.pending_rows_flush_interval` plus 1 second is adjusted to that value.
+## When synchronous pending-row batching serves an enabled Prometheus or OTLP HTTP endpoint,
+## a nonzero timeout less than or equal to `prom_store.pending_rows_flush_interval` plus 1 second
+## is adjusted to that value.
timeout = "0s"
## HTTP request body limit.
## The following units are supported: `B`, `KB`, `KiB`, `MB`, `MiB`, `GB`, `GiB`, `TB`, `TiB`, `PB`, `PiB`.
@@ -235,6 +236,14 @@ enable = true
[otlp]
## Whether to enable OpenTelemetry protocol in HTTP API.
enable = true
+## Enable pending-row batching for eligible OTLP metrics over HTTP and OTel Arrow.
+## Disabled by default. Requires Metric Engine, valid nonzero `prom_store.pending_rows_*` tuning,
+## and `PENDING_ROWS_BATCH_SYNC=true`; otherwise requests use the original Inserter path.
+## Legacy OTLP metric tables and ineligible converted request shapes, such as native histograms or
+## mixed scalar/histogram requests, fall back to the Inserter.
+## This startup option requires a restart. Before disabling it, drain pending rows and inflight
+## requests. A failed multi-region flush may partially write rows, so retries can create duplicates.
+enable_metrics_batching = false
## Experimental: enable cumulative OTLP exponential histogram ingestion.
experimental_enable_exponential_histogram = false
## Maximum spans per trace ingest chunk. Set to 0 to disable splitting.
@@ -258,7 +267,7 @@ prom_validation_mode = "strict"
## Experimental: enable Prometheus remote write v2 native histogram ingestion.
experimental_enable_prometheus_native_histogram = false
## Interval to flush pending rows batcher.
-## Set to "0s" to disable batching mode in Prometheus Remote Write endpoint
+## Set to "0s" to disable batching for Prometheus Remote Write and force OTLP to use its Inserter.
#+pending_rows_flush_interval = "0s"
## Max rows per pending batch before triggering a flush.
#+max_batch_rows = 100000
diff --git a/config/standalone.example.toml b/config/standalone.example.toml
index b05cf0177d0..992c3297d4c 100644
--- a/config/standalone.example.toml
+++ b/config/standalone.example.toml
@@ -77,8 +77,9 @@ max_concurrent_queries = 0
## The address to bind the HTTP server.
addr = "127.0.0.1:4000"
## HTTP request timeout. Set to 0 to disable timeout.
-## When Prometheus pending-row batching is enabled, a nonzero timeout less than or equal to the
-## `prom_store.pending_rows_flush_interval` plus 1 second is adjusted to that value.
+## When synchronous pending-row batching serves an enabled Prometheus or OTLP HTTP endpoint,
+## a nonzero timeout less than or equal to `prom_store.pending_rows_flush_interval` plus 1 second
+## is adjusted to that value.
timeout = "0s"
## HTTP request body limit.
## The following units are supported: `B`, `KB`, `KiB`, `MB`, `MiB`, `GB`, `GiB`, `TB`, `TiB`, `PB`, `PiB`.
@@ -214,6 +215,14 @@ enable = true
[otlp]
## Whether to enable OpenTelemetry protocol in HTTP API.
enable = true
+## Enable pending-row batching for eligible OTLP metrics over HTTP and OTel Arrow.
+## Disabled by default. Requires Metric Engine, valid nonzero `prom_store.pending_rows_*` tuning,
+## and `PENDING_ROWS_BATCH_SYNC=true`; otherwise requests use the original Inserter path.
+## Legacy OTLP metric tables and ineligible converted request shapes, such as native histograms or
+## mixed scalar/histogram requests, fall back to the Inserter.
+## This startup option requires a restart. Before disabling it, drain pending rows and inflight
+## requests. A failed multi-region flush may partially write rows, so retries can create duplicates.
+enable_metrics_batching = false
## Experimental: enable cumulative OTLP exponential histogram ingestion.
experimental_enable_exponential_histogram = false
## Maximum spans per trace ingest chunk. Set to 0 to disable splitting.
@@ -237,7 +246,7 @@ prom_validation_mode = "strict"
## Experimental: enable Prometheus remote write v2 native histogram ingestion.
experimental_enable_prometheus_native_histogram = false
## Interval to flush pending rows batcher.
-## Set to "0s" to disable batching mode in Prometheus Remote Write endpoint
+## Set to "0s" to disable batching for Prometheus Remote Write and force OTLP to use its Inserter.
#+pending_rows_flush_interval = "0s"
## Max rows per pending batch before triggering a flush.
#+max_batch_rows = 100000
diff --git a/grafana/dashboards/metrics/cluster/dashboard.json b/grafana/dashboards/metrics/cluster/dashboard.json
index bd22a0da597..e20a23032a0 100644
--- a/grafana/dashboards/metrics/cluster/dashboard.json
+++ b/grafana/dashboards/metrics/cluster/dashboard.json
@@ -4362,7 +4362,7 @@
"type": "prometheus",
"uid": "${metrics}"
},
- "description": "Prometheus remote-write pending rows, batches, and workers. Rising pending rows indicate remote-write buffering backlog.",
+ "description": "Pending metric rows, batches, and workers. Rising pending rows indicate metric-row batching backlog.",
"fieldConfig": {
"defaults": {
"color": {
@@ -4482,7 +4482,7 @@
"refId": "C"
}
],
- "title": "Prom Store Backlog",
+ "title": "Metric Row Batching Backlog",
"type": "timeseries"
},
{
diff --git a/grafana/dashboards/metrics/cluster/dashboard.md b/grafana/dashboards/metrics/cluster/dashboard.md
index b8e050114c6..b04b43b3d3b 100644
--- a/grafana/dashboards/metrics/cluster/dashboard.md
+++ b/grafana/dashboards/metrics/cluster/dashboard.md
@@ -46,7 +46,7 @@
| Query Memory Usage | `sum by (instance, pod) (greptime_query_memory_pool_usage_bytes)` | `timeseries` | Query memory pool usage. Use this with query memory rejection panels to diagnose query saturation. | `prometheus` | `bytes` | `[{{instance}}]-[{{pod}}]` |
| Scan and Compaction Memory | `sum by (instance, pod) (greptime_mito_scan_memory_usage_bytes{instance=~"$datanode"})`
`sum by (instance, pod) (greptime_mito_compaction_memory_in_use_bytes{instance=~"$datanode"})`
`sum by (instance, pod) (greptime_mito_compaction_memory_limit_bytes{instance=~"$datanode"})` | `timeseries` | Datanode scan memory usage and compaction memory utilization. | `prometheus` | `bytes` | `scan-[{{instance}}]-[{{pod}}]` |
| Write Buffer and Active Stalling | `sum by (instance, pod) (greptime_mito_write_buffer_bytes{instance=~"$datanode"})`
`sum by (instance, pod) (greptime_mito_write_stalling_count{instance=~"$datanode"})` | `timeseries` | Mito write buffer bytes and active write-stalling gauges. Growth here indicates write-path backpressure. | `prometheus` | `bytes` | `buffer-[{{instance}}]-[{{pod}}]` |
-| Prom Store Backlog | `sum by (instance, pod) (greptime_prom_store_pending_rows{instance=~"$frontend"})`
`sum by (instance, pod) (greptime_prom_store_pending_batches{instance=~"$frontend"})`
`sum by (instance, pod) (greptime_prom_store_pending_workers{instance=~"$frontend"})` | `timeseries` | Prometheus remote-write pending rows, batches, and workers. Rising pending rows indicate remote-write buffering backlog. | `prometheus` | `short` | `rows-[{{instance}}]-[{{pod}}]` |
+| Metric Row Batching Backlog | `sum by (instance, pod) (greptime_prom_store_pending_rows{instance=~"$frontend"})`
`sum by (instance, pod) (greptime_prom_store_pending_batches{instance=~"$frontend"})`
`sum by (instance, pod) (greptime_prom_store_pending_workers{instance=~"$frontend"})` | `timeseries` | Pending metric rows, batches, and workers. Rising pending rows indicate metric-row batching backlog. | `prometheus` | `short` | `rows-[{{instance}}]-[{{pod}}]` |
| Inflight Flush and Compaction | `sum by (instance, pod) (greptime_mito_inflight_flush_count{instance=~"$datanode"})`
`sum by (instance, pod) (greptime_mito_inflight_compaction_count{instance=~"$datanode"})` | `timeseries` | Current in-flight flush and compaction tasks on datanodes. | `prometheus` | `short` | `flush-[{{instance}}]-[{{pod}}]` |
# Resources
| Title | Query | Type | Description | Datasource | Unit | Legend Format |
diff --git a/grafana/dashboards/metrics/cluster/dashboard.yaml b/grafana/dashboards/metrics/cluster/dashboard.yaml
index cd105edfcd2..9c139af4ae7 100644
--- a/grafana/dashboards/metrics/cluster/dashboard.yaml
+++ b/grafana/dashboards/metrics/cluster/dashboard.yaml
@@ -632,9 +632,9 @@ groups:
type: prometheus
uid: ${metrics}
legendFormat: stalling-[{{instance}}]-[{{pod}}]
- - title: Prom Store Backlog
+ - title: Metric Row Batching Backlog
type: timeseries
- description: Prometheus remote-write pending rows, batches, and workers. Rising pending rows indicate remote-write buffering backlog.
+ description: Pending metric rows, batches, and workers. Rising pending rows indicate metric-row batching backlog.
unit: short
queries:
- expr: sum by (instance, pod) (greptime_prom_store_pending_rows{instance=~"$frontend"})
diff --git a/grafana/dashboards/metrics/standalone/dashboard.json b/grafana/dashboards/metrics/standalone/dashboard.json
index a90d05760e4..e6410fc780d 100644
--- a/grafana/dashboards/metrics/standalone/dashboard.json
+++ b/grafana/dashboards/metrics/standalone/dashboard.json
@@ -4362,7 +4362,7 @@
"type": "prometheus",
"uid": "${metrics}"
},
- "description": "Prometheus remote-write pending rows, batches, and workers. Rising pending rows indicate remote-write buffering backlog.",
+ "description": "Pending metric rows, batches, and workers. Rising pending rows indicate metric-row batching backlog.",
"fieldConfig": {
"defaults": {
"color": {
@@ -4482,7 +4482,7 @@
"refId": "C"
}
],
- "title": "Prom Store Backlog",
+ "title": "Metric Row Batching Backlog",
"type": "timeseries"
},
{
diff --git a/grafana/dashboards/metrics/standalone/dashboard.md b/grafana/dashboards/metrics/standalone/dashboard.md
index 2ed84c59213..1187c2ff958 100644
--- a/grafana/dashboards/metrics/standalone/dashboard.md
+++ b/grafana/dashboards/metrics/standalone/dashboard.md
@@ -46,7 +46,7 @@
| Query Memory Usage | `sum by (instance, pod) (greptime_query_memory_pool_usage_bytes)` | `timeseries` | Query memory pool usage. Use this with query memory rejection panels to diagnose query saturation. | `prometheus` | `bytes` | `[{{instance}}]-[{{pod}}]` |
| Scan and Compaction Memory | `sum by (instance, pod) (greptime_mito_scan_memory_usage_bytes)`
`sum by (instance, pod) (greptime_mito_compaction_memory_in_use_bytes)`
`sum by (instance, pod) (greptime_mito_compaction_memory_limit_bytes)` | `timeseries` | Datanode scan memory usage and compaction memory utilization. | `prometheus` | `bytes` | `scan-[{{instance}}]-[{{pod}}]` |
| Write Buffer and Active Stalling | `sum by (instance, pod) (greptime_mito_write_buffer_bytes)`
`sum by (instance, pod) (greptime_mito_write_stalling_count)` | `timeseries` | Mito write buffer bytes and active write-stalling gauges. Growth here indicates write-path backpressure. | `prometheus` | `bytes` | `buffer-[{{instance}}]-[{{pod}}]` |
-| Prom Store Backlog | `sum by (instance, pod) (greptime_prom_store_pending_rows)`
`sum by (instance, pod) (greptime_prom_store_pending_batches)`
`sum by (instance, pod) (greptime_prom_store_pending_workers)` | `timeseries` | Prometheus remote-write pending rows, batches, and workers. Rising pending rows indicate remote-write buffering backlog. | `prometheus` | `short` | `rows-[{{instance}}]-[{{pod}}]` |
+| Metric Row Batching Backlog | `sum by (instance, pod) (greptime_prom_store_pending_rows)`
`sum by (instance, pod) (greptime_prom_store_pending_batches)`
`sum by (instance, pod) (greptime_prom_store_pending_workers)` | `timeseries` | Pending metric rows, batches, and workers. Rising pending rows indicate metric-row batching backlog. | `prometheus` | `short` | `rows-[{{instance}}]-[{{pod}}]` |
| Inflight Flush and Compaction | `sum by (instance, pod) (greptime_mito_inflight_flush_count)`
`sum by (instance, pod) (greptime_mito_inflight_compaction_count)` | `timeseries` | Current in-flight flush and compaction tasks on datanodes. | `prometheus` | `short` | `flush-[{{instance}}]-[{{pod}}]` |
# Resources
| Title | Query | Type | Description | Datasource | Unit | Legend Format |
diff --git a/grafana/dashboards/metrics/standalone/dashboard.yaml b/grafana/dashboards/metrics/standalone/dashboard.yaml
index 8da2a8fa6f1..437a34e4674 100644
--- a/grafana/dashboards/metrics/standalone/dashboard.yaml
+++ b/grafana/dashboards/metrics/standalone/dashboard.yaml
@@ -632,9 +632,9 @@ groups:
type: prometheus
uid: ${metrics}
legendFormat: stalling-[{{instance}}]-[{{pod}}]
- - title: Prom Store Backlog
+ - title: Metric Row Batching Backlog
type: timeseries
- description: Prometheus remote-write pending rows, batches, and workers. Rising pending rows indicate remote-write buffering backlog.
+ description: Pending metric rows, batches, and workers. Rising pending rows indicate metric-row batching backlog.
unit: short
queries:
- expr: sum by (instance, pod) (greptime_prom_store_pending_rows)
diff --git a/src/cmd/src/standalone.rs b/src/cmd/src/standalone.rs
index 6c5ac25f641..a3422abd0c4 100644
--- a/src/cmd/src/standalone.rs
+++ b/src/cmd/src/standalone.rs
@@ -1155,6 +1155,7 @@ mod tests {
fn test_toml() {
let opts = StandaloneOptions::default();
let toml_string = toml::to_string(&opts).unwrap();
+ assert!(toml_string.contains("enable_metrics_batching = false"));
assert!(toml_string.contains("experimental_enable_exponential_histogram = false"));
let parsed: StandaloneOptions = toml::from_str(&toml_string).unwrap();
assert_eq!(parsed.otlp, opts.otlp);
diff --git a/src/frontend/src/frontend.rs b/src/frontend/src/frontend.rs
index bb15abd7bbf..649e3585f0c 100644
--- a/src/frontend/src/frontend.rs
+++ b/src/frontend/src/frontend.rs
@@ -222,6 +222,7 @@ mod tests {
fn test_toml() {
let opts = FrontendOptions::default();
let toml_string = toml::to_string(&opts).unwrap();
+ assert!(toml_string.contains("enable_metrics_batching = false"));
assert!(toml_string.contains("experimental_enable_exponential_histogram = false"));
let parsed: FrontendOptions = toml::from_str(&toml_string).unwrap();
assert_eq!(parsed.otlp, opts.otlp);
diff --git a/src/frontend/src/instance/otlp.rs b/src/frontend/src/instance/otlp.rs
index 4c80f1d17f2..eb1dda480db 100644
--- a/src/frontend/src/instance/otlp.rs
+++ b/src/frontend/src/instance/otlp.rs
@@ -35,8 +35,12 @@ use pipeline::{GreptimePipelineParams, PipelineWay};
use servers::error::{self, AuthSnafu, Result as ServerResult};
use servers::http::prom_store::PHYSICAL_TABLE_PARAM;
use servers::interceptor::{OpenTelemetryProtocolInterceptor, OpenTelemetryProtocolInterceptorRef};
+use servers::metrics::METRIC_ROW_BATCH_ELIGIBILITY_FALLBACKS;
use servers::otlp;
use servers::otlp::trace::span::TraceSpanGroup;
+use servers::pending_rows_batcher::{
+ MetricRowBatchProtocol, MetricRowBatcherRef, is_scalar_metric_batchable,
+};
use servers::query_handler::{
MetricsIngestOutcome, OpenTelemetryProtocolHandler, PipelineHandlerRef, TraceIngestOutcome,
};
@@ -89,6 +93,7 @@ impl OpenTelemetryProtocolHandler for Instance {
async fn metrics(
&self,
request: ExportMetricsServiceRequest,
+ metric_row_batcher: Option,
ctx: QueryContextRef,
) -> ServerResult {
self.plugins
@@ -160,23 +165,40 @@ impl OpenTelemetryProtocolHandler for Instance {
Arc::new(c)
};
- // OTLP tables have one sample field in both the legacy and physical paths.
- let output = if metric_ctx.is_legacy || !metric_ctx.with_metric_engine {
- self.handle_row_inserts(requests, ctx.clone(), false, true)
- .await
- .map_err(BoxedError::new)
- .context(error::ExecuteGrpcQuerySnafu)
- } else {
- let physical_table = ctx
- .extension(PHYSICAL_TABLE_PARAM)
- .unwrap_or(GREPTIME_PHYSICAL_TABLE)
- .to_string();
- self.handle_metric_row_inserts(requests, ctx.clone(), physical_table)
- .await
- .map_err(BoxedError::new)
- .context(error::ExecuteGrpcQuerySnafu)
- }?;
- outcome.write_cost = output.meta.cost;
+ let batching_candidate =
+ !metric_ctx.is_legacy && metric_ctx.with_metric_engine && metric_row_batcher.is_some();
+ let batchable = batching_candidate && is_scalar_metric_batchable(&requests)?;
+ let write_cost =
+ if let Some(metric_row_batcher) = metric_row_batcher.as_ref().filter(|_| batchable) {
+ metric_row_batcher
+ .submit(requests, ctx.clone(), MetricRowBatchProtocol::Otlp)
+ .await?;
+ 0
+ } else {
+ if batching_candidate {
+ METRIC_ROW_BATCH_ELIGIBILITY_FALLBACKS
+ .with_label_values(&[MetricRowBatchProtocol::Otlp.as_str()])
+ .inc();
+ }
+ // OTLP tables have one sample field in both the legacy and physical paths.
+ let output = if metric_ctx.is_legacy || !metric_ctx.with_metric_engine {
+ self.handle_row_inserts(requests, ctx.clone(), false, true)
+ .await
+ .map_err(BoxedError::new)
+ .context(error::ExecuteGrpcQuerySnafu)
+ } else {
+ let physical_table = ctx
+ .extension(PHYSICAL_TABLE_PARAM)
+ .unwrap_or(GREPTIME_PHYSICAL_TABLE)
+ .to_string();
+ self.handle_metric_row_inserts(requests, ctx.clone(), physical_table)
+ .await
+ .map_err(BoxedError::new)
+ .context(error::ExecuteGrpcQuerySnafu)
+ }?;
+ output.meta.cost
+ };
+ outcome.write_cost = write_cost;
// Derived enrichment, written after the metric data is committed:
// failing here would make the client retry data the server already
@@ -321,16 +343,334 @@ impl OpenTelemetryProtocolHandler for Instance {
#[cfg(test)]
mod tests {
- use std::sync::Arc;
+ use std::sync::atomic::{AtomicUsize, Ordering};
+ use std::sync::{Arc, Mutex};
- use auth::{PermissionTableTarget, PermissionTableTargets};
+ use api::v1::RowInsertRequests;
+ use api::v1::meta::Role;
+ use async_trait::async_trait;
+ use auth::{
+ PermissionChecker, PermissionCheckerRef, PermissionReq, PermissionResp,
+ PermissionTableTarget, PermissionTableTargets, UserInfoRef,
+ };
+ use common_base::Plugins;
+ use common_catalog::consts::{DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME};
+ use common_error::ext::ErrorExt;
+ use meta_client::client::MetaClientBuilder;
use opentelemetry_proto::tonic::collector::trace::v1::ExportTraceServiceRequest;
use opentelemetry_proto::tonic::common::v1::{AnyValue, KeyValue, any_value};
use opentelemetry_proto::tonic::resource::v1::Resource;
use opentelemetry_proto::tonic::trace::v1::{ResourceSpans, ScopeSpans, Span};
+ use otel_arrow_rust::proto::opentelemetry::collector::metrics::v1::ExportMetricsServiceRequest;
+ use otel_arrow_rust::proto::opentelemetry::common::v1::{
+ AnyValue as MetricAnyValue, KeyValue as MetricKeyValue, any_value as metric_any_value,
+ };
+ use otel_arrow_rust::proto::opentelemetry::metrics::v1::number_data_point::Value;
+ use otel_arrow_rust::proto::opentelemetry::metrics::v1::{
+ AggregationTemporality, ExponentialHistogram, ExponentialHistogramDataPoint, Gauge, Metric,
+ NumberDataPoint, ResourceMetrics, ScopeMetrics, metric,
+ };
+ use otel_arrow_rust::proto::opentelemetry::resource::v1::Resource as MetricResource;
+ use servers::metrics::METRIC_ROW_BATCH_ELIGIBILITY_FALLBACKS;
+ use servers::pending_rows_batcher::{MetricRowBatchProtocol, MetricRowBatcher};
use session::context::QueryContext;
+ use session::protocol_ctx::{OtlpMetricCtx, ProtocolCtx};
- use super::trace_permission_targets;
+ use super::*;
+ use crate::frontend::FrontendOptions;
+ use crate::instance::builder::FrontendBuilder;
+
+ static FALLBACK_METRIC_ASSERTION_LOCK: tokio::sync::Mutex<()> =
+ tokio::sync::Mutex::const_new(());
+
+ #[derive(Default)]
+ struct FakeMetricRowBatcher {
+ requests: Mutex>,
+ protocols: Mutex>,
+ flush_error: Option<&'static str>,
+ accepted_rows: u64,
+ }
+
+ impl FakeMetricRowBatcher {
+ fn successful(accepted_rows: u64) -> Self {
+ Self {
+ accepted_rows,
+ ..Default::default()
+ }
+ }
+
+ fn failing(message: &'static str) -> Self {
+ Self {
+ flush_error: Some(message),
+ ..Default::default()
+ }
+ }
+ }
+
+ #[async_trait]
+ impl MetricRowBatcher for FakeMetricRowBatcher {
+ async fn submit(
+ &self,
+ requests: RowInsertRequests,
+ _ctx: QueryContextRef,
+ protocol: MetricRowBatchProtocol,
+ ) -> ServerResult {
+ self.requests.lock().unwrap().push(requests);
+ self.protocols.lock().unwrap().push(protocol);
+ if let Some(message) = self.flush_error {
+ return Err(servers::error::InternalSnafu {
+ err_msg: message.to_string(),
+ }
+ .build());
+ }
+ Ok(self.accepted_rows)
+ }
+ }
+
+ async fn test_instance() -> Instance {
+ test_instance_with(FrontendOptions::default(), Plugins::new()).await
+ }
+
+ async fn test_instance_with(options: FrontendOptions, plugins: Plugins) -> Instance {
+ let meta_client = Arc::new(
+ MetaClientBuilder::new(0, Role::Frontend)
+ .enable_procedure()
+ .build(),
+ );
+ FrontendBuilder::new_test(&options, meta_client)
+ .with_plugin(plugins)
+ .try_build()
+ .await
+ .unwrap()
+ }
+
+ #[derive(Default)]
+ struct CountingPermissionChecker {
+ checks: AtomicUsize,
+ }
+
+ impl PermissionChecker for CountingPermissionChecker {
+ fn check_permission(
+ &self,
+ _user_info: UserInfoRef,
+ _req: PermissionReq,
+ ) -> auth::error::Result {
+ self.checks.fetch_add(1, Ordering::Relaxed);
+ Ok(PermissionResp::Allow)
+ }
+
+ fn check_permission_with_table_targets(
+ &self,
+ _user_info: UserInfoRef,
+ _req: PermissionReq,
+ _targets: PermissionTableTargets,
+ ) -> auth::error::Result {
+ self.checks.fetch_add(1, Ordering::Relaxed);
+ Ok(PermissionResp::Allow)
+ }
+ }
+
+ fn metric_query_ctx(with_metric_engine: bool) -> QueryContextRef {
+ let mut ctx = QueryContext::with(DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME);
+ ctx.set_protocol_ctx(ProtocolCtx::OtlpMetric(OtlpMetricCtx {
+ with_metric_engine,
+ experimental_enable_exponential_histogram: true,
+ ..Default::default()
+ }));
+ Arc::new(ctx)
+ }
+
+ fn metrics_request(metrics: Vec) -> ExportMetricsServiceRequest {
+ ExportMetricsServiceRequest {
+ resource_metrics: vec![ResourceMetrics {
+ scope_metrics: vec![ScopeMetrics {
+ metrics,
+ ..Default::default()
+ }],
+ ..Default::default()
+ }],
+ }
+ }
+
+ fn gauge_metric(metric_name: &str) -> Metric {
+ Metric {
+ name: metric_name.to_string(),
+ data: Some(metric::Data::Gauge(Gauge {
+ data_points: vec![NumberDataPoint {
+ time_unix_nano: 1_000_000,
+ value: Some(Value::AsDouble(1.0)),
+ ..Default::default()
+ }],
+ })),
+ ..Default::default()
+ }
+ }
+
+ fn gauge_request(metric_name: &str) -> ExportMetricsServiceRequest {
+ metrics_request(vec![gauge_metric(metric_name)])
+ }
+
+ fn native_histogram_metric(metric_name: &str) -> Metric {
+ Metric {
+ name: metric_name.to_string(),
+ data: Some(metric::Data::ExponentialHistogram(ExponentialHistogram {
+ data_points: vec![ExponentialHistogramDataPoint {
+ time_unix_nano: 1_000_000,
+ count: 1,
+ sum: Some(1.0),
+ zero_count: 1,
+ ..Default::default()
+ }],
+ aggregation_temporality: AggregationTemporality::Cumulative as i32,
+ })),
+ ..Default::default()
+ }
+ }
+
+ async fn assert_metrics_falls_back(
+ instance: &Instance,
+ request: ExportMetricsServiceRequest,
+ ctx: QueryContextRef,
+ ) {
+ let batcher = Arc::new(FakeMetricRowBatcher::default());
+
+ let error = instance
+ .metrics(request, Some(batcher.clone()), ctx)
+ .await
+ .unwrap_err();
+
+ let servers::error::Error::ExecuteGrpcQuery { source, .. } = error else {
+ panic!("expected ExecuteGrpcQuery from Inserter, got {error:?}");
+ };
+ let Some(crate::error::Error::TableOperation { source, .. }) =
+ source.as_any().downcast_ref::()
+ else {
+ panic!("expected frontend TableOperation from Inserter, got {source:?}");
+ };
+ assert!(
+ matches!(
+ source,
+ operator::error::Error::SchemaNotFound { schema_info, .. }
+ if schema_info == DEFAULT_SCHEMA_NAME
+ ),
+ "expected test-environment schema lookup failure, got {source:?}"
+ );
+ assert!(batcher.requests.lock().unwrap().is_empty());
+ }
+
+ #[tokio::test]
+ async fn test_metrics_routes_eligible_request_to_batcher() {
+ let instance = test_instance().await;
+ let batcher = Arc::new(FakeMetricRowBatcher::successful(7));
+
+ let outcome = instance
+ .metrics(
+ gauge_request("temperature"),
+ Some(batcher.clone()),
+ metric_query_ctx(true),
+ )
+ .await
+ .unwrap();
+
+ assert_eq!(1, outcome.accepted_data_points);
+ assert_eq!(0, outcome.rejected_data_points);
+ assert_eq!(0, outcome.write_cost);
+ let requests = batcher.requests.lock().unwrap();
+ assert_eq!(1, requests.len());
+ assert_eq!("temperature", requests[0].inserts[0].table_name);
+ assert_eq!(
+ vec![MetricRowBatchProtocol::Otlp],
+ *batcher.protocols.lock().unwrap()
+ );
+ }
+
+ #[tokio::test]
+ async fn test_metrics_legacy_mode_falls_back_to_inserter() {
+ let instance = test_instance().await;
+ let ctx = metric_query_ctx(true);
+ instance
+ .otlp_metrics_table_legacy_cache
+ .entry(ctx.get_db_string())
+ .or_default()
+ .insert("temperature".to_string(), true);
+
+ assert_metrics_falls_back(&instance, gauge_request("temperature"), ctx).await;
+ }
+
+ #[tokio::test]
+ async fn test_metrics_without_metric_engine_falls_back_to_inserter() {
+ let instance = test_instance().await;
+
+ assert_metrics_falls_back(
+ &instance,
+ gauge_request("temperature"),
+ metric_query_ctx(false),
+ )
+ .await;
+ }
+
+ #[tokio::test]
+ async fn test_metrics_native_histogram_falls_back_to_inserter() {
+ let _guard = FALLBACK_METRIC_ASSERTION_LOCK.lock().await;
+ let instance = test_instance().await;
+ let fallback = METRIC_ROW_BATCH_ELIGIBILITY_FALLBACKS
+ .with_label_values(&[MetricRowBatchProtocol::Otlp.as_str()]);
+ let initial = fallback.get();
+
+ assert_metrics_falls_back(
+ &instance,
+ metrics_request(vec![native_histogram_metric("latency")]),
+ metric_query_ctx(true),
+ )
+ .await;
+ assert_eq!(initial + 1, fallback.get());
+ }
+
+ #[tokio::test]
+ async fn test_metrics_mixed_request_falls_back_to_inserter() {
+ let _guard = FALLBACK_METRIC_ASSERTION_LOCK.lock().await;
+ let instance = test_instance().await;
+
+ assert_metrics_falls_back(
+ &instance,
+ metrics_request(vec![
+ gauge_metric("temperature"),
+ native_histogram_metric("latency"),
+ ]),
+ metric_query_ctx(true),
+ )
+ .await;
+ }
+
+ #[tokio::test]
+ async fn test_metrics_batcher_error_skips_resource_info_insert() {
+ let checker = Arc::new(CountingPermissionChecker::default());
+ let plugins = Plugins::new();
+ plugins.insert::(checker.clone());
+ let mut options = FrontendOptions::default();
+ options.otlp.experimental_enable_resource_info = true;
+ let instance = test_instance_with(options, plugins).await;
+ let batcher = Arc::new(FakeMetricRowBatcher::failing("flush failed"));
+ let mut request = gauge_request("temperature");
+ request.resource_metrics[0].resource = Some(MetricResource {
+ attributes: vec![MetricKeyValue {
+ key: "service.name".to_string(),
+ value: Some(MetricAnyValue {
+ value: Some(metric_any_value::Value::StringValue("frontend".to_string())),
+ }),
+ }],
+ ..Default::default()
+ });
+
+ let error = instance
+ .metrics(request, Some(batcher.clone()), metric_query_ctx(true))
+ .await
+ .unwrap_err();
+
+ assert!(error.to_string().contains("flush failed"), "{error:?}");
+ assert_eq!(1, batcher.requests.lock().unwrap().len());
+ assert_eq!(2, checker.checks.load(Ordering::Relaxed));
+ }
#[test]
fn test_trace_permission_targets() {
diff --git a/src/frontend/src/instance/prom_store.rs b/src/frontend/src/instance/prom_store.rs
index e0b17b1a89e..af2a95857c7 100644
--- a/src/frontend/src/instance/prom_store.rs
+++ b/src/frontend/src/instance/prom_store.rs
@@ -387,7 +387,7 @@ impl PendingRowsSchemaAlterer for Instance {
Ok(())
}
- async fn add_missing_prom_tag_columns_batch(
+ async fn add_missing_metric_tag_columns_batch(
&self,
catalog: &str,
schema: &str,
diff --git a/src/frontend/src/server.rs b/src/frontend/src/server.rs
index e112e5ae2a9..70490977efb 100644
--- a/src/frontend/src/server.rs
+++ b/src/frontend/src/server.rs
@@ -38,7 +38,10 @@ use servers::interceptor::LogIngestInterceptorRef;
use servers::metrics_handler::MetricsHandler;
use servers::mysql::server::{MysqlServer, MysqlSpawnConfig, MysqlSpawnRef};
use servers::otel_arrow::OtelArrowServiceHandler;
-use servers::pending_rows_batcher::{PendingRowsBatcher, pending_rows_batch_sync_enabled};
+use servers::pending_rows_batcher::{
+ MetricRowBatcherRef, PendingRowsBatchMode, PendingRowsBatcher, PendingRowsBatcherOptions,
+ pending_rows_batch_sync_enabled,
+};
use servers::postgres::PostgresServer;
use servers::request_memory_limiter::ServerMemoryLimiter;
use servers::server::{Server, ServerHandlers};
@@ -61,15 +64,148 @@ where
plugins: Plugins,
flight_handler: Option,
internal_flight_handler: Option,
+ metric_batching: ResolvedMetricBatching,
+ pending_rows_batcher: Option>,
+ #[cfg(any(test, feature = "testing"))]
+ metric_batcher_wiring: MetricBatcherWiring,
pub server_memory_limiter: ServerMemoryLimiter,
}
+#[cfg(any(test, feature = "testing"))]
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+enum MetricBatchConsumer {
+ PrometheusHttp,
+ OtlpHttp,
+ ExternalOtelArrow,
+ InternalOtelArrow,
+}
+
+#[cfg(any(test, feature = "testing"))]
+type MetricBatcherWiring =
+ Arc)>>>;
+
+struct ResolvedMetricBatching {
+ options: Option,
+ batch_mode: PendingRowsBatchMode,
+ with_metric_engine: bool,
+ prometheus_enabled: bool,
+ otlp_enabled: bool,
+ http_batch_consumer_enabled: bool,
+}
+
+/// Metric HTTP consumers registered outside OSS [`Services`] route assembly.
+///
+/// This input only describes route ownership. Runtime [`FrontendOptions`] remain
+/// authoritative for Metric Engine, batching mode, tuning, and protocol behavior.
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
+pub struct ExternalMetricHttpConsumers {
+ /// An external Prometheus HTTP route consumes the shared metric batcher.
+ pub prometheus: bool,
+ /// An external OTLP HTTP route consumes the shared metric batcher.
+ pub otlp: bool,
+}
+
+impl ResolvedMetricBatching {
+ fn batcher_enabled(&self) -> bool {
+ self.prometheus_enabled || self.otlp_enabled
+ }
+}
+
+fn resolve_metric_batching(
+ opts: &FrontendOptions,
+ external_http_consumers: ExternalMetricHttpConsumers,
+ batch_mode: PendingRowsBatchMode,
+) -> ResolvedMetricBatching {
+ let prom = &opts.prom_store;
+ let options = prom.pending_rows_batcher_options();
+ let valid_options = options.is_some();
+ let prometheus_enabled = (prom.enable || external_http_consumers.prometheus)
+ && prom.with_metric_engine
+ && valid_options;
+ let otlp_enabled = opts.otlp.enable_metrics_batching
+ && prom.with_metric_engine
+ && batch_mode.is_synchronous()
+ && valid_options;
+ let http_batch_consumer_enabled =
+ prometheus_enabled || ((opts.otlp.enable || external_http_consumers.otlp) && otlp_enabled);
+
+ ResolvedMetricBatching {
+ options,
+ batch_mode,
+ with_metric_engine: prom.with_metric_engine,
+ prometheus_enabled,
+ otlp_enabled,
+ http_batch_consumer_enabled,
+ }
+}
+
impl Services
where
T: Into + Configurable + Clone,
{
pub fn new(opts: T, instance: Arc, plugins: Plugins) -> Self {
+ Self::new_with_external_metric_http_consumers(
+ opts,
+ ExternalMetricHttpConsumers::default(),
+ instance,
+ plugins,
+ )
+ }
+
+ /// Creates services that share metric batching with externally owned HTTP routes.
+ pub fn new_with_external_metric_http_consumers(
+ opts: T,
+ external_http_consumers: ExternalMetricHttpConsumers,
+ instance: Arc,
+ plugins: Plugins,
+ ) -> Self {
+ let batch_mode = if pending_rows_batch_sync_enabled() {
+ PendingRowsBatchMode::Synchronous
+ } else {
+ PendingRowsBatchMode::Asynchronous
+ };
+ let metric_batching =
+ resolve_metric_batching(&opts.clone().into(), external_http_consumers, batch_mode);
+ Self::new_with_metric_batching(opts, instance, plugins, metric_batching)
+ }
+
+ #[cfg(test)]
+ fn new_with_synchronous_metric_batching(
+ opts: T,
+ instance: Arc,
+ plugins: Plugins,
+ ) -> Self {
+ let metric_batching = resolve_metric_batching(
+ &opts.clone().into(),
+ ExternalMetricHttpConsumers::default(),
+ PendingRowsBatchMode::Synchronous,
+ );
+ Self::new_with_metric_batching(opts, instance, plugins, metric_batching)
+ }
+
+ fn new_with_metric_batching(
+ opts: T,
+ instance: Arc,
+ plugins: Plugins,
+ metric_batching: ResolvedMetricBatching,
+ ) -> Self {
let feopts = opts.clone().into();
+ let pending_rows_batcher = metric_batching
+ .options
+ .as_ref()
+ .filter(|_| metric_batching.batcher_enabled())
+ .map(|options| {
+ PendingRowsBatcher::new(
+ instance.partition_manager().clone(),
+ instance.node_manager().clone(),
+ instance.catalog_manager().clone(),
+ instance.table_flownode_set_cache().clone(),
+ metric_batching.with_metric_engine,
+ instance.clone(),
+ options.clone(),
+ metric_batching.batch_mode,
+ )
+ });
// Create server request memory limiter for all server protocols
let server_memory_limiter = ServerMemoryLimiter::new(
feopts.max_in_flight_write_bytes.as_bytes(),
@@ -84,6 +220,10 @@ where
plugins,
flight_handler: None,
internal_flight_handler: None,
+ metric_batching,
+ pending_rows_batcher,
+ #[cfg(any(test, feature = "testing"))]
+ metric_batcher_wiring: Default::default(),
server_memory_limiter,
}
}
@@ -105,9 +245,10 @@ where
opts: &FrontendOptions,
request_memory_limiter: ServerMemoryLimiter,
) -> HttpServerBuilder {
- let mut builder = HttpServerBuilder::new(effective_http_options(opts))
- .with_memory_limiter(request_memory_limiter)
- .with_sql_handler(self.instance.clone());
+ let mut builder =
+ HttpServerBuilder::new(effective_http_options(&opts.http, &self.metric_batching))
+ .with_memory_limiter(request_memory_limiter)
+ .with_sql_handler(self.instance.clone());
let validator = self.plugins.get::();
let ingest_interceptor = self.plugins.get::>();
@@ -128,24 +269,10 @@ where
}
if opts.prom_store.enable {
- let pending_rows_batcher = if opts.prom_store.with_metric_engine {
- PendingRowsBatcher::try_new(
- self.instance.partition_manager().clone(),
- self.instance.node_manager().clone(),
- self.instance.catalog_manager().clone(),
- self.instance.table_flownode_set_cache().clone(),
- opts.prom_store.with_metric_engine,
- self.instance.clone(),
- opts.prom_store.pending_rows_flush_interval,
- opts.prom_store.max_batch_rows,
- opts.prom_store.max_concurrent_flushes,
- opts.prom_store.worker_channel_capacity,
- opts.prom_store.max_inflight_requests,
- opts.prom_store.flow_notification_queue_capacity,
- )
- } else {
- None
- };
+ let metric_row_batcher =
+ self.metric_row_batcher(self.metric_batching.prometheus_enabled);
+ #[cfg(any(test, feature = "testing"))]
+ self.record_metric_batcher(MetricBatchConsumer::PrometheusHttp, &metric_row_batcher);
builder = builder
.with_prom_handler(
self.instance.clone(),
@@ -154,16 +281,20 @@ where
opts.prom_store.prom_validation_mode,
opts.prom_store
.experimental_enable_prometheus_native_histogram,
- pending_rows_batcher,
+ metric_row_batcher,
)
.with_prometheus_handler(self.instance.clone());
}
if opts.otlp.enable {
+ let metric_row_batcher = self.metric_row_batcher(self.metric_batching.otlp_enabled);
+ #[cfg(any(test, feature = "testing"))]
+ self.record_metric_batcher(MetricBatchConsumer::OtlpHttp, &metric_row_batcher);
builder = builder.with_otlp_handler(
self.instance.clone(),
opts.prom_store.with_metric_engine,
opts.otlp.experimental_enable_exponential_histogram,
+ metric_row_batcher,
);
}
@@ -218,6 +349,50 @@ where
}
}
+ fn metric_row_batcher(&self, enabled: bool) -> Option {
+ if !enabled {
+ return None;
+ }
+
+ self.pending_rows_batcher
+ .as_ref()
+ .map(|batcher| batcher.clone() as MetricRowBatcherRef)
+ }
+
+ /// Returns the shared batcher when Prometheus pending-row batching is enabled.
+ pub fn prometheus_metric_row_batcher(&self) -> Option {
+ self.metric_row_batcher(self.metric_batching.prometheus_enabled)
+ }
+
+ /// Returns the shared batcher when OTLP metric batching is enabled.
+ pub fn otlp_metric_row_batcher(&self) -> Option {
+ self.metric_row_batcher(self.metric_batching.otlp_enabled)
+ }
+
+ #[cfg(any(test, feature = "testing"))]
+ fn record_metric_batcher(
+ &self,
+ consumer: MetricBatchConsumer,
+ batcher: &Option,
+ ) {
+ self.metric_batcher_wiring
+ .lock()
+ .unwrap()
+ .push((consumer, batcher.clone()));
+ }
+
+ /// Reports whether the production HTTP assembly configured an OTLP metric batcher.
+ #[cfg(feature = "testing")]
+ pub fn otlp_http_metric_batcher_configured(&self) -> Option {
+ self.metric_batcher_wiring
+ .lock()
+ .unwrap()
+ .iter()
+ .rev()
+ .find(|(consumer, _)| *consumer == MetricBatchConsumer::OtlpHttp)
+ .map(|(_, batcher)| batcher.is_some())
+ }
+
fn build_grpc_server(
&mut self,
grpc: &GrpcOptions,
@@ -264,6 +439,17 @@ where
.unwrap_or(default_flight_handler)
};
+ let metric_row_batcher = self.metric_row_batcher(self.metric_batching.otlp_enabled);
+ #[cfg(any(test, feature = "testing"))]
+ self.record_metric_batcher(
+ if external {
+ MetricBatchConsumer::ExternalOtelArrow
+ } else {
+ MetricBatchConsumer::InternalOtelArrow
+ },
+ &metric_row_batcher,
+ );
+
let grpc_server = builder
.name(name)
.database_handler(greptime_request_handler.clone())
@@ -271,6 +457,8 @@ where
.otel_arrow_handler(OtelArrowServiceHandler::new(
self.instance.clone(),
user_provider.clone(),
+ self.metric_batching.with_metric_engine,
+ metric_row_batcher,
))
.flight_handler(flight_handler)
.add_layer(axum::middleware::from_fn_with_state(
@@ -426,19 +614,22 @@ where
}
}
-fn effective_http_options(opts: &FrontendOptions) -> HttpOptions {
- effective_http_options_with_sync(opts, pending_rows_batch_sync_enabled())
-}
-
-fn effective_http_options_with_sync(opts: &FrontendOptions, batch_sync: bool) -> HttpOptions {
- let mut http = opts.http.clone();
- let flush_interval = opts.prom_store.pending_rows_flush_interval;
+fn effective_http_options(
+ configured_http: &HttpOptions,
+ metric_batching: &ResolvedMetricBatching,
+) -> HttpOptions {
+ let mut http = configured_http.clone();
+ let flush_interval = metric_batching
+ .options
+ .as_ref()
+ .map(PendingRowsBatcherOptions::flush_interval)
+ .unwrap_or_default();
let fallback_timeout = flush_interval.saturating_add(Duration::from_secs(1));
// In asynchronous batch mode submissions return right after enqueue and
// no request waits for a pending-row flush, so the timeout must not be
// raised either.
- if !opts.prom_store.pending_rows_batching_enabled()
- || !batch_sync
+ if !metric_batching.batch_mode.is_synchronous()
+ || !metric_batching.http_batch_consumer_enabled
|| http.timeout.is_zero()
|| http.timeout > fallback_timeout
{
@@ -506,6 +697,15 @@ mod tests {
}
}
+ fn resolved_http_options(
+ opts: &FrontendOptions,
+ batch_mode: PendingRowsBatchMode,
+ ) -> HttpOptions {
+ let metric_batching =
+ resolve_metric_batching(opts, ExternalMetricHttpConsumers::default(), batch_mode);
+ effective_http_options(&opts.http, &metric_batching)
+ }
+
#[test]
fn test_effective_http_timeout_for_pending_rows() {
let cases = [
@@ -536,7 +736,7 @@ mod tests {
assert_eq!(
Duration::from_millis(expected),
- effective_http_options_with_sync(&opts, true).timeout,
+ resolved_http_options(&opts, PendingRowsBatchMode::Synchronous).timeout,
"{name}"
);
}
@@ -553,19 +753,18 @@ mod tests {
assert_eq!(
Duration::from_millis(1000),
- effective_http_options_with_sync(&opts, false).timeout,
+ resolved_http_options(&opts, PendingRowsBatchMode::Asynchronous).timeout,
);
assert_eq!(
Duration::from_millis(6000),
- effective_http_options_with_sync(&opts, true).timeout,
+ resolved_http_options(&opts, PendingRowsBatchMode::Synchronous).timeout,
);
}
#[test]
fn test_effective_http_timeout_skips_fallback_when_batcher_disabled() {
- // Mirrors the conditions under which `PendingRowsBatcher::try_new`
- // returns `None`; in these cases no request can wait for a pending-row
- // flush, so the timeout must not be raised.
+ // Invalid tuning cannot produce a resolved batcher, so no request can
+ // wait for a pending-row flush and the timeout must not be raised.
type KnobMutator = fn(&mut FrontendOptions);
let cases: [(&str, KnobMutator); 4] = [
("zero max_batch_rows", |opts| {
@@ -590,12 +789,362 @@ mod tests {
assert_eq!(
Duration::from_millis(1000),
- effective_http_options_with_sync(&opts, true).timeout,
+ resolved_http_options(&opts, PendingRowsBatchMode::Synchronous).timeout,
"{name}"
);
}
}
+ #[test]
+ fn test_metric_batching_enablement_matrix() {
+ type OptionsMutator = fn(&mut FrontendOptions);
+ let cases: [(&str, OptionsMutator, bool, bool, bool, PendingRowsBatchMode); 8] = [
+ (
+ "prometheus only",
+ |_| {},
+ true,
+ false,
+ true,
+ PendingRowsBatchMode::Synchronous,
+ ),
+ (
+ "otlp only with HTTP route disabled",
+ |opts| {
+ opts.prom_store.enable = false;
+ opts.otlp.enable = false;
+ opts.otlp.enable_metrics_batching = true;
+ },
+ false,
+ true,
+ true,
+ PendingRowsBatchMode::Synchronous,
+ ),
+ (
+ "both consumers",
+ |opts| opts.otlp.enable_metrics_batching = true,
+ true,
+ true,
+ true,
+ PendingRowsBatchMode::Synchronous,
+ ),
+ (
+ "both routes disabled",
+ |opts| {
+ opts.prom_store.enable = false;
+ opts.otlp.enable = false;
+ },
+ false,
+ false,
+ false,
+ PendingRowsBatchMode::Synchronous,
+ ),
+ (
+ "metric engine disabled",
+ |opts| {
+ opts.prom_store.with_metric_engine = false;
+ opts.otlp.enable_metrics_batching = true;
+ },
+ false,
+ false,
+ false,
+ PendingRowsBatchMode::Synchronous,
+ ),
+ (
+ "asynchronous mode preserves prometheus only",
+ |opts| opts.otlp.enable_metrics_batching = true,
+ true,
+ false,
+ true,
+ PendingRowsBatchMode::Asynchronous,
+ ),
+ (
+ "prometheus disabled does not disable otlp",
+ |opts| {
+ opts.prom_store.enable = false;
+ opts.otlp.enable_metrics_batching = true;
+ },
+ false,
+ true,
+ true,
+ PendingRowsBatchMode::Synchronous,
+ ),
+ (
+ "otlp batching disabled does not disable prometheus",
+ |opts| opts.otlp.enable_metrics_batching = false,
+ true,
+ false,
+ true,
+ PendingRowsBatchMode::Synchronous,
+ ),
+ ];
+
+ for (name, mutate, expected_prom, expected_otlp, expected_batcher, batch_mode) in cases {
+ let mut opts = FrontendOptions::default();
+ opts.prom_store.pending_rows_flush_interval = Duration::from_secs(5);
+ mutate(&mut opts);
+
+ let resolved =
+ resolve_metric_batching(&opts, ExternalMetricHttpConsumers::default(), batch_mode);
+ assert_eq!(expected_prom, resolved.prometheus_enabled, "{name}");
+ assert_eq!(expected_otlp, resolved.otlp_enabled, "{name}");
+ assert_eq!(batch_mode, resolved.batch_mode, "{name}");
+ assert_eq!(expected_batcher, resolved.batcher_enabled(), "{name}");
+ assert_eq!(
+ Duration::from_secs(5),
+ resolved.options.as_ref().unwrap().flush_interval(),
+ "{name}"
+ );
+ }
+ }
+
+ #[test]
+ fn test_metric_batching_snapshot_remains_authoritative_after_source_options_change() {
+ let mut opts = FrontendOptions::default();
+ opts.http.timeout = Duration::from_secs(1);
+ opts.prom_store.pending_rows_flush_interval = Duration::from_secs(5);
+ let resolved = resolve_metric_batching(
+ &opts,
+ ExternalMetricHttpConsumers::default(),
+ PendingRowsBatchMode::Synchronous,
+ );
+
+ opts.prom_store.pending_rows_flush_interval = Duration::from_secs(30);
+ opts.prom_store.max_batch_rows = 0;
+
+ assert_eq!(
+ Duration::from_secs(6),
+ effective_http_options(&opts.http, &resolved).timeout
+ );
+ assert!(resolved.options.is_some());
+ assert!(resolved.batcher_enabled());
+ }
+
+ #[test]
+ fn test_external_http_consumers_cannot_override_runtime_batching_configuration() {
+ type OptionsMutator = fn(&mut FrontendOptions);
+ struct TestCase {
+ name: &'static str,
+ external_http_consumers: ExternalMetricHttpConsumers,
+ batch_mode: PendingRowsBatchMode,
+ mutate: OptionsMutator,
+ expected_prometheus: bool,
+ expected_otlp: bool,
+ expected_flush_interval: Option,
+ expected_timeout: Duration,
+ }
+
+ let cases = [
+ TestCase {
+ name: "external Prometheus uses runtime tuning",
+ external_http_consumers: ExternalMetricHttpConsumers {
+ prometheus: true,
+ otlp: false,
+ },
+ batch_mode: PendingRowsBatchMode::Synchronous,
+ mutate: |_| {},
+ expected_prometheus: true,
+ expected_otlp: false,
+ expected_flush_interval: Some(Duration::from_secs(5)),
+ expected_timeout: Duration::from_secs(6),
+ },
+ TestCase {
+ name: "external OTLP uses runtime tuning and batching flag",
+ external_http_consumers: ExternalMetricHttpConsumers {
+ prometheus: false,
+ otlp: true,
+ },
+ batch_mode: PendingRowsBatchMode::Synchronous,
+ mutate: |opts| opts.otlp.enable_metrics_batching = true,
+ expected_prometheus: false,
+ expected_otlp: true,
+ expected_flush_interval: Some(Duration::from_secs(5)),
+ expected_timeout: Duration::from_secs(6),
+ },
+ TestCase {
+ name: "external OTLP cannot enable runtime-disabled batching",
+ external_http_consumers: ExternalMetricHttpConsumers {
+ prometheus: false,
+ otlp: true,
+ },
+ batch_mode: PendingRowsBatchMode::Synchronous,
+ mutate: |_| {},
+ expected_prometheus: false,
+ expected_otlp: false,
+ expected_flush_interval: Some(Duration::from_secs(5)),
+ expected_timeout: Duration::from_secs(1),
+ },
+ TestCase {
+ name: "external OTLP cannot replace invalid runtime tuning",
+ external_http_consumers: ExternalMetricHttpConsumers {
+ prometheus: false,
+ otlp: true,
+ },
+ batch_mode: PendingRowsBatchMode::Synchronous,
+ mutate: |opts| {
+ opts.otlp.enable_metrics_batching = true;
+ opts.prom_store.max_batch_rows = 0;
+ },
+ expected_prometheus: false,
+ expected_otlp: false,
+ expected_flush_interval: None,
+ expected_timeout: Duration::from_secs(1),
+ },
+ TestCase {
+ name: "external OTLP cannot override asynchronous runtime mode",
+ external_http_consumers: ExternalMetricHttpConsumers {
+ prometheus: false,
+ otlp: true,
+ },
+ batch_mode: PendingRowsBatchMode::Asynchronous,
+ mutate: |opts| opts.otlp.enable_metrics_batching = true,
+ expected_prometheus: false,
+ expected_otlp: false,
+ expected_flush_interval: Some(Duration::from_secs(5)),
+ expected_timeout: Duration::from_secs(1),
+ },
+ ];
+
+ for case in cases {
+ let mut opts = FrontendOptions::default();
+ opts.http.timeout = Duration::from_secs(1);
+ opts.prom_store.enable = false;
+ opts.prom_store.pending_rows_flush_interval = Duration::from_secs(5);
+ opts.otlp.enable = false;
+ (case.mutate)(&mut opts);
+
+ let resolved =
+ resolve_metric_batching(&opts, case.external_http_consumers, case.batch_mode);
+
+ assert_eq!(
+ case.expected_prometheus, resolved.prometheus_enabled,
+ "{}",
+ case.name
+ );
+ assert_eq!(case.expected_otlp, resolved.otlp_enabled, "{}", case.name);
+ assert_eq!(case.batch_mode, resolved.batch_mode, "{}", case.name);
+ assert_eq!(
+ case.expected_flush_interval,
+ resolved
+ .options
+ .as_ref()
+ .map(|options| options.flush_interval()),
+ "{}",
+ case.name
+ );
+ assert_eq!(
+ case.expected_timeout,
+ effective_http_options(&opts.http, &resolved).timeout,
+ "{}",
+ case.name
+ );
+ }
+ }
+
+ #[test]
+ fn test_metric_batching_invalid_options_disable_all_consumers() {
+ type KnobMutator = fn(&mut FrontendOptions);
+ let cases: [(&str, KnobMutator); 5] = [
+ ("zero flush interval", |opts| {
+ opts.prom_store.pending_rows_flush_interval = Duration::ZERO
+ }),
+ ("zero max batch rows", |opts| {
+ opts.prom_store.max_batch_rows = 0
+ }),
+ ("zero max concurrent flushes", |opts| {
+ opts.prom_store.max_concurrent_flushes = 0
+ }),
+ ("zero worker channel capacity", |opts| {
+ opts.prom_store.worker_channel_capacity = 0
+ }),
+ ("zero max inflight requests", |opts| {
+ opts.prom_store.max_inflight_requests = 0
+ }),
+ ];
+
+ for (name, disable_batcher) in cases {
+ let mut opts = FrontendOptions::default();
+ opts.prom_store.pending_rows_flush_interval = Duration::from_secs(5);
+ opts.otlp.enable_metrics_batching = true;
+ disable_batcher(&mut opts);
+
+ let resolved = resolve_metric_batching(
+ &opts,
+ ExternalMetricHttpConsumers::default(),
+ PendingRowsBatchMode::Synchronous,
+ );
+ assert!(!resolved.prometheus_enabled, "{name}");
+ assert!(!resolved.otlp_enabled, "{name}");
+ assert!(resolved.options.is_none(), "{name}");
+ }
+ }
+
+ #[test]
+ fn test_effective_http_timeout_for_enabled_sync_consumers() {
+ let cases = [
+ ("prometheus", true, false, true, true, 6000),
+ ("otlp HTTP", false, true, true, true, 6000),
+ ("otlp Arrow only", false, true, false, true, 1000),
+ ("asynchronous prometheus", true, false, true, false, 1000),
+ (
+ "otlp fallback in asynchronous mode",
+ false,
+ true,
+ true,
+ false,
+ 1000,
+ ),
+ ];
+
+ for (name, prom_enable, otlp_batching, otlp_http, batch_sync, expected) in cases {
+ let mut opts = FrontendOptions::default();
+ opts.http.timeout = Duration::from_millis(1000);
+ opts.prom_store.enable = prom_enable;
+ opts.prom_store.pending_rows_flush_interval = Duration::from_millis(5000);
+ opts.otlp.enable = otlp_http;
+ opts.otlp.enable_metrics_batching = otlp_batching;
+
+ assert_eq!(
+ Duration::from_millis(expected),
+ resolved_http_options(
+ &opts,
+ if batch_sync {
+ PendingRowsBatchMode::Synchronous
+ } else {
+ PendingRowsBatchMode::Asynchronous
+ },
+ )
+ .timeout,
+ "{name}"
+ );
+ }
+ }
+
+ #[test]
+ fn test_enterprise_otlp_consumer_extends_timeout_with_oss_route_disabled() {
+ let mut opts = FrontendOptions::default();
+ opts.http.timeout = Duration::from_secs(1);
+ opts.prom_store.enable = false;
+ opts.prom_store.pending_rows_flush_interval = Duration::from_secs(5);
+ opts.otlp.enable = false;
+ opts.otlp.enable_metrics_batching = true;
+ let resolved = resolve_metric_batching(
+ &opts,
+ ExternalMetricHttpConsumers {
+ prometheus: false,
+ otlp: true,
+ },
+ PendingRowsBatchMode::Synchronous,
+ );
+
+ assert_eq!(
+ Duration::from_secs(6),
+ effective_http_options(&opts.http, &resolved).timeout
+ );
+ assert!(!opts.otlp.enable);
+ assert!(resolved.otlp_enabled);
+ assert!(resolved.http_batch_consumer_enabled);
+ }
+
#[tokio::test]
async fn test_database_sql_authentication_differs_between_public_and_internal_grpc() {
let options = FrontendOptions {
@@ -741,4 +1290,59 @@ mod tests {
// Assert
assert!(health_check.is_ok());
}
+
+ #[tokio::test]
+ async fn test_services_wires_same_metric_batcher_to_all_enabled_consumers() {
+ let options = FrontendOptions {
+ grpc: GrpcOptions::default().with_bind_addr("127.0.0.1:0"),
+ internal_grpc: Some(GrpcOptions::default().with_bind_addr("127.0.0.1:0")),
+ prom_store: crate::service_config::PromStoreOptions {
+ pending_rows_flush_interval: Duration::from_secs(1),
+ ..Default::default()
+ },
+ otlp: crate::service_config::OtlpOptions {
+ enable_metrics_batching: true,
+ ..Default::default()
+ },
+ ..Default::default()
+ };
+ let meta_client = Arc::new(
+ MetaClientBuilder::new(0, Role::Frontend)
+ .enable_procedure()
+ .build(),
+ );
+ let instance = Arc::new(
+ FrontendBuilder::new_test(&options, meta_client)
+ .try_build()
+ .await
+ .unwrap(),
+ );
+ let services =
+ Services::new_with_synchronous_metric_batching(options, instance, Default::default());
+ let wiring = services.metric_batcher_wiring.clone();
+
+ let _handlers = services.build().unwrap();
+
+ let wiring = wiring.lock().unwrap();
+ assert_eq!(4, wiring.len());
+ let consumers = [
+ MetricBatchConsumer::PrometheusHttp,
+ MetricBatchConsumer::OtlpHttp,
+ MetricBatchConsumer::ExternalOtelArrow,
+ MetricBatchConsumer::InternalOtelArrow,
+ ];
+ let first = wiring
+ .iter()
+ .find(|(consumer, _)| *consumer == consumers[0])
+ .and_then(|(_, batcher)| batcher.as_ref())
+ .unwrap();
+ for consumer in consumers {
+ let batcher = wiring
+ .iter()
+ .find(|(wired_consumer, _)| *wired_consumer == consumer)
+ .and_then(|(_, batcher)| batcher.as_ref())
+ .unwrap();
+ assert!(Arc::ptr_eq(first, batcher), "{consumer:?}");
+ }
+ }
}
diff --git a/src/frontend/src/service_config/otlp.rs b/src/frontend/src/service_config/otlp.rs
index d6a36f702e4..a556aa0a359 100644
--- a/src/frontend/src/service_config/otlp.rs
+++ b/src/frontend/src/service_config/otlp.rs
@@ -20,6 +20,13 @@ const DEFAULT_TRACE_INGEST_CHUNK_SIZE: usize = 512;
#[serde(default)]
pub struct OtlpOptions {
pub enable: bool,
+ /// Enables pending-row batching for eligible OTLP metrics over HTTP and OTel Arrow.
+ ///
+ /// Disabled by default. Batching requires Metric Engine, valid nonzero
+ /// `prom_store.pending_rows_*` tuning, and synchronous pending-row mode.
+ /// Requests fall back to the Inserter when these conditions are not met or
+ /// when the converted rows are not scalar metrics supported by the batcher.
+ pub enable_metrics_batching: bool,
pub experimental_enable_exponential_histogram: bool,
/// Maximum spans per trace ingest chunk. Set to 0 to disable splitting.
pub trace_ingest_chunk_size: usize,
@@ -34,6 +41,7 @@ impl Default for OtlpOptions {
fn default() -> Self {
Self {
enable: true,
+ enable_metrics_batching: false,
experimental_enable_exponential_histogram: false,
trace_ingest_chunk_size: DEFAULT_TRACE_INGEST_CHUNK_SIZE,
experimental_enable_resource_info: false,
@@ -49,12 +57,14 @@ mod tests {
fn test_otlp_options() {
let default = OtlpOptions::default();
assert!(default.enable);
+ assert!(!default.enable_metrics_batching);
assert!(!default.experimental_enable_exponential_histogram);
assert_eq!(default.trace_ingest_chunk_size, 512);
assert!(!default.experimental_enable_resource_info);
let options: OtlpOptions = toml::from_str("enable = false").unwrap();
assert!(!options.enable);
+ assert!(!options.enable_metrics_batching);
assert!(!options.experimental_enable_exponential_histogram);
assert_eq!(
options.trace_ingest_chunk_size,
@@ -71,7 +81,12 @@ mod tests {
assert!(options.experimental_enable_exponential_histogram);
let serialized = toml::to_string(&options).unwrap();
+ assert!(serialized.contains("enable_metrics_batching = false"));
assert!(serialized.contains("experimental_enable_exponential_histogram = true"));
assert_eq!(toml::from_str::(&serialized).unwrap(), options);
+
+ let options: OtlpOptions = toml::from_str("enable_metrics_batching = true").unwrap();
+ assert!(options.enable_metrics_batching);
+ assert!(options.enable);
}
}
diff --git a/src/frontend/src/service_config/prom_store.rs b/src/frontend/src/service_config/prom_store.rs
index d2e386caae0..39c8e6e9026 100644
--- a/src/frontend/src/service_config/prom_store.rs
+++ b/src/frontend/src/service_config/prom_store.rs
@@ -16,6 +16,7 @@ use std::num::NonZeroUsize;
use std::time::Duration;
use serde::{Deserialize, Serialize};
+use servers::pending_rows_batcher::{PendingRowsBatcherOptions, PendingRowsBatcherTuning};
use servers::prom_remote_write::validation::PromValidationMode;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
@@ -64,18 +65,21 @@ fn default_flow_notification_queue_capacity() -> NonZeroUsize {
}
impl PromStoreOptions {
+ pub(crate) fn pending_rows_batcher_options(&self) -> Option {
+ PendingRowsBatcherOptions::try_new(PendingRowsBatcherTuning {
+ flush_interval: self.pending_rows_flush_interval,
+ max_batch_rows: self.max_batch_rows,
+ max_concurrent_flushes: self.max_concurrent_flushes,
+ worker_channel_capacity: self.worker_channel_capacity,
+ max_inflight_requests: self.max_inflight_requests,
+ flow_notification_queue_capacity: self.flow_notification_queue_capacity,
+ })
+ }
+
/// Returns whether the pending rows batcher can be enabled with these
- /// options. Mirrors the enablement conditions of
- /// `PendingRowsBatcher::try_new` in the servers crate, which returns
- /// `None` when any of these knobs is zero.
+ /// options.
pub fn pending_rows_batching_enabled(&self) -> bool {
- self.enable
- && self.with_metric_engine
- && !self.pending_rows_flush_interval.is_zero()
- && self.max_batch_rows > 0
- && self.max_concurrent_flushes > 0
- && self.worker_channel_capacity > 0
- && self.max_inflight_requests > 0
+ self.enable && self.with_metric_engine && self.pending_rows_batcher_options().is_some()
}
}
@@ -133,4 +137,16 @@ mod tests {
default_flow_notification_queue_capacity()
);
}
+
+ #[test]
+ fn test_pending_rows_options_validity_is_independent_of_prometheus_route() {
+ let options = PromStoreOptions {
+ enable: false,
+ pending_rows_flush_interval: Duration::from_secs(1),
+ ..Default::default()
+ };
+
+ assert!(options.pending_rows_batcher_options().is_some());
+ assert!(!options.pending_rows_batching_enabled());
+ }
}
diff --git a/src/servers/src/http.rs b/src/servers/src/http.rs
index 311ed48d912..a7cfca2603a 100644
--- a/src/servers/src/http.rs
+++ b/src/servers/src/http.rs
@@ -73,7 +73,7 @@ use crate::http::result::null_result::NullResponse;
use crate::interceptor::LogIngestInterceptorRef;
use crate::metrics::http_metrics_layer;
use crate::metrics_handler::MetricsHandler;
-use crate::pending_rows_batcher::PendingRowsBatcher;
+use crate::pending_rows_batcher::MetricRowBatcherRef;
use crate::prometheus_handler::PrometheusHandlerRef;
use crate::query_handler::sql::ServerSqlQueryHandlerRef;
use crate::query_handler::{
@@ -682,7 +682,7 @@ impl HttpServerBuilder {
prom_store_with_metric_engine: bool,
prom_validation_mode: PromValidationMode,
experimental_enable_prometheus_native_histogram: bool,
- pending_rows_batcher: Option>,
+ pending_rows_batcher: Option,
) -> Self {
let state = PromStoreState {
prom_store_handler: handler,
@@ -717,6 +717,7 @@ impl HttpServerBuilder {
handler: OpenTelemetryProtocolHandlerRef,
with_metric_engine: bool,
experimental_enable_exponential_histogram: bool,
+ metric_row_batcher: Option,
) -> Self {
Self {
router: self.router.nest(
@@ -725,6 +726,7 @@ impl HttpServerBuilder {
handler,
with_metric_engine,
experimental_enable_exponential_histogram,
+ metric_row_batcher,
),
),
..self
@@ -1392,6 +1394,7 @@ impl HttpServer {
otlp_handler: OpenTelemetryProtocolHandlerRef,
with_metric_engine: bool,
experimental_enable_exponential_histogram: bool,
+ metric_row_batcher: Option,
) -> Router {
Router::new()
.route("/v1/metrics", routing::post(otlp::metrics))
@@ -1405,6 +1408,7 @@ impl HttpServer {
with_metric_engine,
experimental_enable_exponential_histogram,
handler: otlp_handler,
+ metric_row_batcher,
})
}
diff --git a/src/servers/src/http/otlp.rs b/src/servers/src/http/otlp.rs
index 2d5385c5e33..a5da2ad8c37 100644
--- a/src/servers/src/http/otlp.rs
+++ b/src/servers/src/http/otlp.rs
@@ -46,6 +46,7 @@ use crate::http::extractor::{
};
use crate::http::header::{CONTENT_TYPE_PROTOBUF, write_cost_header_map};
use crate::metrics::METRIC_HTTP_OPENTELEMETRY_LOGS_ELAPSED;
+use crate::pending_rows_batcher::MetricRowBatcherRef;
use crate::query_handler::{
MetricsIngestOutcome, OpenTelemetryProtocolHandlerRef, PipelineHandler, TraceIngestOutcome,
};
@@ -79,6 +80,7 @@ pub struct OtlpState {
pub with_metric_engine: bool,
pub experimental_enable_exponential_histogram: bool,
pub handler: OpenTelemetryProtocolHandlerRef,
+ pub metric_row_batcher: Option,
}
#[axum_macros::debug_handler]
@@ -110,6 +112,7 @@ pub async fn metrics(
with_metric_engine,
experimental_enable_exponential_histogram,
handler,
+ metric_row_batcher,
} = state;
query_ctx.set_protocol_ctx(ProtocolCtx::OtlpMetric(OtlpMetricCtx {
@@ -126,7 +129,10 @@ pub async fn metrics(
}));
let query_ctx = Arc::new(query_ctx);
- match handler.metrics(request, query_ctx).await {
+ match handler
+ .metrics(request, metric_row_batcher, query_ctx)
+ .await
+ {
Ok(outcome) => {
if outcome.accepted_data_points == 0 && outcome.rejected_data_points > 0 {
Ok(OtlpMetricsResponse::Failure(outcome))
diff --git a/src/servers/src/http/prom_store.rs b/src/servers/src/http/prom_store.rs
index cb7b6307c4b..3c0f5fc50ad 100644
--- a/src/servers/src/http/prom_store.rs
+++ b/src/servers/src/http/prom_store.rs
@@ -16,7 +16,6 @@ use std::sync::Arc;
use api::prom_store::remote::ReadRequest;
use api::v1::RowInsertRequests;
-use async_trait::async_trait;
use axum::Extension;
use axum::body::Bytes;
use axum::extract::{Query, State};
@@ -45,7 +44,8 @@ use crate::http::extractor::PipelineInfo;
use crate::http::header::{
CONTENT_TYPE_PROTOBUF_STR, GREPTIME_DB_HEADER_METRICS, write_cost_header_map,
};
-use crate::pending_rows_batcher::PendingRowsBatcher;
+use crate::metrics::METRIC_ROW_BATCH_ELIGIBILITY_FALLBACKS;
+use crate::pending_rows_batcher::{MetricRowBatchProtocol, MetricRowBatcher, MetricRowBatcherRef};
use crate::prom_remote_write::decode::PromSeriesProcessor;
use crate::prom_remote_write::v2::decode_remote_write_v2;
use crate::prom_remote_write::validation::PromValidationMode;
@@ -75,7 +75,7 @@ pub struct PromStoreState {
pub prom_store_with_metric_engine: bool,
pub prom_validation_mode: PromValidationMode,
pub experimental_enable_prometheus_native_histogram: bool,
- pub pending_rows_batcher: Option>,
+ pub pending_rows_batcher: Option,
}
#[derive(Debug, Serialize, Deserialize)]
@@ -352,18 +352,6 @@ struct PromWriteV2Error {
type PromWriteBatch = (QueryContextRef, RowInsertRequests);
-#[async_trait]
-trait PromWriteBatcher: Send + Sync {
- async fn submit(&self, requests: RowInsertRequests, ctx: QueryContextRef) -> Result;
-}
-
-#[async_trait]
-impl PromWriteBatcher for PendingRowsBatcher {
- async fn submit(&self, requests: RowInsertRequests, ctx: QueryContextRef) -> Result {
- PendingRowsBatcher::submit(self, requests, ctx).await
- }
-}
-
fn into_prom_write_batches(req: ContextReq, query_ctx: QueryContextRef) -> Vec {
req.as_req_iter(query_ctx).collect()
}
@@ -387,7 +375,7 @@ async fn preflight_prometheus_rows(
/// sample/histogram headers even when a later table write fails.
async fn write_prometheus_rows_with_progress(
prom_store_handler: PromStoreProtocolHandlerRef,
- pending_rows_batcher: Option>,
+ pending_rows_batcher: Option,
prom_store_with_metric_engine: bool,
mut batches: Vec,
) -> std::result::Result {
@@ -401,7 +389,7 @@ async fn write_prometheus_rows_with_progress(
let mut rows_written = 0;
for (temp_ctx, reqs) in batches {
let rows = batcher
- .submit(reqs, temp_ctx)
+ .submit(reqs, temp_ctx, MetricRowBatchProtocol::Prometheus)
.await
.map_err(|error| PromWriteError {
error,
@@ -453,7 +441,7 @@ async fn write_prometheus_rows_with_progress(
async fn write_prometheus_v2_rows_with_progress(
prom_store_handler: PromStoreProtocolHandlerRef,
- pending_rows_batcher: Option>,
+ pending_rows_batcher: Option,
prom_store_with_metric_engine: bool,
sample_batches: Vec,
histogram_batches: Vec,
@@ -539,13 +527,18 @@ async fn write_prometheus_v2_rows_with_progress(
})
}
-async fn write_batched_prometheus_v2_rows_with_progress(
+async fn write_batched_prometheus_v2_rows_with_progress(
prom_store_handler: PromStoreProtocolHandlerRef,
- batcher: &B,
+ batcher: &dyn MetricRowBatcher,
prom_store_with_metric_engine: bool,
sample_batches: Vec,
histogram_batches: Vec,
) -> std::result::Result {
+ if !histogram_batches.is_empty() {
+ METRIC_ROW_BATCH_ELIGIBILITY_FALLBACKS
+ .with_label_values(&[MetricRowBatchProtocol::Prometheus.as_str()])
+ .inc();
+ }
let sample_batch_count = sample_batches.len();
let mut batches = sample_batches;
batches.extend(histogram_batches);
@@ -563,7 +556,7 @@ async fn write_batched_prometheus_v2_rows_with_progress PromWriteBatch {
@@ -904,15 +966,40 @@ mod tests {
));
}
- struct RecordingPromWriteBatcher {
+ struct RecordingMetricRowBatcher {
events: Arc>>,
+ protocols: Arc>>,
+ rows_written: u64,
}
#[async_trait]
- impl PromWriteBatcher for RecordingPromWriteBatcher {
- async fn submit(&self, requests: RowInsertRequests, _ctx: QueryContextRef) -> Result {
+ impl MetricRowBatcher for RecordingMetricRowBatcher {
+ async fn submit(
+ &self,
+ requests: RowInsertRequests,
+ _ctx: QueryContextRef,
+ protocol: MetricRowBatchProtocol,
+ ) -> Result {
record_write_event(&self.events, "batch", &requests);
- Ok(prom_write_row_count(&requests))
+ self.protocols.lock().unwrap().push(protocol);
+ Ok(self.rows_written)
+ }
+ }
+
+ struct FailingMetricRowBatcher;
+
+ #[async_trait]
+ impl MetricRowBatcher for FailingMetricRowBatcher {
+ async fn submit(
+ &self,
+ _requests: RowInsertRequests,
+ _ctx: QueryContextRef,
+ _protocol: MetricRowBatchProtocol,
+ ) -> Result {
+ Err(error::InternalSnafu {
+ err_msg: "metric row batcher failed".to_string(),
+ }
+ .build())
}
}
diff --git a/src/servers/src/metrics.rs b/src/servers/src/metrics.rs
index 0d70540be31..4b56185bd7d 100644
--- a/src/servers/src/metrics.rs
+++ b/src/servers/src/metrics.rs
@@ -48,6 +48,7 @@ pub(crate) const METRIC_VERSION_LABEL: &str = "version";
pub(crate) const METRIC_SUCCESS_VALUE: &str = "success";
pub(crate) const METRIC_FAILURE_VALUE: &str = "failure";
+pub(crate) const METRIC_CANCELLED_VALUE: &str = "cancelled";
lazy_static! {
@@ -192,6 +193,27 @@ lazy_static! {
vec![0.0005, 0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1.0, 5.0, 10.0, 60.0]
)
.unwrap();
+ pub static ref METRIC_ROW_BATCH_SUBMISSIONS: IntCounterVec = register_int_counter_vec!(
+ "greptime_servers_metric_row_batch_submissions_total",
+ "Total metric row batch submissions",
+ &[METRIC_PROTOCOL_LABEL]
+ )
+ .unwrap();
+ pub static ref METRIC_ROW_BATCH_ELIGIBILITY_FALLBACKS: IntCounterVec =
+ register_int_counter_vec!(
+ "greptime_servers_metric_row_batch_eligibility_fallbacks_total",
+ "Total metric requests that fell back because their rows were not batchable",
+ &[METRIC_PROTOCOL_LABEL]
+ )
+ .unwrap();
+ pub static ref METRIC_ROW_BATCH_WAIT_FLUSH_RESULT_ELAPSED: HistogramVec =
+ register_histogram_vec!(
+ "greptime_servers_metric_row_batch_wait_flush_result_elapsed",
+ "Elapsed time waiting for metric row batch flush results in seconds by result",
+ &[METRIC_PROTOCOL_LABEL, METRIC_RESULT_LABEL],
+ vec![0.0005, 0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1.0, 5.0, 10.0, 60.0]
+ )
+ .unwrap();
/// Http prometheus read duration per database.
pub static ref METRIC_HTTP_PROM_STORE_READ_ELAPSED: HistogramVec = register_histogram_vec!(
"greptime_servers_http_prometheus_read_elapsed",
diff --git a/src/servers/src/otel_arrow.rs b/src/servers/src/otel_arrow.rs
index 730020161d3..65ea169d1a6 100644
--- a/src/servers/src/otel_arrow.rs
+++ b/src/servers/src/otel_arrow.rs
@@ -32,6 +32,7 @@ use tonic::{Request, Response, Status, Streaming};
use crate::error;
use crate::grpc::context_auth;
+use crate::pending_rows_batcher::MetricRowBatcherRef;
use crate::query_handler::{MetricsIngestOutcome, OpenTelemetryProtocolHandlerRef};
const EXPONENTIAL_HISTOGRAM_UNSUPPORTED: &str = "OTel Arrow exponential histograms are unsupported because the Arrow wire format omits zero_threshold";
@@ -39,17 +40,33 @@ const EXPONENTIAL_HISTOGRAM_UNSUPPORTED: &str = "OTel Arrow exponential histogra
pub struct OtelArrowServiceHandler {
handler: T,
user_provider: Option,
+ with_metric_engine: bool,
+ metric_row_batcher: Option,
}
impl OtelArrowServiceHandler {
- pub fn new(handler: T, user_provider: Option) -> Self {
+ pub fn new(
+ handler: T,
+ user_provider: Option,
+ with_metric_engine: bool,
+ metric_row_batcher: Option,
+ ) -> Self {
Self {
handler,
user_provider,
+ with_metric_engine,
+ metric_row_batcher,
}
}
}
+fn otel_arrow_metric_context(with_metric_engine: bool) -> OtlpMetricCtx {
+ OtlpMetricCtx {
+ with_metric_engine,
+ ..Default::default()
+ }
+}
+
fn batch_status(
batch_id: i64,
outcome: MetricsIngestOutcome,
@@ -90,11 +107,14 @@ impl ArrowMetricsService for OtelArrowServiceHandler outcome,
Err(error::Error::InvalidOtlpMetricInput { reason }) => {
let _ = sender
@@ -212,4 +235,10 @@ mod tests {
assert_eq!(ArrowStatusCode::InvalidArgument as i32, status.status_code);
assert_eq!(EXPONENTIAL_HISTOGRAM_UNSUPPORTED, status.status_message);
}
+
+ #[test]
+ fn otel_arrow_metric_context_uses_metric_engine_setting() {
+ assert!(otel_arrow_metric_context(true).with_metric_engine);
+ assert!(!otel_arrow_metric_context(false).with_metric_engine);
+ }
}
diff --git a/src/servers/src/pending_rows_batcher.rs b/src/servers/src/pending_rows_batcher.rs
index 03eb90dce28..3c580d537de 100644
--- a/src/servers/src/pending_rows_batcher.rs
+++ b/src/servers/src/pending_rows_batcher.rs
@@ -22,7 +22,7 @@ use api::v1::meta::Peer;
use api::v1::region::{
BulkInsertRequest, RegionRequest, RegionRequestHeader, bulk_insert_request, region_request,
};
-use api::v1::{ArrowIpc, ColumnSchema, RowInsertRequests, Rows};
+use api::v1::{ArrowIpc, ColumnDataType, ColumnSchema, RowInsertRequests, Rows, SemanticType};
use arrow::array::Array;
use arrow::compute::{concat_batches, filter_record_batch};
use arrow::datatypes::{DataType as ArrowDataType, Schema as ArrowSchema, TimeUnit};
@@ -53,11 +53,13 @@ use crate::error;
use crate::error::{Error, Result};
use crate::metrics::{
FLOW_NOTIFICATION_DROPPED, FLUSH_DROPPED_ROWS, FLUSH_ELAPSED, FLUSH_FAILURES, FLUSH_ROWS,
- FLUSH_TOTAL, PENDING_BATCHES, PENDING_ROWS, PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED,
- PENDING_ROWS_BATCH_INGEST_STAGE_ELAPSED, PENDING_WORKERS,
+ FLUSH_TOTAL, METRIC_CANCELLED_VALUE, METRIC_FAILURE_VALUE, METRIC_ROW_BATCH_SUBMISSIONS,
+ METRIC_ROW_BATCH_WAIT_FLUSH_RESULT_ELAPSED, METRIC_SUCCESS_VALUE, PENDING_BATCHES,
+ PENDING_ROWS, PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED, PENDING_ROWS_BATCH_INGEST_STAGE_ELAPSED,
+ PENDING_WORKERS,
};
use crate::prom_row_builder::{
- build_prom_create_table_schema_from_proto, identify_missing_columns_from_proto,
+ build_metric_create_table_schema_from_proto, identify_missing_columns_from_proto,
rows_to_aligned_record_batch,
};
@@ -69,9 +71,8 @@ const PENDING_ROWS_BATCH_SYNC_ENV: &str = "PENDING_ROWS_BATCH_SYNC";
/// before replying to the client (synchronous mode), controlled by the
/// `PENDING_ROWS_BATCH_SYNC` environment variable and defaulting to `true`.
///
-/// Callers that reason about how long a remote write request may block (e.g.
-/// the frontend HTTP timeout fallback) must consult this instead of
-/// duplicating the env lookup.
+/// Startup code must read this once and propagate the resolved mode to batcher
+/// construction and callers that derive timeout behavior.
pub fn pending_rows_batch_sync_enabled() -> bool {
std::env::var(PENDING_ROWS_BATCH_SYNC_ENV)
.ok()
@@ -79,9 +80,113 @@ pub fn pending_rows_batch_sync_enabled() -> bool {
.and_then(|v| v.parse::().ok())
.unwrap_or(true)
}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum PendingRowsBatchMode {
+ Synchronous,
+ Asynchronous,
+}
+
+impl PendingRowsBatchMode {
+ pub fn is_synchronous(self) -> bool {
+ matches!(self, Self::Synchronous)
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct PendingRowsBatcherTuning {
+ pub flush_interval: Duration,
+ pub max_batch_rows: usize,
+ pub max_concurrent_flushes: usize,
+ pub worker_channel_capacity: usize,
+ pub max_inflight_requests: usize,
+ pub flow_notification_queue_capacity: NonZeroUsize,
+}
+
+#[derive(Debug, Clone)]
+pub struct PendingRowsBatcherOptions {
+ tuning: PendingRowsBatcherTuning,
+}
+
+impl PendingRowsBatcherOptions {
+ pub fn try_new(tuning: PendingRowsBatcherTuning) -> Option {
+ if tuning.flush_interval.is_zero()
+ || tuning.max_batch_rows == 0
+ || tuning.max_concurrent_flushes == 0
+ || tuning.worker_channel_capacity == 0
+ || tuning.max_inflight_requests == 0
+ {
+ return None;
+ }
+
+ Some(Self { tuning })
+ }
+
+ pub fn flush_interval(&self) -> Duration {
+ self.tuning.flush_interval
+ }
+}
const WORKER_IDLE_TIMEOUT_MULTIPLIER: u32 = 3;
const PHYSICAL_REGION_ESSENTIAL_COLUMN_COUNT: usize = 3;
const MAX_CONCURRENT_FLOW_NOTIFICATIONS: usize = 8;
+
+/// Batches protocol-neutral metric rows for ingestion.
+#[async_trait]
+pub trait MetricRowBatcher: Send + Sync {
+ /// Submits row insert requests and returns the number of accepted rows.
+ async fn submit(
+ &self,
+ requests: RowInsertRequests,
+ ctx: QueryContextRef,
+ protocol: MetricRowBatchProtocol,
+ ) -> Result;
+}
+
+pub type MetricRowBatcherRef = Arc;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum MetricRowBatchProtocol {
+ Prometheus,
+ Otlp,
+}
+
+impl MetricRowBatchProtocol {
+ pub const fn as_str(self) -> &'static str {
+ match self {
+ Self::Prometheus => "prometheus",
+ Self::Otlp => "otlp",
+ }
+ }
+}
+
+struct WaitFlushResultTimer {
+ started_at: Instant,
+ protocol: MetricRowBatchProtocol,
+ result_label: &'static str,
+}
+
+impl WaitFlushResultTimer {
+ fn start(protocol: MetricRowBatchProtocol) -> Self {
+ Self {
+ started_at: Instant::now(),
+ protocol,
+ result_label: METRIC_CANCELLED_VALUE,
+ }
+ }
+
+ fn finish_with_result(mut self, result_label: &'static str) {
+ self.result_label = result_label;
+ }
+}
+
+impl Drop for WaitFlushResultTimer {
+ fn drop(&mut self) {
+ METRIC_ROW_BATCH_WAIT_FLUSH_RESULT_ELAPSED
+ .with_label_values(&[self.protocol.as_str(), self.result_label])
+ .observe(self.started_at.elapsed().as_secs_f64());
+ }
+}
+
#[async_trait]
pub trait PendingRowsSchemaAlterer: Send + Sync {
/// Batch-create multiple logical tables that are missing.
@@ -97,7 +202,7 @@ pub trait PendingRowsSchemaAlterer: Send + Sync {
/// Batch-alter multiple logical tables to add missing tag columns.
/// Each entry is `(table_name, missing_column_names)`.
- async fn add_missing_prom_tag_columns_batch(
+ async fn add_missing_metric_tag_columns_batch(
&self,
catalog: &str,
schema: &str,
@@ -145,6 +250,10 @@ pub trait PhysicalFlushNodeRequester: Send + Sync {
) -> Result;
}
+type PhysicalFlushCatalogProviderRef = Arc;
+type PhysicalFlushPartitionProviderRef = Arc;
+type PhysicalFlushNodeRequesterRef = Arc;
+
#[derive(Clone)]
struct CatalogManagerPhysicalFlushAdapter {
catalog_manager: CatalogManagerRef,
@@ -341,83 +450,195 @@ fn batch_key_from_ctx(ctx: &QueryContextRef) -> BatchKey {
}
}
-/// Prometheus remote write pending rows batcher.
+/// Returns whether all non-empty inserts in `requests` can use the scalar metric batcher.
+///
+/// Empty inserts are ignored to match [`PendingRowsBatcher::submit`] filtering, and a request
+/// without rows is not batchable. Valid schemas outside the scalar metric shape return `false`,
+/// while malformed scalar row widths or value variants return an error before callers perform DDL.
+pub fn is_scalar_metric_batchable(requests: &RowInsertRequests) -> Result {
+ let mut has_rows = false;
+ let mut batchable = true;
+
+ for request in &requests.inserts {
+ let Some(rows) = &request.rows else {
+ continue;
+ };
+ if rows.rows.is_empty() {
+ continue;
+ }
+ has_rows = true;
+ if !is_scalar_metric_schema(&rows.schema) {
+ batchable = false;
+ continue;
+ }
+ validate_row_values(&request.table_name, rows)?;
+ }
+
+ Ok(has_rows && batchable)
+}
+
+fn validate_row_values(table_name: &str, rows: &Rows) -> Result<()> {
+ let datatypes = rows
+ .schema
+ .iter()
+ .map(|column| parse_column_datatype(table_name, column))
+ .collect::>>()?;
+
+ for (row_index, row) in rows.rows.iter().enumerate() {
+ ensure!(
+ row.values.len() == rows.schema.len(),
+ error::InvalidParameterSnafu {
+ reason: format!(
+ "Column count mismatch in table '{}' row {}, expected {}, got {}",
+ table_name,
+ row_index,
+ rows.schema.len(),
+ row.values.len()
+ )
+ }
+ );
+
+ for (column_index, (value, datatype)) in row.values.iter().zip(&datatypes).enumerate() {
+ let Some(value_datatype) = api::helper::proto_value_type(value) else {
+ continue;
+ };
+ ensure!(
+ value_datatype == *datatype,
+ error::InvalidParameterSnafu {
+ reason: format!(
+ "Value variant mismatch in table '{}' row {}, column '{}' (index {}), expected {:?}, got {:?}",
+ table_name,
+ row_index,
+ rows.schema[column_index].column_name,
+ column_index,
+ datatype,
+ value_datatype
+ )
+ }
+ );
+ }
+ }
+
+ Ok(())
+}
+
+fn parse_column_datatype(table_name: &str, column: &ColumnSchema) -> Result {
+ ColumnDataType::try_from(column.datatype).map_err(|_| {
+ error::InvalidParameterSnafu {
+ reason: format!(
+ "Unknown datatype {} for column '{}' in table '{}'",
+ column.datatype, column.column_name, table_name
+ ),
+ }
+ .build()
+ })
+}
+
+fn is_scalar_metric_schema(schema: &[ColumnSchema]) -> bool {
+ let timestamp_count = schema
+ .iter()
+ .filter(|column| {
+ column.datatype == ColumnDataType::TimestampMillisecond as i32
+ && column.semantic_type == SemanticType::Timestamp as i32
+ })
+ .count();
+ let field_count = schema
+ .iter()
+ .filter(|column| {
+ column.datatype == ColumnDataType::Float64 as i32
+ && column.semantic_type == SemanticType::Field as i32
+ })
+ .count();
+
+ timestamp_count == 1
+ && field_count == 1
+ && schema.iter().all(|column| {
+ (column.datatype == ColumnDataType::TimestampMillisecond as i32
+ && column.semantic_type == SemanticType::Timestamp as i32)
+ || (column.datatype == ColumnDataType::Float64 as i32
+ && column.semantic_type == SemanticType::Field as i32)
+ || (column.datatype == ColumnDataType::String as i32
+ && column.semantic_type == SemanticType::Tag as i32)
+ })
+ && schema
+ .iter()
+ .all(|column| column.datatype_extension.is_none())
+}
+
+/// Metric pending rows batcher.
pub struct PendingRowsBatcher {
workers: Arc>,
flush_interval: Duration,
max_batch_rows: usize,
- partition_manager: PartitionRuleManagerRef,
- node_manager: NodeManagerRef,
+ partition_provider: PhysicalFlushPartitionProviderRef,
+ node_requester: PhysicalFlushNodeRequesterRef,
catalog_manager: CatalogManagerRef,
+ flush_catalog_provider: PhysicalFlushCatalogProviderRef,
flow_notification_tx: mpsc::Sender,
flush_semaphore: Arc,
inflight_semaphore: Arc,
worker_channel_capacity: usize,
- prom_store_with_metric_engine: bool,
+ with_metric_engine: bool,
schema_alterer: PendingRowsSchemaAltererRef,
- pending_rows_batch_sync: bool,
+ batch_mode: PendingRowsBatchMode,
shutdown: broadcast::Sender<()>,
}
impl PendingRowsBatcher {
#[allow(clippy::too_many_arguments)]
- pub fn try_new(
+ pub fn new(
partition_manager: PartitionRuleManagerRef,
node_manager: NodeManagerRef,
catalog_manager: CatalogManagerRef,
table_flownode_set_cache: TableFlownodeSetCacheRef,
- prom_store_with_metric_engine: bool,
+ with_metric_engine: bool,
schema_alterer: PendingRowsSchemaAltererRef,
- flush_interval: Duration,
- max_batch_rows: usize,
- max_concurrent_flushes: usize,
- worker_channel_capacity: usize,
- max_inflight_requests: usize,
- flow_notification_queue_capacity: NonZeroUsize,
- ) -> Option> {
- // Disable the batcher if flush is disabled or configuration is invalid.
- // Zero values for these knobs either cause panics (e.g., zero-capacity channels)
- // or deadlocks (e.g., semaphores with no permits).
- if flush_interval.is_zero()
- || max_batch_rows == 0
- || max_concurrent_flushes == 0
- || worker_channel_capacity == 0
- || max_inflight_requests == 0
- {
- return None;
- }
-
+ options: PendingRowsBatcherOptions,
+ batch_mode: PendingRowsBatchMode,
+ ) -> Arc {
let (shutdown, _) = broadcast::channel(1);
- let pending_rows_batch_sync = pending_rows_batch_sync_enabled();
let workers = Arc::new(DashMap::new());
PENDING_WORKERS.set(workers.len() as i64);
let (flow_notification_tx, flow_notification_rx) =
- mpsc::channel(flow_notification_queue_capacity.get());
+ mpsc::channel(options.tuning.flow_notification_queue_capacity.get());
start_flow_notification_worker(
flow_notification_rx,
table_flownode_set_cache,
node_manager.clone(),
);
- Some(Arc::new(Self {
+ Arc::new(Self {
workers,
- flush_interval,
- max_batch_rows,
- partition_manager,
- node_manager,
+ flush_interval: options.tuning.flush_interval,
+ max_batch_rows: options.tuning.max_batch_rows,
+ partition_provider: Arc::new(PartitionManagerPhysicalFlushAdapter {
+ partition_manager,
+ }),
+ node_requester: Arc::new(NodeManagerPhysicalFlushAdapter { node_manager }),
+ flush_catalog_provider: Arc::new(CatalogManagerPhysicalFlushAdapter {
+ catalog_manager: catalog_manager.clone(),
+ }),
catalog_manager,
flow_notification_tx,
- prom_store_with_metric_engine,
+ with_metric_engine,
schema_alterer,
- flush_semaphore: Arc::new(Semaphore::new(max_concurrent_flushes)),
- inflight_semaphore: Arc::new(Semaphore::new(max_inflight_requests)),
- worker_channel_capacity,
- pending_rows_batch_sync,
+ flush_semaphore: Arc::new(Semaphore::new(options.tuning.max_concurrent_flushes)),
+ inflight_semaphore: Arc::new(Semaphore::new(options.tuning.max_inflight_requests)),
+ worker_channel_capacity: options.tuning.worker_channel_capacity,
+ batch_mode,
shutdown,
- }))
+ })
}
- pub async fn submit(&self, requests: RowInsertRequests, ctx: QueryContextRef) -> Result {
+ pub async fn submit(
+ &self,
+ requests: RowInsertRequests,
+ ctx: QueryContextRef,
+ protocol: MetricRowBatchProtocol,
+ ) -> Result {
+ METRIC_ROW_BATCH_SUBMISSIONS
+ .with_label_values(&[protocol.as_str()])
+ .inc();
let (table_batches, total_rows) = {
let _timer = PENDING_ROWS_BATCH_INGEST_STAGE_ELAPSED
.with_label_values(&["submit_build_and_align"])
@@ -479,18 +700,23 @@ impl PendingRowsBatcher {
}
}
- if self.pending_rows_batch_sync {
- let result = {
+ if self.batch_mode.is_synchronous() {
+ let wait_timer = WaitFlushResultTimer::start(protocol);
+ let flush_result = {
let _timer = PENDING_ROWS_BATCH_INGEST_STAGE_ELAPSED
.with_label_values(&["submit_wait_flush_result"])
.start_timer();
response_rx
.await
- .map_err(|_| error::BatcherChannelClosedSnafu.build())?
+ .map_err(|_| error::BatcherChannelClosedSnafu.build())
+ .and_then(|waiter_result| waiter_result.context(error::SubmitBatchSnafu))
};
- result
- .context(error::SubmitBatchSnafu)
- .map(|()| total_rows as u64)
+ wait_timer.finish_with_result(if flush_result.is_ok() {
+ METRIC_SUCCESS_VALUE
+ } else {
+ METRIC_FAILURE_VALUE
+ });
+ flush_result.map(|()| total_rows as u64)
} else {
Ok(total_rows as u64)
}
@@ -633,7 +859,7 @@ impl PendingRowsBatcher {
let _timer = PENDING_ROWS_BATCH_INGEST_STAGE_ELAPSED
.with_label_values(&["align_build_create_table_schema"])
.start_timer();
- build_prom_create_table_schema_from_proto(rows_schema)?
+ build_metric_create_table_schema_from_proto(rows_schema)?
};
plan.tables_to_create
.push(((*table_name).to_string(), request_schema));
@@ -672,7 +898,7 @@ impl PendingRowsBatcher {
catalog,
schema,
&create_refs,
- self.prom_store_with_metric_engine,
+ self.with_metric_engine,
ctx.clone(),
)
.await?;
@@ -771,7 +997,7 @@ impl PendingRowsBatcher {
.with_label_values(&["align_batch_add_missing_columns"])
.start_timer();
self.schema_alterer
- .add_missing_prom_tag_columns_batch(catalog, schema, &alter_refs, ctx.clone())
+ .add_missing_metric_tag_columns_batch(catalog, schema, &alter_refs, ctx.clone())
.await?;
}
@@ -868,16 +1094,17 @@ impl PendingRowsBatcher {
.flush_interval
.checked_mul(WORKER_IDLE_TIMEOUT_MULTIPLIER)
.unwrap_or(self.flush_interval);
+ let shutdown_rx = self.shutdown.subscribe();
start_worker(
key,
worker.tx.clone(),
self.workers.clone(),
rx,
- self.shutdown.clone(),
- self.partition_manager.clone(),
- self.node_manager.clone(),
- self.catalog_manager.clone(),
+ shutdown_rx,
+ self.partition_provider.clone(),
+ self.node_requester.clone(),
+ self.flush_catalog_provider.clone(),
self.flow_notification_tx.clone(),
self.flush_interval,
worker_idle_timeout,
@@ -889,6 +1116,18 @@ impl PendingRowsBatcher {
}
}
+#[async_trait]
+impl MetricRowBatcher for PendingRowsBatcher {
+ async fn submit(
+ &self,
+ requests: RowInsertRequests,
+ ctx: QueryContextRef,
+ protocol: MetricRowBatchProtocol,
+ ) -> Result {
+ PendingRowsBatcher::submit(self, requests, ctx, protocol).await
+ }
+}
+
impl Drop for PendingRowsBatcher {
fn drop(&mut self) {
let _ = self.shutdown.send(());
@@ -930,10 +1169,10 @@ fn start_worker(
worker_tx: mpsc::Sender,
workers: Arc>,
mut rx: mpsc::Receiver,
- shutdown: broadcast::Sender<()>,
- partition_manager: PartitionRuleManagerRef,
- node_manager: NodeManagerRef,
- catalog_manager: CatalogManagerRef,
+ mut shutdown_rx: broadcast::Receiver<()>,
+ partition_provider: PhysicalFlushPartitionProviderRef,
+ node_requester: PhysicalFlushNodeRequesterRef,
+ catalog_provider: PhysicalFlushCatalogProviderRef,
flow_notification_tx: mpsc::Sender,
flush_interval: Duration,
worker_idle_timeout: Duration,
@@ -944,13 +1183,13 @@ fn start_worker(
let mut batch = None;
let flush_timer = tokio::time::sleep(flush_interval);
tokio::pin!(flush_timer);
- let mut shutdown_rx = shutdown.subscribe();
let idle_deadline = tokio::time::Instant::now() + worker_idle_timeout;
let idle_timer = tokio::time::sleep_until(idle_deadline);
tokio::pin!(idle_timer);
loop {
tokio::select! {
+ biased;
cmd = rx.recv() => {
match cmd {
Some(WorkerCommand::Submit { table_batches, total_rows, ctx, response_tx, _permit }) => {
@@ -981,9 +1220,9 @@ fn start_worker(
&& let Some(flush) = drain_batch(&mut batch) {
spawn_flush(
flush,
- partition_manager.clone(),
- node_manager.clone(),
- catalog_manager.clone(),
+ partition_provider.clone(),
+ node_requester.clone(),
+ catalog_provider.clone(),
flow_notification_tx.clone(),
flush_semaphore.clone(),
).await;
@@ -991,11 +1230,11 @@ fn start_worker(
}
None => {
if let Some(flush) = drain_batch(&mut batch) {
- flush_batch_with_managers(
+ flush_batch(
flush,
- partition_manager.clone(),
- node_manager.clone(),
- catalog_manager.clone(),
+ partition_provider.as_ref(),
+ node_requester.as_ref(),
+ catalog_provider.as_ref(),
flow_notification_tx.clone(),
).await;
}
@@ -1030,9 +1269,9 @@ fn start_worker(
if let Some(flush) = drain_batch(&mut batch) {
spawn_flush(
flush,
- partition_manager.clone(),
- node_manager.clone(),
- catalog_manager.clone(),
+ partition_provider.clone(),
+ node_requester.clone(),
+ catalog_provider.clone(),
flow_notification_tx.clone(),
flush_semaphore.clone(),
).await;
@@ -1040,11 +1279,11 @@ fn start_worker(
}
_ = shutdown_rx.recv() => {
if let Some(flush) = drain_batch(&mut batch) {
- flush_batch_with_managers(
+ flush_batch(
flush,
- partition_manager.clone(),
- node_manager.clone(),
- catalog_manager.clone(),
+ partition_provider.as_ref(),
+ node_requester.as_ref(),
+ catalog_provider.as_ref(),
flow_notification_tx.clone(),
).await;
}
@@ -1103,9 +1342,9 @@ fn drain_batch(batch: &mut Option) -> Option {
async fn spawn_flush(
flush: FlushBatch,
- partition_manager: PartitionRuleManagerRef,
- node_manager: NodeManagerRef,
- catalog_manager: CatalogManagerRef,
+ partition_provider: PhysicalFlushPartitionProviderRef,
+ node_requester: PhysicalFlushNodeRequesterRef,
+ catalog_provider: PhysicalFlushCatalogProviderRef,
flow_notification_tx: mpsc::Sender,
semaphore: Arc,
) {
@@ -1113,11 +1352,11 @@ async fn spawn_flush(
Ok(permit) => {
tokio::spawn(async move {
let _permit = permit;
- flush_batch_with_managers(
+ flush_batch(
flush,
- partition_manager,
- node_manager,
- catalog_manager,
+ partition_provider.as_ref(),
+ node_requester.as_ref(),
+ catalog_provider.as_ref(),
flow_notification_tx,
)
.await;
@@ -1125,11 +1364,11 @@ async fn spawn_flush(
}
Err(err) => {
warn!(err; "Flush semaphore closed, flushing inline");
- flush_batch_with_managers(
+ flush_batch(
flush,
- partition_manager,
- node_manager,
- catalog_manager,
+ partition_provider.as_ref(),
+ node_requester.as_ref(),
+ catalog_provider.as_ref(),
flow_notification_tx,
)
.await;
@@ -1317,28 +1556,6 @@ async fn flush_region_writes_concurrently(
Ok(affected_rows)
}
-async fn flush_batch_with_managers(
- flush: FlushBatch,
- partition_manager: PartitionRuleManagerRef,
- node_manager: NodeManagerRef,
- catalog_manager: CatalogManagerRef,
- flow_notification_tx: mpsc::Sender,
-) {
- let partition_provider = PartitionManagerPhysicalFlushAdapter { partition_manager };
- let node_requester = NodeManagerPhysicalFlushAdapter {
- node_manager: node_manager.clone(),
- };
- let catalog_provider = CatalogManagerPhysicalFlushAdapter { catalog_manager };
- flush_batch(
- flush,
- &partition_provider,
- &node_requester,
- &catalog_provider,
- flow_notification_tx,
- )
- .await;
-}
-
async fn flush_batch(
flush: FlushBatch,
partition_manager: &(impl PhysicalFlushPartitionProvider + ?Sized),
@@ -1876,6 +2093,7 @@ mod tests {
use std::sync::{Arc, Mutex};
use std::time::Duration;
+ use api::helper::ColumnDataTypeWrapper;
use api::region::RegionResponse;
use api::v1::flow::{DirtyWindowRequests, FlowRequest, FlowResponse};
use api::v1::meta::Peer;
@@ -1883,7 +2101,7 @@ mod tests {
use api::v1::value::ValueData;
use api::v1::{
ColumnDataType, ColumnSchema, Row, RowInsertRequest, RowInsertRequests, Rows, SemanticType,
- Value,
+ StructValue, Value,
};
use arrow::array::{BinaryArray, BooleanArray, StringArray, TimestampMillisecondArray};
use arrow::datatypes::{DataType as ArrowDataType, Field, Schema as ArrowSchema};
@@ -1907,6 +2125,7 @@ mod tests {
BatchPutRequest, BatchPutResponse, DeleteRangeRequest, DeleteRangeResponse, PutRequest,
PutResponse, RangeRequest, RangeResponse,
};
+ use common_query::native_histogram::native_histogram_column_schema;
use common_query::request::QueryRequest;
use common_recordbatch::SendableRecordBatchStream;
use dashmap::DashMap;
@@ -1920,24 +2139,30 @@ mod tests {
use snafu::ResultExt;
use store_api::storage::RegionId;
use table::metadata::TableId;
+ use table::test_util::EmptyTable;
use table::test_util::table_info::test_table_info;
use tokio::sync::{Notify, Semaphore, broadcast, mpsc, oneshot};
use tokio::time::{advance, sleep};
use super::{
- BatchKey, Error, FlushBatch, FlushRegionWrite, FlushWaiter, PendingBatch,
- PendingRowsBatcher, PendingWorker, PhysicalFlushCatalogProvider,
- PhysicalFlushNodeRequester, PhysicalFlushPartitionProvider, PhysicalTableMetadata,
- PlannedRegionBatch, RecordBatchWithTsIdx, ResolvedRegionBatch, TableBatch, WorkerCommand,
- columns_taxonomy, drain_batch, encode_region_write_requests, extract_timestamps,
- flush_batch, flush_batch_physical, flush_region_writes_concurrently, greptime_timestamp,
- notify_flow_dirty_windows_after_flush, plan_region_batches, remove_worker_if_same_channel,
- should_close_worker_on_idle_timeout, should_dispatch_concurrently,
- start_flow_notification_worker, start_worker, strip_partition_columns_from_batch,
- transform_logical_batches_to_physical, try_enqueue_flow_notification,
+ BatchKey, Error, FlushBatch, FlushRegionWrite, FlushWaiter, MetricRowBatchProtocol,
+ PendingBatch, PendingRowsBatchMode, PendingRowsBatcher, PendingRowsSchemaAlterer,
+ PendingWorker, PhysicalFlushCatalogProvider, PhysicalFlushNodeRequester,
+ PhysicalFlushPartitionProvider, PhysicalTableMetadata, PlannedRegionBatch,
+ RecordBatchWithTsIdx, ResolvedRegionBatch, TableBatch, WorkerCommand, columns_taxonomy,
+ drain_batch, encode_region_write_requests, extract_timestamps, flush_batch,
+ flush_batch_physical, flush_region_writes_concurrently, greptime_timestamp,
+ is_scalar_metric_batchable, notify_flow_dirty_windows_after_flush, plan_region_batches,
+ remove_worker_if_same_channel, should_close_worker_on_idle_timeout,
+ should_dispatch_concurrently, start_flow_notification_worker, start_worker,
+ strip_partition_columns_from_batch, transform_logical_batches_to_physical,
+ try_enqueue_flow_notification,
};
use crate::error;
- use crate::metrics::FLOW_NOTIFICATION_DROPPED;
+ use crate::metrics::{
+ FLOW_NOTIFICATION_DROPPED, METRIC_FAILURE_VALUE, METRIC_ROW_BATCH_SUBMISSIONS,
+ METRIC_ROW_BATCH_WAIT_FLUSH_RESULT_ELAPSED, METRIC_SUCCESS_VALUE,
+ };
use crate::prom_row_builder::rows_to_aligned_record_batch;
fn mock_rows(row_count: usize, schema_name: &str) -> Rows {
@@ -1950,6 +2175,220 @@ mod tests {
}
}
+ fn scalar_insert(table_name: &str) -> RowInsertRequest {
+ RowInsertRequest {
+ table_name: table_name.to_string(),
+ rows: Some(Rows {
+ schema: vec![
+ ColumnSchema {
+ column_name: "ts".to_string(),
+ datatype: ColumnDataType::TimestampMillisecond as i32,
+ semantic_type: SemanticType::Timestamp as i32,
+ ..Default::default()
+ },
+ ColumnSchema {
+ column_name: "value".to_string(),
+ datatype: ColumnDataType::Float64 as i32,
+ semantic_type: SemanticType::Field as i32,
+ ..Default::default()
+ },
+ ColumnSchema {
+ column_name: "host".to_string(),
+ datatype: ColumnDataType::String as i32,
+ semantic_type: SemanticType::Tag as i32,
+ ..Default::default()
+ },
+ ],
+ rows: vec![Row {
+ values: vec![
+ Value {
+ value_data: Some(ValueData::TimestampMillisecondValue(1000)),
+ },
+ Value {
+ value_data: Some(ValueData::F64Value(1.0)),
+ },
+ Value {
+ value_data: Some(ValueData::StringValue("host-1".to_string())),
+ },
+ ],
+ }],
+ }),
+ }
+ }
+
+ fn scalar_request() -> RowInsertRequests {
+ RowInsertRequests {
+ inserts: vec![scalar_insert("cpu")],
+ }
+ }
+
+ #[test]
+ fn test_scalar_metric_batchability_accepts_ordinary_table() {
+ assert!(is_scalar_metric_batchable(&scalar_request()).unwrap());
+ }
+
+ #[test]
+ fn test_scalar_metric_batchability_accepts_multiple_logical_tables() {
+ let requests = RowInsertRequests {
+ inserts: vec![scalar_insert("cpu"), scalar_insert("memory")],
+ };
+
+ assert!(is_scalar_metric_batchable(&requests).unwrap());
+ }
+
+ #[test]
+ fn test_scalar_metric_batchability_rejects_native_histogram() {
+ let mut requests = scalar_request();
+ let rows = requests.inserts[0].rows.as_mut().unwrap();
+ rows.schema[1] = native_histogram_column_schema().unwrap();
+ rows.rows[0].values[1].value_data = Some(ValueData::StructValue(StructValue::default()));
+
+ assert!(!is_scalar_metric_batchable(&requests).unwrap());
+ }
+
+ #[test]
+ fn test_scalar_metric_batchability_rejects_datatype_extension() {
+ let mut requests = scalar_request();
+ requests.inserts[0].rows.as_mut().unwrap().schema[1].datatype_extension =
+ Some(Default::default());
+
+ assert!(!is_scalar_metric_batchable(&requests).unwrap());
+ }
+
+ #[test]
+ fn test_scalar_metric_batchability_rejects_multiple_fields() {
+ let mut requests = scalar_request();
+ let rows = requests.inserts[0].rows.as_mut().unwrap();
+ rows.schema.push(ColumnSchema {
+ column_name: "another_value".to_string(),
+ datatype: ColumnDataType::Float64 as i32,
+ semantic_type: SemanticType::Field as i32,
+ ..Default::default()
+ });
+ rows.rows[0].values.push(Value {
+ value_data: Some(ValueData::F64Value(2.0)),
+ });
+
+ assert!(!is_scalar_metric_batchable(&requests).unwrap());
+ }
+
+ #[test]
+ fn test_scalar_metric_batchability_rejects_non_string_tag() {
+ let mut requests = scalar_request();
+ let rows = requests.inserts[0].rows.as_mut().unwrap();
+ rows.schema[2].datatype = ColumnDataType::Int64 as i32;
+ rows.rows[0].values[2].value_data = Some(ValueData::I64Value(1));
+
+ assert!(!is_scalar_metric_batchable(&requests).unwrap());
+ }
+
+ #[test]
+ fn test_scalar_metric_batchability_falls_back_for_vector_binary_value() {
+ let mut requests = scalar_request();
+ let rows = requests.inserts[0].rows.as_mut().unwrap();
+ let (datatype, datatype_extension) = ColumnDataTypeWrapper::vector_datatype(3).into_parts();
+ rows.schema[2].datatype = datatype as i32;
+ rows.schema[2].datatype_extension = datatype_extension;
+ rows.rows[0].values[2].value_data = Some(ValueData::BinaryValue(vec![0; 12]));
+
+ assert!(!is_scalar_metric_batchable(&requests).unwrap());
+ }
+
+ #[test]
+ fn test_scalar_metric_batchability_checks_malformed_scalar_after_unsupported_insert() {
+ let mut vector = scalar_insert("vector");
+ let vector_rows = vector.rows.as_mut().unwrap();
+ let (datatype, datatype_extension) = ColumnDataTypeWrapper::vector_datatype(3).into_parts();
+ vector_rows.schema[2].datatype = datatype as i32;
+ vector_rows.schema[2].datatype_extension = datatype_extension;
+ vector_rows.rows[0].values[2].value_data = Some(ValueData::BinaryValue(vec![0; 12]));
+
+ let mut malformed = scalar_insert("malformed");
+ malformed.rows.as_mut().unwrap().rows[0].values.pop();
+ let requests = RowInsertRequests {
+ inserts: vec![vector, malformed],
+ };
+
+ assert!(is_scalar_metric_batchable(&requests).is_err());
+ }
+
+ #[test]
+ fn test_scalar_metric_batchability_rejects_non_millisecond_timestamp() {
+ let mut requests = scalar_request();
+ let rows = requests.inserts[0].rows.as_mut().unwrap();
+ rows.schema[0].datatype = ColumnDataType::TimestampNanosecond as i32;
+ rows.rows[0].values[0].value_data = Some(ValueData::TimestampNanosecondValue(1_000_000));
+
+ assert!(!is_scalar_metric_batchable(&requests).unwrap());
+ }
+
+ #[test]
+ fn test_scalar_metric_batchability_rejects_missing_timestamp_or_value() {
+ for missing_index in [0, 1] {
+ let mut requests = scalar_request();
+ let rows = requests.inserts[0].rows.as_mut().unwrap();
+ rows.schema.remove(missing_index);
+ rows.rows[0].values.remove(missing_index);
+
+ assert!(!is_scalar_metric_batchable(&requests).unwrap());
+ }
+ }
+
+ #[test]
+ fn test_scalar_metric_batchability_rejects_wrong_semantic_types() {
+ for (column_index, semantic_type) in [
+ (0, SemanticType::Tag),
+ (1, SemanticType::Tag),
+ (2, SemanticType::Field),
+ ] {
+ let mut requests = scalar_request();
+ requests.inserts[0].rows.as_mut().unwrap().schema[column_index].semantic_type =
+ semantic_type as i32;
+
+ assert!(!is_scalar_metric_batchable(&requests).unwrap());
+ }
+ }
+
+ #[test]
+ fn test_scalar_metric_batchability_errors_on_row_width_mismatch() {
+ let mut requests = scalar_request();
+ requests.inserts[0].rows.as_mut().unwrap().rows[0]
+ .values
+ .pop();
+
+ assert!(is_scalar_metric_batchable(&requests).is_err());
+ }
+
+ #[test]
+ fn test_scalar_metric_batchability_errors_on_value_variant_mismatch() {
+ let mut requests = scalar_request();
+ requests.inserts[0].rows.as_mut().unwrap().rows[0].values[2].value_data =
+ Some(ValueData::BoolValue(true));
+
+ assert!(is_scalar_metric_batchable(&requests).is_err());
+ }
+
+ #[test]
+ fn test_scalar_metric_batchability_accepts_null_cell() {
+ let mut requests = scalar_request();
+ requests.inserts[0].rows.as_mut().unwrap().rows[0].values[1].value_data = None;
+
+ assert!(is_scalar_metric_batchable(&requests).unwrap());
+ }
+
+ #[test]
+ fn test_scalar_metric_batchability_treats_empty_requests_as_not_batchable() {
+ assert!(!is_scalar_metric_batchable(&RowInsertRequests::default()).unwrap());
+
+ let requests = RowInsertRequests {
+ inserts: vec![RowInsertRequest {
+ table_name: "empty".to_string(),
+ rows: Some(Rows::default()),
+ }],
+ };
+ assert!(!is_scalar_metric_batchable(&requests).unwrap());
+ }
+
fn mock_tag_batch(tag_name: &str, tag_value: &str, ts: i64, val: f64) -> RecordBatch {
let schema = Arc::new(ArrowSchema::new(vec![
Field::new(
@@ -2344,6 +2783,26 @@ mod tests {
}
}
+ struct BlockingFlushNodeRequester {
+ write_started_tx: Mutex