diff --git a/Cargo.lock b/Cargo.lock
index c02c7fdcc60..9b447169fbc 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2459,7 +2459,9 @@ dependencies = [
"common-macro",
"common-meta",
"greptime-proto",
+ "lazy_static",
"meta-client",
+ "prometheus 0.14.0",
"serde",
"serde_json",
"snafu 0.8.6",
@@ -5604,6 +5606,7 @@ dependencies = [
"lazy_static",
"log-query",
"meta-client",
+ "moka",
"opentelemetry-proto",
"opentelemetry-semantic-conventions 0.31.0",
"operator",
@@ -10860,6 +10863,7 @@ dependencies = [
"chrono-tz",
"common-catalog",
"common-error",
+ "common-frontend",
"common-macro",
"common-meta",
"common-query",
@@ -13522,6 +13526,7 @@ dependencies = [
"common-catalog",
"common-decimal",
"common-error",
+ "common-frontend",
"common-grpc",
"common-macro",
"common-mem-prof",
@@ -15122,6 +15127,7 @@ dependencies = [
"store-api",
"substrait 1.4.0-alpha.0",
"table",
+ "temp-env",
"tempfile",
"time",
"tokio",
diff --git a/config/config.md b/config/config.md
index c538559e7cf..612a5cd43c0 100644
--- a/config/config.md
+++ b/config/config.md
@@ -95,6 +95,7 @@
| `otlp` | -- | -- | OpenTelemetry protocol options. |
| `otlp.enable` | Bool | `true` | Whether to enable OpenTelemetry protocol in HTTP API. |
| `otlp.trace_ingest_chunk_size` | Integer | `512` | Maximum spans per trace ingest chunk. Set to 0 to disable splitting. |
+| `otlp.trace_aux_cache_size` | String | Auto | Estimated memory budget for cached trace service/operation keys per frontend,
shared across all catalogs, schemas, and trace tables. Set to 0 to disable caching.
Defaults to 1/128 of the host or pod memory limit, with a minimum of 32MiB.
Uses 32MiB if memory detection is unavailable.
Excludes cache and allocator overhead; shared table names are charged per entry. |
| `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. |
| `prom_store` | -- | -- | Prometheus remote storage options |
| `prom_store.enable` | Bool | `true` | Whether to enable Prometheus remote write and read in HTTP API. |
@@ -368,6 +369,7 @@
| `otlp` | -- | -- | OpenTelemetry protocol options. |
| `otlp.enable` | Bool | `true` | Whether to enable OpenTelemetry protocol in HTTP API. |
| `otlp.trace_ingest_chunk_size` | Integer | `512` | Maximum spans per trace ingest chunk. Set to 0 to disable splitting. |
+| `otlp.trace_aux_cache_size` | String | Auto | Estimated memory budget for cached trace service/operation keys per frontend,
shared across all catalogs, schemas, and trace tables. Set to 0 to disable caching.
Defaults to 1/128 of the host or pod memory limit, with a minimum of 32MiB.
Uses 32MiB if memory detection is unavailable.
Excludes cache and allocator overhead; shared table names are charged per entry. |
| `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. |
| `prom_store` | -- | -- | Prometheus remote storage options |
| `prom_store.enable` | Bool | `true` | Whether to enable Prometheus remote write and read in HTTP API. |
diff --git a/config/frontend.example.toml b/config/frontend.example.toml
index 21e7bbb40f3..e4ef6e36935 100644
--- a/config/frontend.example.toml
+++ b/config/frontend.example.toml
@@ -310,6 +310,13 @@ enable = true
enable = true
## Maximum spans per trace ingest chunk. Set to 0 to disable splitting.
trace_ingest_chunk_size = 512
+## Estimated memory budget for cached trace service/operation keys per frontend,
+## shared across all catalogs, schemas, and trace tables. Set to 0 to disable caching.
+## Defaults to 1/128 of the host or pod memory limit, with a minimum of 32MiB.
+## Uses 32MiB if memory detection is unavailable.
+## Excludes cache and allocator overhead; shared table names are charged per entry.
+## @toml2docs:none-default="Auto"
+#+ trace_aux_cache_size = "32MiB"
## Whether to synthesize the `greptime_otel_resource_info` table from OTLP metric
## resource attributes, so metrics-only services reach the semantic graph.
experimental_enable_resource_info = false
diff --git a/config/standalone.example.toml b/config/standalone.example.toml
index 7e1f8dcf6b0..c656049c5d2 100644
--- a/config/standalone.example.toml
+++ b/config/standalone.example.toml
@@ -282,6 +282,13 @@ enable = true
enable = true
## Maximum spans per trace ingest chunk. Set to 0 to disable splitting.
trace_ingest_chunk_size = 512
+## Estimated memory budget for cached trace service/operation keys per frontend,
+## shared across all catalogs, schemas, and trace tables. Set to 0 to disable caching.
+## Defaults to 1/128 of the host or pod memory limit, with a minimum of 32MiB.
+## Uses 32MiB if memory detection is unavailable.
+## Excludes cache and allocator overhead; shared table names are charged per entry.
+## @toml2docs:none-default="Auto"
+#+ trace_aux_cache_size = "32MiB"
## Whether to synthesize the `greptime_otel_resource_info` table from OTLP metric
## resource attributes, so metrics-only services reach the semantic graph.
experimental_enable_resource_info = false
diff --git a/src/cmd/tests/load_config_test.rs b/src/cmd/tests/load_config_test.rs
index 1f4c69c8938..00cdafd9b8a 100644
--- a/src/cmd/tests/load_config_test.rs
+++ b/src/cmd/tests/load_config_test.rs
@@ -731,6 +731,29 @@ fn test_example_configs_document_actual_defaults() {
assert!(errors.is_empty(), "{}", errors.join("\n"));
}
+#[test]
+fn test_load_trace_aux_cache_size() {
+ let default = FrontendOptions::default().otlp.trace_aux_cache_size;
+ for (setting, size) in [
+ ("", default),
+ ("trace_aux_cache_size = 0", ReadableSize(0)),
+ ("trace_aux_cache_size = \"1MiB\"", ReadableSize::mb(1)),
+ ] {
+ let config = tempfile::NamedTempFile::new().unwrap();
+ std::fs::write(config.path(), format!("[otlp]\n{setting}\n")).unwrap();
+
+ let frontend =
+ GreptimeOptions::::load_layered_options(config.path().to_str(), "")
+ .unwrap();
+ let standalone =
+ GreptimeOptions::::load_layered_options(config.path().to_str(), "")
+ .unwrap();
+ for options in [frontend.component, standalone.component.frontend_options()] {
+ assert_eq!(options.otlp.trace_aux_cache_size, size);
+ }
+ }
+}
+
#[test]
fn test_load_removed_histogram_options() {
for enabled in [false, true] {
diff --git a/src/common/frontend/Cargo.toml b/src/common/frontend/Cargo.toml
index 26092ae13f7..84a8373dcbb 100644
--- a/src/common/frontend/Cargo.toml
+++ b/src/common/frontend/Cargo.toml
@@ -13,7 +13,9 @@ common-grpc.workspace = true
common-macro.workspace = true
common-meta.workspace = true
greptime-proto.workspace = true
+lazy_static.workspace = true
meta-client.workspace = true
+prometheus.workspace = true
serde.workspace = true
serde_json.workspace = true
snafu.workspace = true
diff --git a/src/common/frontend/src/lib.rs b/src/common/frontend/src/lib.rs
index dacffd19968..b6ec72da1a3 100644
--- a/src/common/frontend/src/lib.rs
+++ b/src/common/frontend/src/lib.rs
@@ -18,6 +18,7 @@ use std::str::FromStr;
use snafu::OptionExt;
pub mod error;
+pub mod metrics;
pub mod selector;
pub mod slow_query_event;
diff --git a/src/common/frontend/src/metrics.rs b/src/common/frontend/src/metrics.rs
new file mode 100644
index 00000000000..7a2c152fa1f
--- /dev/null
+++ b/src/common/frontend/src/metrics.rs
@@ -0,0 +1,117 @@
+// Copyright 2023 Greptime Team
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! Cache lookup counters shared by frontend, pipeline, and protocol handlers.
+//!
+//! The `type` label identifies `otlp_trace_aux`, `otlp_metrics_legacy`,
+//! `pipeline_table`, `pipeline`, `pipeline_original`, `pipeline_failover`, or
+//! `mysql_prepared_stmt`. Counters aggregate across catalogs and connections.
+//! Trace lookups count distinct service/operation keys once per request, excluding
+//! rechecks; legacy metric lookups count each requested metric name. Pipeline
+//! loaders count successful coalesced waiters as hits and failed loads as misses.
+//! Failover counts one outcome after resolving all candidate schemas; an ambiguous
+//! result is a miss. Table and prepared-statement caches count each lookup.
+//! Both outcomes for all known caches are exported from the first metric update.
+
+use lazy_static::lazy_static;
+use prometheus::{IntCounterVec, register_int_counter_vec};
+
+const CACHE_TYPES: [&str; 7] = [
+ "otlp_trace_aux",
+ "otlp_metrics_legacy",
+ "pipeline_table",
+ "pipeline",
+ "pipeline_original",
+ "pipeline_failover",
+ "mysql_prepared_stmt",
+];
+
+lazy_static! {
+ static ref CACHE_COUNTERS: (IntCounterVec, IntCounterVec) = {
+ let hits = register_int_counter_vec!(
+ "greptime_frontend_cache_hit",
+ "frontend cache hit",
+ &["type"]
+ )
+ .unwrap();
+ let misses = register_int_counter_vec!(
+ "greptime_frontend_cache_miss",
+ "frontend cache miss",
+ &["type"]
+ )
+ .unwrap();
+ // Register both outcomes together so an all-hit or all-miss cache has both series.
+ for cache_type in CACHE_TYPES {
+ hits.with_label_values(&[cache_type]);
+ misses.with_label_values(&[cache_type]);
+ }
+ (hits, misses)
+ };
+
+ /// Frontend cache hits by cache type.
+ pub static ref CACHE_HIT: IntCounterVec = CACHE_COUNTERS.0.clone();
+ /// Frontend cache misses by cache type.
+ pub static ref CACHE_MISS: IntCounterVec = CACHE_COUNTERS.1.clone();
+}
+
+/// Records one completed cache lookup.
+pub fn record_cache_lookup(cache_type: &str, hit: bool) {
+ if hit {
+ CACHE_HIT.with_label_values(&[cache_type]).inc();
+ } else {
+ CACHE_MISS.with_label_values(&[cache_type]).inc();
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_cache_lookup_counters() {
+ record_cache_lookup("pipeline", false);
+ // Scrape before accessing CACHE_HIT, which would initialize it and hide a missing series.
+ let families = prometheus::gather();
+ for (name, expected) in [
+ ("greptime_frontend_cache_hit", 0.0),
+ ("greptime_frontend_cache_miss", 1.0),
+ ] {
+ let family = families
+ .iter()
+ .find(|family| family.name() == name)
+ .unwrap();
+ assert_eq!(family.get_metric().len(), CACHE_TYPES.len());
+ for cache_type in CACHE_TYPES {
+ let metric = family
+ .get_metric()
+ .iter()
+ .find(|metric| metric.get_label()[0].value() == cache_type)
+ .unwrap();
+ assert_eq!(metric.get_label()[0].name(), "type");
+ assert_eq!(
+ metric.get_counter().value(),
+ if cache_type == "pipeline" {
+ expected
+ } else {
+ 0.0
+ }
+ );
+ }
+ }
+ record_cache_lookup("pipeline", true);
+ record_cache_lookup("pipeline", false);
+ assert_eq!(CACHE_HIT.with_label_values(&["pipeline"]).get(), 1);
+ assert_eq!(CACHE_MISS.with_label_values(&["pipeline"]).get(), 2);
+ }
+}
diff --git a/src/common/query/src/lib.rs b/src/common/query/src/lib.rs
index 8ce028ef9fe..8151dfc5731 100644
--- a/src/common/query/src/lib.rs
+++ b/src/common/query/src/lib.rs
@@ -25,10 +25,13 @@ pub mod stream;
pub mod test_util;
use std::fmt::{Debug, Display, Formatter};
+use std::future::Future;
+use std::pin::Pin;
use std::sync::Arc;
use api::greptime_proto::v1::AddColumnLocation as Location;
use api::greptime_proto::v1::add_column_location::LocationType;
+use common_error::ext::BoxedError;
use common_recordbatch::{
RecordBatches, SendableRecordBatchMapper, SendableRecordBatchStream, map_dictionary_to_values,
map_dictionary_to_values_schema,
@@ -74,6 +77,30 @@ pub struct OutputMeta {
/// May exist for query output. One can retrieve execution metrics from this plan.
pub plan: Option>,
pub cost: OutputCost,
+ /// Process-local storage results attached by asynchronous ordinary-table batching.
+ /// Callers needing confirmed writes must await these. Dropping them does not cancel writes.
+ pub write_completions: Vec,
+}
+
+/// Actual storage completion of an already accepted asynchronous write.
+pub struct WriteCompletion(Pin> + Send>>);
+
+impl WriteCompletion {
+ /// Wraps the storage result without changing how the write is scheduled.
+ pub fn new(completion: impl Future