diff --git a/Cargo.lock b/Cargo.lock
index dba349c74e5..2badf7c6605 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5684,6 +5684,7 @@ dependencies = [
"store-api",
"strfmt",
"table",
+ "temp-env",
"tokio",
"tokio-util",
"toml 0.8.23",
@@ -14361,6 +14362,7 @@ dependencies = [
"snafu 0.8.6",
"store-api",
"table",
+ "temp-env",
"tokio",
"toml 0.8.23",
"url",
diff --git a/Cargo.toml b/Cargo.toml
index 886c1bb9104..96ce1229621 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -253,6 +253,7 @@ sqlx = { version = "0.8", default-features = false, features = [
stringprep = "0.1"
strum = { version = "0.27", features = ["derive"] }
sysinfo = "0.33"
+temp-env = "0.3"
tempfile = "3"
tokio = { version = "1.47", features = ["full"] }
tokio-postgres = "0.7.18"
diff --git a/config/config.md b/config/config.md
index dd5b980f1ba..4dcffe40654 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 Prometheus or shared table batching is enabled, a nonzero timeout is
raised to at least the largest active flush interval plus 1 second. The intervals come from
`prom_store.pending_rows_flush_interval` and `pending_rows_batcher.pending_rows_flush_interval`. |
| `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. |
@@ -74,6 +74,13 @@
| `influxdb` | -- | -- | InfluxDB protocol options. |
| `influxdb.enable` | Bool | `true` | Whether to enable InfluxDB protocol in HTTP API. |
| `influxdb.default_merge_mode` | String | `last_non_null` | Default merge mode for tables automatically created by InfluxDB protocol.
Available values: "last_non_null", "last_row". |
+| `pending_rows_batcher` | -- | -- | Shared experimental ordinary-table batching for opted-in ingestion protocols.
Legacy Prometheus batching settings under prom_store remain supported.
HTTP write protocols sharing this batcher. Omitted or empty disables all entrances.
Supported: influxdb, opentsdb, otlp, logs, loki, splunk, elasticsearch, http_sql, prom.
Prom uses ordinary-table batching without metric engine, otherwise its dedicated batcher.
Effective shared Prom settings take precedence; existing prom_store settings remain compatible. |
+| `pending_rows_batcher.pending_rows_flush_interval` | String | `0s` | Flush interval measured from the first pending submission. Zero disables batching. |
+| `pending_rows_batcher.max_batch_rows` | Integer | `100000` | Flush after a complete submission reaches this row threshold. |
+| `pending_rows_batcher.max_concurrent_flushes` | Integer | `256` | Maximum concurrent flushes shared by the frontend batcher. |
+| `pending_rows_batcher.worker_channel_capacity` | Integer | `65526` | Maximum queued submissions per table worker. |
+| `pending_rows_batcher.max_inflight_requests` | Integer | `3000` | Maximum admitted original requests awaiting completion. |
+| `pending_rows_batcher.flow_notification_queue_capacity` | Integer | `1024` | Maximum number of queued table Flow notifications. |
| `jaeger` | -- | -- | Jaeger protocol options. |
| `jaeger.enable` | Bool | `true` | Whether to enable Jaeger protocol in HTTP API. |
| `otlp` | -- | -- | OpenTelemetry protocol options. |
@@ -279,7 +286,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 Prometheus or shared table batching is enabled, a nonzero timeout is
raised to at least the largest active flush interval plus 1 second. The intervals come from
`prom_store.pending_rows_flush_interval` and `pending_rows_batcher.pending_rows_flush_interval`. |
| `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. |
@@ -332,6 +339,13 @@
| `influxdb` | -- | -- | InfluxDB protocol options. |
| `influxdb.enable` | Bool | `true` | Whether to enable InfluxDB protocol in HTTP API. |
| `influxdb.default_merge_mode` | String | `last_non_null` | Default merge mode for tables automatically created by InfluxDB protocol.
Available values: "last_non_null", "last_row". |
+| `pending_rows_batcher` | -- | -- | Shared experimental ordinary-table batching for opted-in ingestion protocols.
Legacy Prometheus batching settings under prom_store remain supported.
HTTP write protocols sharing this batcher. Omitted or empty disables all entrances.
Supported: influxdb, opentsdb, otlp, logs, loki, splunk, elasticsearch, http_sql, prom.
Prom uses ordinary-table batching without metric engine, otherwise its dedicated batcher.
Effective shared Prom settings take precedence; existing prom_store settings remain compatible. |
+| `pending_rows_batcher.pending_rows_flush_interval` | String | `0s` | Flush interval measured from the first pending submission. Zero disables batching. |
+| `pending_rows_batcher.max_batch_rows` | Integer | `100000` | Flush after a complete submission reaches this row threshold. |
+| `pending_rows_batcher.max_concurrent_flushes` | Integer | `256` | Maximum concurrent flushes shared by the frontend batcher. |
+| `pending_rows_batcher.worker_channel_capacity` | Integer | `65526` | Maximum queued submissions per table worker. |
+| `pending_rows_batcher.max_inflight_requests` | Integer | `3000` | Maximum admitted original requests awaiting completion. |
+| `pending_rows_batcher.flow_notification_queue_capacity` | Integer | `1024` | Maximum number of queued table Flow notifications. |
| `jaeger` | -- | -- | Jaeger protocol options. |
| `jaeger.enable` | Bool | `true` | Whether to enable Jaeger protocol in HTTP API. |
| `otlp` | -- | -- | OpenTelemetry protocol options. |
diff --git a/config/frontend.example.toml b/config/frontend.example.toml
index 7c6862916e4..95550931d46 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 Prometheus or shared table batching is enabled, a nonzero timeout is
+## raised to at least the largest active flush interval plus 1 second. The intervals come from
+## `prom_store.pending_rows_flush_interval` and `pending_rows_batcher.pending_rows_flush_interval`.
timeout = "0s"
## HTTP request body limit.
## The following units are supported: `B`, `KB`, `KiB`, `MB`, `MiB`, `GB`, `GiB`, `TB`, `TiB`, `PB`, `PiB`.
@@ -226,6 +227,37 @@ enable = true
## Available values: "last_non_null", "last_row".
default_merge_mode = "last_non_null"
+## Shared experimental ordinary-table batching for opted-in ingestion protocols.
+## Legacy Prometheus batching settings under prom_store remain supported.
+## HTTP write protocols sharing this batcher. Omitted or empty disables all entrances.
+## Supported: influxdb, opentsdb, otlp, logs, loki, splunk, elasticsearch, http_sql, prom.
+## Prom uses ordinary-table batching without metric engine, otherwise its dedicated batcher.
+## Effective shared Prom settings take precedence; existing prom_store settings remain compatible.
+[pending_rows_batcher]
+# protocols = [
+# "influxdb",
+# "opentsdb",
+# "otlp",
+# "logs",
+# "loki",
+# "splunk",
+# "elasticsearch",
+# "http_sql",
+# "prom",
+# ]
+## Flush interval measured from the first pending submission. Zero disables batching.
+pending_rows_flush_interval = "0s"
+## Flush after a complete submission reaches this row threshold.
+max_batch_rows = 100000
+## Maximum concurrent flushes shared by the frontend batcher.
+max_concurrent_flushes = 256
+## Maximum queued submissions per table worker.
+worker_channel_capacity = 65526
+## Maximum admitted original requests awaiting completion.
+max_inflight_requests = 3000
+## Maximum number of queued table Flow notifications.
+flow_notification_queue_capacity = 1024
+
## Jaeger protocol options.
[jaeger]
## Whether to enable Jaeger protocol in HTTP API.
diff --git a/config/standalone.example.toml b/config/standalone.example.toml
index e0a5f4bcd11..54a1555232e 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 Prometheus or shared table batching is enabled, a nonzero timeout is
+## raised to at least the largest active flush interval plus 1 second. The intervals come from
+## `prom_store.pending_rows_flush_interval` and `pending_rows_batcher.pending_rows_flush_interval`.
timeout = "0s"
## HTTP request body limit.
## The following units are supported: `B`, `KB`, `KiB`, `MB`, `MiB`, `GB`, `GiB`, `TB`, `TiB`, `PB`, `PiB`.
@@ -205,6 +206,37 @@ enable = true
## Available values: "last_non_null", "last_row".
default_merge_mode = "last_non_null"
+## Shared experimental ordinary-table batching for opted-in ingestion protocols.
+## Legacy Prometheus batching settings under prom_store remain supported.
+## HTTP write protocols sharing this batcher. Omitted or empty disables all entrances.
+## Supported: influxdb, opentsdb, otlp, logs, loki, splunk, elasticsearch, http_sql, prom.
+## Prom uses ordinary-table batching without metric engine, otherwise its dedicated batcher.
+## Effective shared Prom settings take precedence; existing prom_store settings remain compatible.
+[pending_rows_batcher]
+# protocols = [
+# "influxdb",
+# "opentsdb",
+# "otlp",
+# "logs",
+# "loki",
+# "splunk",
+# "elasticsearch",
+# "http_sql",
+# "prom",
+# ]
+## Flush interval measured from the first pending submission. Zero disables batching.
+pending_rows_flush_interval = "0s"
+## Flush after a complete submission reaches this row threshold.
+max_batch_rows = 100000
+## Maximum concurrent flushes shared by the frontend batcher.
+max_concurrent_flushes = 256
+## Maximum queued submissions per table worker.
+worker_channel_capacity = 65526
+## Maximum admitted original requests awaiting completion.
+max_inflight_requests = 3000
+## Maximum number of queued table Flow notifications.
+flow_notification_queue_capacity = 1024
+
## Jaeger protocol options.
[jaeger]
## Whether to enable Jaeger protocol in HTTP API.
diff --git a/src/frontend/AGENTS.md b/src/frontend/AGENTS.md
index b5c0871fc11..9740d160164 100644
--- a/src/frontend/AGENTS.md
+++ b/src/frontend/AGENTS.md
@@ -44,6 +44,12 @@ remote datanodes via `operator`/`client`.
auto-create, partition routing) → local `RegionServer` (standalone) or RPC to
datanodes (distributed).
+- **Table batching** (`instance/builder.rs`): protocol entry points opt in through
+ `QueryContext`. The primary inserter prepares eligible ordinary-table writes
+ for `servers::batcher::table::TablePendingRowsBatcher`. A separate execution-only
+ inserter, with no batcher attached, sends the prepared bulk writes to datanodes,
+ avoiding recursive batching. The batcher handles successful-write Flow notifications.
+
## Public surface
- `Instance` (`instance.rs`) — the business-logic container.
diff --git a/src/frontend/Cargo.toml b/src/frontend/Cargo.toml
index 677dbbbe55f..b59e1e83a4a 100644
--- a/src/frontend/Cargo.toml
+++ b/src/frontend/Cargo.toml
@@ -98,5 +98,6 @@ hyper-util = { workspace = true, features = ["tokio"] }
reqwest.workspace = true
serde_json.workspace = true
strfmt = "0.2"
+temp-env.workspace = true
tower.workspace = true
uuid.workspace = true
diff --git a/src/frontend/src/frontend.rs b/src/frontend/src/frontend.rs
index bb15abd7bbf..793ab94205e 100644
--- a/src/frontend/src/frontend.rs
+++ b/src/frontend/src/frontend.rs
@@ -35,8 +35,8 @@ use crate::error::Result;
use crate::heartbeat::HeartbeatTask;
use crate::instance::Instance;
use crate::service_config::{
- InfluxdbOptions, JaegerOptions, MysqlOptions, OpentsdbOptions, OtlpOptions, PostgresOptions,
- PromStoreOptions,
+ InfluxdbOptions, JaegerOptions, MysqlOptions, OpentsdbOptions, OtlpOptions,
+ PendingRowsBatcherOptions, PostgresOptions, PromStoreOptions,
};
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
@@ -65,6 +65,8 @@ pub struct FrontendOptions {
pub postgres: PostgresOptions,
pub opentsdb: OpentsdbOptions,
pub influxdb: InfluxdbOptions,
+ /// Shared experimental ordinary-table batching; independent of Prom batching.
+ pub pending_rows_batcher: PendingRowsBatcherOptions,
pub prom_store: PromStoreOptions,
pub jaeger: JaegerOptions,
pub otlp: OtlpOptions,
@@ -100,6 +102,7 @@ impl Default for FrontendOptions {
postgres: PostgresOptions::default(),
opentsdb: OpentsdbOptions::default(),
influxdb: InfluxdbOptions::default(),
+ pending_rows_batcher: PendingRowsBatcherOptions::default(),
jaeger: JaegerOptions::default(),
prom_store: PromStoreOptions::default(),
otlp: OtlpOptions::default(),
@@ -124,6 +127,7 @@ impl Configurable for FrontendOptions {
"heartbeat_env_vars",
"meta_client.metasrv_addrs",
"event_recorder.event_types",
+ "pending_rows_batcher.protocols",
])
}
}
@@ -208,7 +212,7 @@ mod tests {
use tonic::codegen::tokio_stream::wrappers::ReceiverStream;
use tonic::{Request, Response, Status, Streaming};
- use super::*;
+ use crate::frontend::*;
use crate::heartbeat::{
FrontendHeartbeatExtension, FrontendHeartbeatExtensionResult, FrontendHeartbeatExtensions,
};
@@ -218,6 +222,54 @@ mod tests {
type GrpcStream =
Pin> + Send + Sync + 'static>>;
+ #[test]
+ fn test_batcher_protocols_from_env() {
+ temp_env::with_vars(
+ [(
+ "FRONTEND_BATCHER_TEST__PENDING_ROWS_BATCHER__PROTOCOLS",
+ Some("influxdb,http_sql"),
+ )],
+ || {
+ let options =
+ FrontendOptions::load_layered_options(None, "FRONTEND_BATCHER_TEST").unwrap();
+ assert_eq!(
+ options.pending_rows_batcher.protocols,
+ vec![
+ servers::http::BatchingProtocol::Influxdb,
+ servers::http::BatchingProtocol::HttpSql
+ ]
+ );
+ },
+ );
+ }
+
+ #[test]
+ fn test_protocol_pending_rows_batcher_config() {
+ let defaults: FrontendOptions = toml::from_str("").unwrap();
+ assert!(
+ !defaults
+ .pending_rows_batcher
+ .pending_rows_batching_enabled()
+ );
+ let options: FrontendOptions = toml::from_str(
+ r#"
+[pending_rows_batcher]
+protocols = ["influxdb", "http_sql"]
+pending_rows_flush_interval = "5ms"
+max_batch_rows = 25
+"#,
+ )
+ .unwrap();
+ assert_eq!(options.pending_rows_batcher.max_batch_rows, 25);
+ assert_eq!(options.pending_rows_batcher.protocols.len(), 2);
+ assert!(options.pending_rows_batcher.pending_rows_batching_enabled());
+ let serialized = toml::to_string(&options).unwrap();
+ let parsed: FrontendOptions = toml::from_str(&serialized).unwrap();
+ assert_eq!(options.influxdb, parsed.influxdb);
+ assert_eq!(options.opentsdb, parsed.opentsdb);
+ assert_eq!(options.pending_rows_batcher, parsed.pending_rows_batcher);
+ }
+
#[test]
fn test_toml() {
let opts = FrontendOptions::default();
@@ -225,6 +277,8 @@ mod tests {
assert!(toml_string.contains("experimental_enable_exponential_histogram = false"));
let parsed: FrontendOptions = toml::from_str(&toml_string).unwrap();
assert_eq!(parsed.otlp, opts.otlp);
+ assert_eq!(parsed.influxdb, opts.influxdb);
+ assert_eq!(parsed.opentsdb, opts.opentsdb);
}
#[test]
diff --git a/src/frontend/src/instance.rs b/src/frontend/src/instance.rs
index f834722cca5..7225987d752 100644
--- a/src/frontend/src/instance.rs
+++ b/src/frontend/src/instance.rs
@@ -1854,9 +1854,9 @@ mod tests {
use tokio::sync::{mpsc, oneshot};
use tower::ServiceExt;
- use super::*;
use crate::frontend::FrontendOptions;
use crate::instance::builder::FrontendBuilder;
+ use crate::instance::*;
fn parse_test_sql(sql: &str) -> Vec {
parse_stmt(sql, &GreptimeDbDialect {}).unwrap()
diff --git a/src/frontend/src/instance/builder.rs b/src/frontend/src/instance/builder.rs
index e49b75bbf89..7a6ba474168 100644
--- a/src/frontend/src/instance/builder.rs
+++ b/src/frontend/src/instance/builder.rs
@@ -24,7 +24,7 @@ use catalog::system_schema::semantic_graph::EntityGraphProviderRef;
use common_base::Plugins;
use common_datasource::object_store::LocalFileAccess;
use common_event_recorder::{EventRecorderImpl, EventRecorderRef};
-use common_meta::cache::{LayeredCacheRegistryRef, TableRouteCacheRef};
+use common_meta::cache::{LayeredCacheRegistryRef, TableFlownodeSetCacheRef, TableRouteCacheRef};
use common_meta::cache_invalidator::{CacheInvalidatorRef, DummyCacheInvalidator};
use common_meta::key::TableMetadataManager;
use common_meta::key::flow::FlowMetadataManager;
@@ -32,6 +32,7 @@ use common_meta::kv_backend::KvBackendRef;
use common_meta::node_manager::NodeManagerRef;
use common_meta::procedure_executor::ProcedureExecutorRef;
use dashmap::DashMap;
+use operator::batcher::PendingRowsBatcher;
use operator::delete::Deleter;
use operator::flow::FlowServiceOperator;
use operator::insert::Inserter;
@@ -49,6 +50,8 @@ use partition::manager::PartitionRuleManager;
use pipeline::pipeline_operator::PipelineOperator;
use query::QueryEngineFactory;
use query::region_query::RegionQueryHandlerFactoryRef;
+use servers::batcher::table::TablePendingRowsBatcher;
+use servers::http::BatchingProtocol;
use snafu::{OptionExt, ResultExt};
use crate::error::{self, DataFusionSnafu, ExternalSnafu, Result};
@@ -58,6 +61,7 @@ use crate::heartbeat::frontend_peer_addr;
use crate::instance::Instance;
use crate::instance::entity_graph::EntityGraphProviderImpl;
use crate::instance::region_query::FrontendRegionQueryHandler;
+use crate::service_config::PendingRowsBatcherOptions;
/// The frontend [`Instance`] builder.
pub struct FrontendBuilder {
@@ -215,20 +219,50 @@ impl FrontendBuilder {
FrontendRegionQueryHandler::arc(partition_manager.clone(), node_manager.clone())
};
- let table_flownode_cache =
- self.layered_cache_registry
- .get()
- .context(error::CacheRequiredSnafu {
- name: TABLE_FLOWNODE_SET_CACHE_NAME,
- })?;
+ let table_flownode_cache: TableFlownodeSetCacheRef = self
+ .layered_cache_registry
+ .get()
+ .context(error::CacheRequiredSnafu {
+ name: TABLE_FLOWNODE_SET_CACHE_NAME,
+ })?;
- let inserter = Arc::new(Inserter::new(
- self.catalog_manager.clone(),
- partition_manager.clone(),
- node_manager.clone(),
- table_flownode_cache,
- self.options.auto_create_table,
- ));
+ let create_inserter = || {
+ Inserter::new(
+ self.catalog_manager.clone(),
+ partition_manager.clone(),
+ node_manager.clone(),
+ table_flownode_cache.clone(),
+ self.options.auto_create_table,
+ )
+ };
+ // The execution-only inserter owns no batchers, avoiding an Arc cycle.
+ let bulk_inserter = Arc::new(create_inserter());
+ let build_batcher =
+ |options: &PendingRowsBatcherOptions| -> Option> {
+ if !options.pending_rows_batching_enabled()
+ || (self.options.prom_store.with_metric_engine
+ && options
+ .protocols
+ .iter()
+ .all(|protocol| *protocol == BatchingProtocol::Prom))
+ {
+ return None;
+ }
+ TablePendingRowsBatcher::try_new(
+ options.pending_rows_flush_interval,
+ options.max_batch_rows,
+ options.max_concurrent_flushes,
+ options.worker_channel_capacity,
+ options.max_inflight_requests,
+ options.flow_notification_queue_capacity,
+ bulk_inserter.clone(),
+ )
+ .map(|batcher| batcher as Arc)
+ };
+ let inserter = Arc::new(
+ create_inserter()
+ .with_pending_rows_batcher(build_batcher(&self.options.pending_rows_batcher)),
+ );
let deleter = Arc::new(Deleter::new(
self.catalog_manager.clone(),
partition_manager.clone(),
diff --git a/src/frontend/src/instance/influxdb.rs b/src/frontend/src/instance/influxdb.rs
index c23065394f1..8e4d0af87d8 100644
--- a/src/frontend/src/instance/influxdb.rs
+++ b/src/frontend/src/instance/influxdb.rs
@@ -208,7 +208,7 @@ mod tests {
use session::context::QueryContext;
use store_api::mito_engine_options::MERGE_MODE_KEY;
- use super::*;
+ use crate::instance::influxdb::*;
use crate::service_config::influxdb::InfluxdbMergeMode;
#[test]
diff --git a/src/frontend/src/instance/opentsdb.rs b/src/frontend/src/instance/opentsdb.rs
index e5fda529594..17908519ad3 100644
--- a/src/frontend/src/instance/opentsdb.rs
+++ b/src/frontend/src/instance/opentsdb.rs
@@ -21,7 +21,7 @@ use auth::{
};
use common_error::ext::BoxedError;
use common_telemetry::tracing;
-use servers::error::{self as server_error, AuthSnafu, ExecuteGrpcQuerySnafu};
+use servers::error::{AuthSnafu, ExecuteGrpcQuerySnafu, Result as ServerResult};
use servers::opentsdb::codec::DataPoint;
use servers::opentsdb::data_point_to_grpc_row_insert_requests;
use servers::query_handler::OpentsdbProtocolHandler;
@@ -44,11 +44,7 @@ fn permission_targets(data_points: &[DataPoint], ctx: &QueryContextRef) -> Permi
#[async_trait]
impl OpentsdbProtocolHandler for Instance {
- async fn preflight(
- &self,
- data_points: &[DataPoint],
- ctx: QueryContextRef,
- ) -> server_error::Result<()> {
+ async fn preflight(&self, data_points: &[DataPoint], ctx: QueryContextRef) -> ServerResult<()> {
self.check_table_permission(
&ctx,
PermissionReq::Action(OPENTSDB_WRITE),
@@ -59,11 +55,30 @@ impl OpentsdbProtocolHandler for Instance {
}
#[tracing::instrument(skip_all, fields(protocol = "opentsdb"))]
- async fn exec(
+ async fn exec(&self, data_points: Vec, ctx: QueryContextRef) -> ServerResult {
+ // Keep diagnostic per-point errors independent of other batched writes.
+ let mut ctx = ctx.fork();
+ ctx.set_batching_enabled(false);
+ self.execute_opentsdb_write(data_points, Arc::new(ctx))
+ .await
+ }
+
+ #[tracing::instrument(skip_all, fields(protocol = "opentsdb"))]
+ async fn exec_batch(
&self,
data_points: Vec,
ctx: QueryContextRef,
- ) -> server_error::Result {
+ ) -> ServerResult {
+ self.execute_opentsdb_write(data_points, ctx).await
+ }
+}
+
+impl Instance {
+ async fn execute_opentsdb_write(
+ &self,
+ data_points: Vec,
+ ctx: QueryContextRef,
+ ) -> ServerResult {
self.plugins
.get::()
.as_ref()
@@ -99,7 +114,7 @@ impl OpentsdbProtocolHandler for Instance {
mod tests {
use session::context::QueryContext;
- use super::*;
+ use crate::instance::opentsdb::*;
#[test]
fn test_permission_targets_do_not_require_row_conversion() {
diff --git a/src/frontend/src/server.rs b/src/frontend/src/server.rs
index 1812dc71a51..257e5243e89 100644
--- a/src/frontend/src/server.rs
+++ b/src/frontend/src/server.rs
@@ -33,7 +33,7 @@ use servers::grpc::{GrpcOptions, GrpcServer};
use servers::http::event::LogValidatorRef;
use servers::http::result::error_result::ErrorResponse;
use servers::http::utils::router::RouterConfigurator;
-use servers::http::{HttpOptions, HttpServer, HttpServerBuilder};
+use servers::http::{BatchingProtocol, HttpOptions, HttpServer, HttpServerBuilder};
use servers::interceptor::LogIngestInterceptorRef;
use servers::metrics_handler::MetricsHandler;
use servers::mysql::server::{MysqlServer, MysqlSpawnConfig, MysqlSpawnRef};
@@ -49,6 +49,7 @@ use tonic::Status;
use crate::error::{self, Result, StartServerSnafu, TomlFormatSnafu};
use crate::frontend::FrontendOptions;
use crate::instance::Instance;
+use crate::service_config::PromStoreOptions;
pub struct Services
where
@@ -106,6 +107,7 @@ where
request_memory_limiter: ServerMemoryLimiter,
) -> HttpServerBuilder {
let mut builder = HttpServerBuilder::new(effective_http_options(opts))
+ .with_batching_protocols(opts.pending_rows_batcher.protocols.clone())
.with_memory_limiter(request_memory_limiter)
.with_sql_handler(self.instance.clone());
@@ -127,21 +129,22 @@ where
builder = builder.with_influxdb_handler(self.instance.clone());
}
- if opts.prom_store.enable {
- let pending_rows_batcher = if opts.prom_store.with_metric_engine {
+ let prom_store = effective_prom_store_options(opts);
+ if prom_store.enable {
+ let pending_rows_batcher = if 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,
+ 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,
+ prom_store.pending_rows_flush_interval,
+ prom_store.max_batch_rows,
+ prom_store.max_concurrent_flushes,
+ prom_store.worker_channel_capacity,
+ prom_store.max_inflight_requests,
+ prom_store.flow_notification_queue_capacity,
)
} else {
None
@@ -426,22 +429,45 @@ where
}
}
+/// Selected shared controls override legacy Prom batching knobs, not protocol behavior.
+fn effective_prom_store_options(opts: &FrontendOptions) -> PromStoreOptions {
+ let mut prom_store = opts.prom_store.clone();
+ let shared = &opts.pending_rows_batcher;
+ if shared.protocols.contains(&BatchingProtocol::Prom) && shared.pending_rows_batching_enabled()
+ {
+ prom_store.pending_rows_flush_interval = shared.pending_rows_flush_interval;
+ prom_store.max_batch_rows = shared.max_batch_rows;
+ prom_store.max_concurrent_flushes = shared.max_concurrent_flushes;
+ prom_store.worker_channel_capacity = shared.worker_channel_capacity;
+ prom_store.max_inflight_requests = shared.max_inflight_requests;
+ prom_store.flow_notification_queue_capacity = shared.flow_notification_queue_capacity;
+ }
+ prom_store
+}
+
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;
+ let prom_store = effective_prom_store_options(opts);
+ let shared = &opts.pending_rows_batcher;
+ // Ordinary-table batching always waits for its flush, independently of the
+ // dedicated Prom batcher's asynchronous acknowledgement mode.
+ let common_enabled = shared.pending_rows_batching_enabled()
+ && shared.protocols.iter().any(|protocol| {
+ *protocol != BatchingProtocol::Prom
+ || (prom_store.enable && !prom_store.with_metric_engine)
+ });
+ let common_interval = common_enabled.then_some(shared.pending_rows_flush_interval);
+ let prom_interval = (prom_store.pending_rows_batching_enabled() && batch_sync)
+ .then_some(prom_store.pending_rows_flush_interval);
+ let Some(flush_interval) = common_interval.into_iter().chain(prom_interval).max() else {
+ return http;
+ };
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
- || http.timeout.is_zero()
- || http.timeout > fallback_timeout
- {
+ if http.timeout.is_zero() || http.timeout > fallback_timeout {
return http;
}
@@ -462,6 +488,7 @@ fn parse_addr(addr: &str) -> Result {
#[cfg(test)]
mod tests {
+ use std::num::NonZeroUsize;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
@@ -478,8 +505,98 @@ mod tests {
use servers::grpc::flight::{FlightCraft, FlightCraftRef, TonicStream};
use tonic::{Code, Request, Response, Status, Streaming};
- use super::*;
use crate::instance::builder::FrontendBuilder;
+ use crate::server::*;
+
+ #[test]
+ fn test_effective_prom_batching_controls() {
+ // Only an enabled shared Prom selection replaces the legacy controls.
+ for (selected, shared_enabled, metric_engine, prom_enabled) in [
+ (true, true, true, true),
+ (false, true, true, true),
+ (true, false, true, true),
+ (true, true, false, true),
+ (true, true, true, false),
+ ] {
+ let mut opts = FrontendOptions::default();
+ opts.http.timeout = Duration::from_millis(1);
+ opts.prom_store.pending_rows_flush_interval = Duration::from_secs(2);
+ opts.prom_store.with_metric_engine = metric_engine;
+ opts.prom_store.enable = prom_enabled;
+ opts.prom_store
+ .experimental_enable_prometheus_native_histogram = true;
+ let shared = &mut opts.pending_rows_batcher;
+ shared.protocols = vec![if selected {
+ BatchingProtocol::Prom
+ } else {
+ BatchingProtocol::Influxdb
+ }];
+ shared.pending_rows_flush_interval = if shared_enabled {
+ Duration::from_secs(5)
+ } else {
+ Duration::ZERO
+ };
+ shared.max_batch_rows = 7;
+ shared.max_concurrent_flushes = 3;
+ shared.worker_channel_capacity = 11;
+ shared.max_inflight_requests = 13;
+ shared.flow_notification_queue_capacity = NonZeroUsize::new(17).unwrap();
+
+ let mut expected = opts.prom_store.clone();
+ if selected && shared_enabled {
+ expected.pending_rows_flush_interval = shared.pending_rows_flush_interval;
+ expected.max_batch_rows = shared.max_batch_rows;
+ expected.max_concurrent_flushes = shared.max_concurrent_flushes;
+ expected.worker_channel_capacity = shared.worker_channel_capacity;
+ expected.max_inflight_requests = shared.max_inflight_requests;
+ expected.flow_notification_queue_capacity = shared.flow_notification_queue_capacity;
+ }
+ let actual = effective_prom_store_options(&opts);
+ assert_eq!(actual, expected);
+ assert_eq!(
+ actual.pending_rows_batching_enabled(),
+ metric_engine && prom_enabled
+ );
+ }
+ }
+
+ #[test]
+ fn test_http_timeout_covers_synchronous_batchers() {
+ // Shared ordinary writes remain synchronous even when Prom is asynchronous.
+ for (
+ protocols,
+ metric_engine,
+ batch_sync,
+ shared_secs,
+ legacy_secs,
+ timeout_secs,
+ expected_secs,
+ ) in [
+ (vec![BatchingProtocol::Prom], false, false, 5, 2, 1, 6),
+ (vec![BatchingProtocol::Prom], true, false, 5, 2, 1, 1),
+ (vec![BatchingProtocol::Prom], true, true, 5, 2, 1, 6),
+ (vec![BatchingProtocol::Influxdb], true, false, 5, 2, 1, 6),
+ (vec![BatchingProtocol::Influxdb], true, true, 5, 8, 1, 9),
+ (vec![BatchingProtocol::Influxdb], true, true, 8, 5, 1, 9),
+ (vec![BatchingProtocol::Influxdb], true, false, 5, 2, 0, 0),
+ (vec![BatchingProtocol::Influxdb], true, false, 5, 2, 10, 10),
+ (vec![BatchingProtocol::Prom], false, false, 0, 2, 1, 1),
+ (vec![], true, false, 5, 2, 1, 1),
+ (vec![], true, true, 5, 2, 1, 3),
+ ] {
+ let mut opts = FrontendOptions::default();
+ opts.http.timeout = Duration::from_secs(timeout_secs);
+ opts.prom_store.with_metric_engine = metric_engine;
+ opts.prom_store.pending_rows_flush_interval = Duration::from_secs(legacy_secs);
+ opts.pending_rows_batcher.protocols = protocols;
+ opts.pending_rows_batcher.pending_rows_flush_interval =
+ Duration::from_secs(shared_secs);
+ assert_eq!(
+ effective_http_options_with_sync(&opts, batch_sync).timeout,
+ Duration::from_secs(expected_secs)
+ );
+ }
+ }
struct CountingFlightCraft {
inner: FlightCraftRef,
@@ -561,6 +678,41 @@ mod tests {
);
}
+ #[test]
+ fn test_invalid_shared_batching_preserves_prom_store_options() {
+ type KnobMutator = fn(&mut FrontendOptions);
+ let cases: [KnobMutator; 5] = [
+ |opts| opts.pending_rows_batcher.max_concurrent_flushes = usize::MAX,
+ |opts| opts.pending_rows_batcher.worker_channel_capacity = usize::MAX,
+ |opts| opts.pending_rows_batcher.max_inflight_requests = usize::MAX,
+ |opts| {
+ opts.pending_rows_batcher.flow_notification_queue_capacity =
+ NonZeroUsize::new(usize::MAX).unwrap()
+ },
+ |opts| opts.pending_rows_batcher.pending_rows_flush_interval = Duration::MAX,
+ ];
+ for invalidate in cases {
+ let mut opts = FrontendOptions::default();
+ opts.http.timeout = Duration::from_secs(1);
+ opts.prom_store.pending_rows_flush_interval = Duration::from_secs(5);
+ opts.pending_rows_batcher.protocols =
+ vec![BatchingProtocol::Prom, BatchingProtocol::Influxdb];
+ opts.pending_rows_batcher.pending_rows_flush_interval = Duration::from_secs(10);
+ invalidate(&mut opts);
+ assert!(!opts.pending_rows_batcher.pending_rows_batching_enabled());
+ assert_eq!(opts.prom_store, effective_prom_store_options(&opts));
+ assert_eq!(
+ Duration::from_secs(6),
+ effective_http_options_with_sync(&opts, true).timeout
+ );
+ opts.prom_store.pending_rows_flush_interval = Duration::ZERO;
+ assert_eq!(
+ Duration::from_secs(1),
+ effective_http_options_with_sync(&opts, true).timeout
+ );
+ }
+ }
+
#[test]
fn test_effective_http_timeout_skips_fallback_when_batcher_disabled() {
// Mirrors the conditions under which `PendingRowsBatcher::try_new`
diff --git a/src/frontend/src/service_config.rs b/src/frontend/src/service_config.rs
index 03ec12e2c9f..bcd1097cfcf 100644
--- a/src/frontend/src/service_config.rs
+++ b/src/frontend/src/service_config.rs
@@ -17,6 +17,7 @@ pub mod jaeger;
pub mod mysql;
pub mod opentsdb;
pub mod otlp;
+pub mod pending_rows_batcher;
pub mod postgres;
pub mod prom_store;
@@ -25,5 +26,6 @@ pub use jaeger::JaegerOptions;
pub use mysql::MysqlOptions;
pub use opentsdb::OpentsdbOptions;
pub use otlp::OtlpOptions;
+pub use pending_rows_batcher::PendingRowsBatcherOptions;
pub use postgres::PostgresOptions;
pub use prom_store::PromStoreOptions;
diff --git a/src/frontend/src/service_config/influxdb.rs b/src/frontend/src/service_config/influxdb.rs
index 912e342471d..af28dee935f 100644
--- a/src/frontend/src/service_config/influxdb.rs
+++ b/src/frontend/src/service_config/influxdb.rs
@@ -49,7 +49,7 @@ impl Default for InfluxdbOptions {
#[cfg(test)]
mod tests {
- use super::InfluxdbOptions;
+ use crate::service_config::influxdb::InfluxdbOptions;
#[test]
fn test_influxdb_options() {
diff --git a/src/frontend/src/service_config/pending_rows_batcher.rs b/src/frontend/src/service_config/pending_rows_batcher.rs
new file mode 100644
index 00000000000..ef7ce14ef02
--- /dev/null
+++ b/src/frontend/src/service_config/pending_rows_batcher.rs
@@ -0,0 +1,150 @@
+// 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.
+
+use std::num::NonZeroUsize;
+use std::time::Duration;
+
+use common_batcher::flush_policy::timing::TimingFlushPolicy;
+use serde::{Deserialize, Serialize};
+use servers::http::BatchingProtocol;
+use tokio::sync::Semaphore;
+
+/// Experimental table write batching options shared by HTTP ingestion protocols.
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
+#[serde(default)]
+pub struct PendingRowsBatcherOptions {
+ /// HTTP write protocols sharing this batcher; empty disables all entrances.
+ pub protocols: Vec,
+ /// Time from the first pending submission to a timed flush. Zero disables batching.
+ #[serde(with = "humantime_serde")]
+ pub pending_rows_flush_interval: Duration,
+ /// Row threshold checked after appending a complete submission.
+ pub max_batch_rows: usize,
+ /// Maximum concurrent flushes shared by the frontend batcher.
+ pub max_concurrent_flushes: usize,
+ /// Maximum number of queued submissions per table worker.
+ pub worker_channel_capacity: usize,
+ /// Maximum number of original requests awaiting completion.
+ pub max_inflight_requests: usize,
+ /// Maximum number of table Flow notifications waiting in the shared queue.
+ pub flow_notification_queue_capacity: NonZeroUsize,
+}
+
+impl PendingRowsBatcherOptions {
+ /// Returns whether a protocol opts in and its controls pass construction validation.
+ pub fn pending_rows_batching_enabled(&self) -> bool {
+ !self.protocols.is_empty()
+ && TimingFlushPolicy::validate(self.pending_rows_flush_interval)
+ && self.max_batch_rows > 0
+ && [
+ self.max_concurrent_flushes,
+ self.worker_channel_capacity,
+ self.max_inflight_requests,
+ self.flow_notification_queue_capacity.get(),
+ ]
+ .into_iter()
+ .all(|capacity| (1..=Semaphore::MAX_PERMITS).contains(&capacity))
+ }
+}
+
+impl Default for PendingRowsBatcherOptions {
+ fn default() -> Self {
+ Self {
+ protocols: Vec::new(),
+ pending_rows_flush_interval: Duration::ZERO,
+ max_batch_rows: 100_000,
+ max_concurrent_flushes: 256,
+ worker_channel_capacity: 65526,
+ max_inflight_requests: 3000,
+ flow_notification_queue_capacity: NonZeroUsize::new(1024).unwrap_or(NonZeroUsize::MIN),
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::service_config::pending_rows_batcher::*;
+
+ #[test]
+ fn test_protocols() {
+ let options: PendingRowsBatcherOptions = toml::from_str(
+ "protocols = ['influxdb', 'opentsdb', 'otlp', 'logs', 'loki', 'splunk', 'elasticsearch', 'http_sql', 'prom']",
+ ).unwrap();
+ assert_eq!(options.protocols.len(), 9);
+ assert!(options.protocols.contains(&BatchingProtocol::HttpSql));
+ assert!(PendingRowsBatcherOptions::default().protocols.is_empty());
+ for invalid in ["sql", "jaeger", "unknown"] {
+ assert!(
+ toml::from_str::(&format!("protocols = ['{invalid}']"))
+ .is_err()
+ );
+ }
+ }
+
+ #[test]
+ fn test_notification_capacity() {
+ let default = PendingRowsBatcherOptions::default();
+ assert_eq!(default.flow_notification_queue_capacity.get(), 1024);
+ let configured: PendingRowsBatcherOptions =
+ toml::from_str("flow_notification_queue_capacity = 8").unwrap();
+ assert_eq!(configured.flow_notification_queue_capacity.get(), 8);
+ assert!(
+ toml::from_str::("flow_notification_queue_capacity = 0")
+ .is_err()
+ );
+ }
+
+ #[test]
+ fn test_defaults_and_roundtrip() {
+ let options: PendingRowsBatcherOptions = toml::from_str("").unwrap();
+ assert_eq!(options, PendingRowsBatcherOptions::default());
+ assert!(!options.pending_rows_batching_enabled());
+ assert_eq!(options.max_batch_rows, 100_000);
+ assert_eq!(options.max_concurrent_flushes, 256);
+ assert_eq!(options.worker_channel_capacity, 65526);
+ assert_eq!(options.max_inflight_requests, 3000);
+ let serialized = toml::to_string(&options).unwrap();
+ assert_eq!(
+ options,
+ toml::from_str::(&serialized).unwrap()
+ );
+ }
+
+ #[test]
+ fn test_partial_options_and_zero_controls() {
+ let options: PendingRowsBatcherOptions =
+ toml::from_str("pending_rows_flush_interval = '5ms'").unwrap();
+ assert_eq!(
+ options.pending_rows_flush_interval,
+ Duration::from_millis(5)
+ );
+ assert!(!options.pending_rows_batching_enabled());
+ let enabled: PendingRowsBatcherOptions =
+ toml::from_str("protocols = ['http_sql']\npending_rows_flush_interval = '5ms'")
+ .unwrap();
+ assert!(enabled.pending_rows_batching_enabled());
+ for field in [
+ "max_batch_rows",
+ "max_concurrent_flushes",
+ "worker_channel_capacity",
+ "max_inflight_requests",
+ ] {
+ let options: PendingRowsBatcherOptions = toml::from_str(&format!(
+ "protocols = ['influxdb']\npending_rows_flush_interval = '5ms'\n{field} = 0"
+ ))
+ .unwrap();
+ assert!(!options.pending_rows_batching_enabled(), "{field}");
+ }
+ }
+}
diff --git a/src/operator/src/insert.rs b/src/operator/src/insert.rs
index d88006b18ad..7fb538dc775 100644
--- a/src/operator/src/insert.rs
+++ b/src/operator/src/insert.rs
@@ -24,7 +24,7 @@ use api::v1::region::{
};
use api::v1::{
AlterTableExpr, ColumnDataType, ColumnSchema, CreateTableExpr, InsertRequests,
- RowInsertRequest, RowInsertRequests, SemanticType,
+ RowInsertRequest, RowInsertRequests, Rows, SemanticType,
};
use catalog::CatalogManagerRef;
use client::{OutputData, OutputMeta};
@@ -65,7 +65,7 @@ use store_api::mito_engine_options::{
};
use store_api::storage::{RegionId, TableId};
use table::TableRef;
-use table::metadata::TableInfo;
+use table::metadata::{TableInfo, TableInfoRef};
use table::requests::{
AUTO_CREATE_TABLE_KEY, InsertRequest as TableInsertRequest, SEMANTIC_PER_TABLE_INDEX_KEY,
TABLE_DATA_MODEL, TABLE_DATA_MODEL_TRACE_V1, TRACE_TABLE_PARTITIONS_HINT_KEY,
@@ -73,6 +73,7 @@ use table::requests::{
};
use table::table_reference::TableReference;
+use crate::batcher::PendingRowsBatcher;
use crate::error::{
CatalogSnafu, ColumnOptionsSnafu, CreatePartitionRulesSnafu, FindRegionLeaderSnafu,
InvalidInsertRequestSnafu, JoinTaskSnafu, RequestInsertsSnafu, Result, TableNotFoundSnafu,
@@ -81,7 +82,8 @@ use crate::expr_helper;
use crate::region_req_factory::RegionRequestFactory;
use crate::req_convert::common::preprocess_row_insert_requests;
use crate::req_convert::insert::{
- ColumnToRow, RowToRegion, StatementToRegion, TableToRegion, fill_reqs_with_impure_default,
+ ColumnToRow, ImpureDefaultFiller, RowToRegion, StatementToRegion, TableToRegion,
+ fill_reqs_with_impure_default, rows_to_record_batch,
};
use crate::statement::StatementExecutor;
@@ -94,6 +96,7 @@ pub struct Inserter {
/// When `false`, missing tables are never auto-created regardless of the
/// per-request `auto_create_table` hint. When `true`, the hint still applies.
auto_create_table: bool,
+ pending_rows_batcher: Option>,
}
pub type InserterRef = Arc;
@@ -163,9 +166,19 @@ impl Inserter {
node_manager,
table_flownode_set_cache,
auto_create_table,
+ pending_rows_batcher: None,
}
}
+ /// Installs the shared batcher; callers explicitly select its ingestion entry point.
+ pub fn with_pending_rows_batcher(
+ mut self,
+ batcher: Option>,
+ ) -> Self {
+ self.pending_rows_batcher = batcher;
+ self
+ }
+
pub async fn handle_column_inserts(
&self,
requests: InsertRequests,
@@ -267,6 +280,11 @@ impl Inserter {
) -> Result