diff --git a/Cargo.lock b/Cargo.lock
index 5d7ac01047..17d5882e5e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -10785,6 +10785,7 @@ dependencies = [
"enum_dispatch",
"futures",
"greptime-proto",
+ "humantime-serde",
"itertools 0.14.0",
"jsonb",
"jsonpath-rust 0.7.5",
@@ -14301,6 +14302,7 @@ dependencies = [
"hostname 0.4.1",
"log-store",
"mito2",
+ "pipeline",
"query",
"serde",
"servers",
diff --git a/config/config.md b/config/config.md
index 11e846ac76..319110809d 100644
--- a/config/config.md
+++ b/config/config.md
@@ -244,6 +244,8 @@
| `slow_query.sample_ratio` | Float | Unset | The sampling ratio of slow query log. The value should be in the range of (0, 1]. |
| `tracing` | -- | -- | The tracing options. Only effect when compiled with `tokio-console` feature. |
| `tracing.tokio_console_addr` | String | Unset | The tokio console address. |
+| `pipeline` | -- | -- | The pipeline options. |
+| `pipeline.cache_ttl` | String | `10s` | Time to live of the local pipeline cache. Default is `10s`. |
| `event_recorder` | -- | -- | Configuration options for the event recorder. |
| `event_recorder.ttl` | String | `90d` | TTL for the events table that will be used to store the events. Default is `90d`. |
| `event_recorder.event_types` | Array | -- | Event types to record. Current available event types: `create_database`, `alter_database`, `drop_database`, `create_flow`, `drop_flow`, `create_table`, `create_logical_tables`, `alter_table`, `alter_logical_tables`, `drop_table`, `undrop_table`, `purge_dropped_table`, `truncate_table`, `create_view`, `drop_view`, `admin_function`, `reconcile_table`. When omitted, all current and future event types are recorded. Set to an empty array to disable event recording. |
@@ -388,6 +390,8 @@
| `tracing.tokio_console_addr` | String | Unset | The tokio console address. |
| `memory` | -- | -- | The memory options. |
| `memory.enable_heap_profiling` | Bool | `true` | Whether to enable heap profiling activation during startup. When enabled, heap profiling will be activated if the `MALLOC_CONF` environment variable is set to "prof:true,prof_active:false". The official image adds this env variable. Default is true. |
+| `pipeline` | -- | -- | The pipeline options. |
+| `pipeline.cache_ttl` | String | `10s` | Time to live of the frontend-local pipeline cache. A pipeline created or deleted on another frontend takes effect on this one after at most this duration. |
| `event_recorder` | -- | -- | Configuration options for the event recorder. |
| `event_recorder.ttl` | String | `90d` | TTL for the events table that will be used to store the events. Default is `90d`. |
| `event_recorder.event_types` | Array | -- | Event types to record. Current available event type: `admin_function`. When omitted, all current and future event types are recorded. Set to an empty array to disable event recording. |
diff --git a/config/frontend.example.toml b/config/frontend.example.toml
index 9175fa598c..e85773c52e 100644
--- a/config/frontend.example.toml
+++ b/config/frontend.example.toml
@@ -428,6 +428,12 @@ ttl = "90d"
## Default is true.
enable_heap_profiling = true
+## The pipeline options.
+[pipeline]
+## Time to live of the frontend-local pipeline cache. A pipeline created or deleted on
+## another frontend takes effect on this one after at most this duration.
+cache_ttl = "10s"
+
## Configuration options for the event recorder.
[event_recorder]
## TTL for the events table that will be used to store the events. Default is `90d`.
diff --git a/config/standalone.example.toml b/config/standalone.example.toml
index 5458e0ed83..0444863727 100644
--- a/config/standalone.example.toml
+++ b/config/standalone.example.toml
@@ -956,6 +956,11 @@ default_ratio = 1.0
## @toml2docs:none-default
#+ tokio_console_addr = "127.0.0.1"
+## The pipeline options.
+[pipeline]
+## Time to live of the local pipeline cache. Default is `10s`.
+cache_ttl = "10s"
+
## Configuration options for the event recorder.
[event_recorder]
## TTL for the events table that will be used to store the events. Default is `90d`.
diff --git a/src/frontend/src/frontend.rs b/src/frontend/src/frontend.rs
index 18cedc0545..bb15abd7bb 100644
--- a/src/frontend/src/frontend.rs
+++ b/src/frontend/src/frontend.rs
@@ -22,6 +22,7 @@ use common_options::datanode::DatanodeClientOptions;
use common_options::memory::MemoryOptions;
use common_telemetry::logging::{LoggingOptions, SlowQueryOptions, TracingOptions};
use meta_client::MetaClientOptions;
+use pipeline::PipelineOptions;
use query::options::QueryOptions;
use serde::{Deserialize, Serialize};
use servers::grpc::GrpcOptions;
@@ -75,6 +76,8 @@ pub struct FrontendOptions {
pub query: QueryOptions,
pub slow_query: SlowQueryOptions,
pub memory: MemoryOptions,
+ /// The pipeline options.
+ pub pipeline: PipelineOptions,
/// The event recorder options.
pub event_recorder: EventRecorderOptions,
/// Environment variable keys to read and report in heartbeat messages.
@@ -108,6 +111,7 @@ impl Default for FrontendOptions {
query: QueryOptions::default(),
slow_query: SlowQueryOptions::default(),
memory: MemoryOptions::default(),
+ pipeline: PipelineOptions::default(),
event_recorder: EventRecorderOptions::default(),
heartbeat_env_vars: vec![],
}
diff --git a/src/frontend/src/instance/builder.rs b/src/frontend/src/instance/builder.rs
index 0f87dc424f..e49b75bbf8 100644
--- a/src/frontend/src/instance/builder.rs
+++ b/src/frontend/src/instance/builder.rs
@@ -334,6 +334,7 @@ impl FrontendBuilder {
statement_executor.clone(),
self.catalog_manager.clone(),
query_engine.clone(),
+ &self.options.pipeline,
));
plugins.insert::(statement_executor.clone());
diff --git a/src/pipeline/Cargo.toml b/src/pipeline/Cargo.toml
index d1a1f88ca8..620ac36ac7 100644
--- a/src/pipeline/Cargo.toml
+++ b/src/pipeline/Cargo.toml
@@ -41,11 +41,12 @@ dyn-fmt = "0.4"
enum_dispatch = "0.3"
futures.workspace = true
greptime-proto.workspace = true
+humantime-serde.workspace = true
itertools.workspace = true
jsonb.workspace = true
jsonpath-rust = "0.7.5"
lazy_static.workspace = true
-moka = { workspace = true, features = ["sync"] }
+moka = { workspace = true, features = ["future"] }
once_cell.workspace = true
operator.workspace = true
ordered-float.workspace = true
@@ -53,6 +54,7 @@ paste.workspace = true
prometheus.workspace = true
query.workspace = true
regex.workspace = true
+serde = { version = "1.0", features = ["derive"] }
serde_json.workspace = true
session.workspace = true
snafu.workspace = true
@@ -67,7 +69,6 @@ yaml-rust = "0.4"
catalog = { workspace = true, features = ["testing"] }
criterion = { workspace = true, features = ["html_reports"] }
rayon = "1.0"
-serde = { version = "1.0", features = ["derive"] }
session = { workspace = true, features = ["testing"] }
[[bench]]
diff --git a/src/pipeline/src/error.rs b/src/pipeline/src/error.rs
index 36abcaae9e..0da65d2543 100644
--- a/src/pipeline/src/error.rs
+++ b/src/pipeline/src/error.rs
@@ -699,6 +699,15 @@ pub enum Error {
location: Location,
},
+ /// `try_get_with` shares one loader across concurrent misses, so its error
+ /// arrives behind an `Arc`.
+ #[snafu(display("Failed to load pipeline into cache: {}", error))]
+ CacheLoad {
+ error: std::sync::Arc,
+ #[snafu(implicit)]
+ location: Location,
+ },
+
#[snafu(display("Failed to collect record batch"))]
CollectRecords {
#[snafu(implicit)]
@@ -895,6 +904,7 @@ impl ErrorExt for Error {
fn status_code(&self) -> StatusCode {
use Error::*;
match self {
+ CacheLoad { error, .. } => error.status_code(),
CastType { .. } => StatusCode::Unexpected,
PipelineTableNotFound { .. } => StatusCode::TableNotFound,
InsertPipeline { source, .. } => source.status_code(),
diff --git a/src/pipeline/src/lib.rs b/src/pipeline/src/lib.rs
index c3a45d4fe4..e323859d02 100644
--- a/src/pipeline/src/lib.rs
+++ b/src/pipeline/src/lib.rs
@@ -19,6 +19,7 @@ pub mod error;
mod etl;
mod manager;
mod metrics;
+pub mod options;
mod tablesuffix;
pub use etl::ctx_req::{ContextOpt, ContextReq};
@@ -35,6 +36,7 @@ pub use manager::{
IdentityTimeIndex, PipelineContext, PipelineDefinition, PipelineInfo, PipelineRef,
PipelineTableRef, PipelineVersion, PipelineWay, SelectInfo, pipeline_operator, table, util,
};
+pub use options::PipelineOptions;
#[macro_export]
macro_rules! unwrap_or_continue_if_err {
diff --git a/src/pipeline/src/manager/pipeline_cache.rs b/src/pipeline/src/manager/pipeline_cache.rs
index 2abe24e94b..e60963e7f8 100644
--- a/src/pipeline/src/manager/pipeline_cache.rs
+++ b/src/pipeline/src/manager/pipeline_cache.rs
@@ -12,14 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+use std::future::Future;
use std::sync::Arc;
use std::time::Duration;
-use common_telemetry::debug;
use datatypes::timestamp::TimestampNanosecond;
-use moka::sync::Cache;
+use moka::future::Cache;
-use crate::error::{MultiPipelineWithDiffSchemaSnafu, Result};
+use crate::error::{CacheLoadSnafu, MultiPipelineWithDiffSchemaSnafu, Result};
use crate::etl::Pipeline;
use crate::manager::PipelineVersion;
use crate::table::EMPTY_SCHEMA_NAME;
@@ -27,11 +27,14 @@ use crate::util::{generate_pipeline_cache_key, generate_pipeline_cache_key_suffi
/// Pipeline table cache size.
const PIPELINES_CACHE_SIZE: u64 = 10000;
-/// Pipeline table cache time to live.
-const PIPELINES_CACHE_TTL: Duration = Duration::from_secs(10);
/// Pipeline cache is located on a separate file on purpose,
/// to encapsulate inner cache. Only public methods are exposed.
+///
+/// `pipelines` and `original_pipelines` are keyed by the *requested* schema so
+/// a lookup is a single key probe, as [`Cache::try_get_with`] requires;
+/// resolving it to a stored schema is the loader's job. `failover_cache` has no
+/// loader and keeps the stored-schema key.
pub(crate) struct PipelineCache {
pipelines: Cache>,
original_pipelines: Cache,
@@ -49,16 +52,16 @@ pub struct PipelineContent {
}
impl PipelineCache {
- pub(crate) fn new() -> Self {
+ pub(crate) fn new(ttl: Duration) -> Self {
Self {
pipelines: Cache::builder()
.max_capacity(PIPELINES_CACHE_SIZE)
- .time_to_live(PIPELINES_CACHE_TTL)
+ .time_to_live(ttl)
.name("pipelines")
.build(),
original_pipelines: Cache::builder()
.max_capacity(PIPELINES_CACHE_SIZE)
- .time_to_live(PIPELINES_CACHE_TTL)
+ .time_to_live(ttl)
.name("original_pipelines")
.build(),
failover_cache: Cache::builder()
@@ -68,163 +71,242 @@ impl PipelineCache {
}
}
- pub(crate) fn insert_pipeline_cache(
+ /// Concurrent misses on the same key share one `init` call.
+ pub(crate) async fn get_pipeline_with(
&self,
schema: &str,
name: &str,
version: PipelineVersion,
- pipeline: Arc,
- with_latest: bool,
- ) {
- insert_cache_generic(
- &self.pipelines,
- schema,
- name,
- version,
- pipeline.clone(),
- with_latest,
- );
+ init: impl Future