1mod batch;
16mod batch_convert;
17mod flow_notifier;
18mod pending_worker;
19mod region_write;
20mod tables;
21#[cfg(test)]
22mod test_util;
23
24use std::collections::{HashMap, HashSet};
25use std::future::{Future, ready};
26use std::num::NonZeroUsize;
27use std::sync::Arc;
28use std::time::Duration;
29
30use api::v1::RowInsertRequests;
31use catalog::CatalogManagerRef;
32use common_batcher::flush_limiter::FlushLimiter;
33use common_batcher::flush_policy::timing::TimingFlushPolicy;
34use common_batcher::request_limiter::RequestLimiter;
35use common_batcher::worker_registry::WorkerRegistry;
36use common_meta::cache::TableFlownodeSetCacheRef;
37use common_meta::node_manager::NodeManagerRef;
38use common_query::prelude::GREPTIME_PHYSICAL_TABLE;
39use common_time::timestamp::TimeUnit;
40use meter_core::data::MeterRecord;
41use meter_macros::write_meter;
42use operator::write_dispatcher::WriteDispatcherRef;
43use partition::manager::PartitionRuleManagerRef;
44use session::context::QueryContextRef;
45use snafu::ResultExt;
46use store_api::metric_engine_consts::{LOGICAL_TABLE_METADATA_KEY, METRIC_ENGINE_NAME};
47use tokio::sync::{Semaphore, broadcast, mpsc, oneshot};
48
49use crate::batcher::flow_notifier::{FlowNotifier, start_flow_notification_worker};
50pub use crate::batcher::logical_table::batch::flush_batch_physical;
51pub use crate::batcher::logical_table::batch_convert::{RecordBatchWithTsIdx, TableBatch};
52use crate::batcher::logical_table::pending_worker::{
53 PendingWorker, WorkerCommand, remove_worker_if_same_channel, start_worker,
54};
55pub use crate::batcher::logical_table::region_write::{
56 PhysicalFlushCatalogProvider, PhysicalFlushPartitionProvider, PhysicalFlushWriteDispatcher,
57 PhysicalTableMetadata,
58};
59pub use crate::batcher::logical_table::tables::{
60 PendingRowsSchemaAlterer, PendingRowsSchemaAltererRef,
61};
62use crate::batcher::pending_rows_batch_sync_enabled;
63use crate::error;
64use crate::error::{Error, Result};
65use crate::metrics::{
66 FLOW_NOTIFICATION_DROPPED, PENDING_ROWS_BATCH_INGEST_STAGE_ELAPSED, PENDING_WORKERS,
67};
68
69const PHYSICAL_TABLE_KEY: &str = "physical_table";
70
71const WORKER_IDLE_TIMEOUT_MULTIPLIER: u32 = 3;
72
73#[derive(Debug, Clone, Hash, Eq, PartialEq)]
74pub(crate) struct BatchKey {
75 pub(crate) catalog: String,
76 pub(crate) schema: String,
77 pub(crate) physical_table: String,
78 pub(crate) skip_wal: bool,
79}
80
81pub(crate) fn batch_key_from_ctx(ctx: &QueryContextRef) -> BatchKey {
83 let physical_table = ctx
84 .extension(PHYSICAL_TABLE_KEY)
85 .unwrap_or(GREPTIME_PHYSICAL_TABLE)
86 .to_string();
87 BatchKey {
88 catalog: ctx.current_catalog().to_string(),
89 schema: ctx.current_schema(),
90 physical_table,
91 skip_wal: ctx.skip_wal(),
92 }
93}
94
95pub struct LogicalTablePendingRowsBatcher {
97 workers: Arc<WorkerRegistry<BatchKey, WorkerCommand>>,
98 flush_interval: Duration,
99 flush_policy: TimingFlushPolicy,
100 partition_manager: PartitionRuleManagerRef,
101 write_dispatcher: WriteDispatcherRef,
102 catalog_manager: CatalogManagerRef,
103 flow_notification_tx: FlowNotifier,
104 flush_limiter: FlushLimiter,
105 request_limiter: RequestLimiter,
106 worker_channel_capacity: usize,
107 prom_store_with_metric_engine: bool,
108 schema_alterer: PendingRowsSchemaAltererRef,
109 pending_rows_batch_sync: bool,
110 shutdown: broadcast::Sender<()>,
111}
112
113impl LogicalTablePendingRowsBatcher {
114 #[allow(clippy::too_many_arguments)]
116 pub fn try_new(
117 partition_manager: PartitionRuleManagerRef,
118 node_manager: NodeManagerRef,
119 write_dispatcher: WriteDispatcherRef,
120 catalog_manager: CatalogManagerRef,
121 table_flownode_set_cache: TableFlownodeSetCacheRef,
122 prom_store_with_metric_engine: bool,
123 schema_alterer: PendingRowsSchemaAltererRef,
124 flush_interval: Duration,
125 max_batch_rows: usize,
126 max_concurrent_flushes: usize,
127 worker_channel_capacity: usize,
128 max_inflight_requests: usize,
129 flow_notification_queue_capacity: NonZeroUsize,
130 ) -> Option<Arc<Self>> {
131 if worker_channel_capacity == 0 || worker_channel_capacity > Semaphore::MAX_PERMITS {
132 return None;
133 }
134
135 let flush_policy = TimingFlushPolicy::try_new(flush_interval, max_batch_rows)?;
136 let flush_limiter = FlushLimiter::try_new(max_concurrent_flushes)?;
137
138 let request_limiter = RequestLimiter::try_new(max_inflight_requests)?;
139 let (flow_notification_tx, flow_notification_rx) = FlowNotifier::try_new(
140 flow_notification_queue_capacity.get(),
141 FLOW_NOTIFICATION_DROPPED.clone(),
142 )?;
143
144 let (shutdown, _) = broadcast::channel(1);
145 let pending_rows_batch_sync = pending_rows_batch_sync_enabled();
146 let workers = Arc::new(WorkerRegistry::new());
147 PENDING_WORKERS.set(0);
148 start_flow_notification_worker(
149 flow_notification_rx,
150 table_flownode_set_cache,
151 node_manager.clone(),
152 );
153
154 Some(Arc::new(Self {
155 workers,
156 flush_interval,
157 flush_policy,
158 partition_manager,
159 write_dispatcher,
160 catalog_manager,
161 flow_notification_tx,
162 prom_store_with_metric_engine,
163 schema_alterer,
164 flush_limiter,
165 request_limiter,
166 worker_channel_capacity,
167 pending_rows_batch_sync,
168 shutdown,
169 }))
170 }
171}
172
173impl LogicalTablePendingRowsBatcher {
174 pub async fn submit(&self, requests: RowInsertRequests, ctx: QueryContextRef) -> Result<u64> {
175 self.submit_with(requests, ctx, |_| ready(Ok(())))
176 .await
177 .map(|(rows, ())| rows)
178 }
179
180 async fn physical_time_index_unit_or_default(&self, ctx: &QueryContextRef) -> TimeUnit {
184 let key = batch_key_from_ctx(ctx);
185 let Ok(Some(table)) = self
186 .catalog_manager
187 .table(&key.catalog, &key.schema, &key.physical_table, None)
188 .await
189 else {
190 return TimeUnit::Millisecond;
191 };
192 table
193 .table_info()
194 .meta
195 .schema
196 .timestamp_column()
197 .and_then(|col| col.data_type.as_timestamp().map(|ts| ts.unit()))
198 .unwrap_or(TimeUnit::Millisecond)
199 }
200
201 pub(crate) async fn accepts_bulk_destinations(
212 &self,
213 batches: impl Iterator<Item = &(QueryContextRef, RowInsertRequests)>,
214 ) -> bool {
215 let mut checked = HashSet::new();
220 let mut missing_selections: HashMap<(String, String), String> = HashMap::new();
224 for (ctx, requests) in batches {
225 let physical_table = batch_key_from_ctx(ctx).physical_table;
226 let schema = ctx.current_schema();
227 for request in &requests.inserts {
228 if !checked.insert((
229 schema.clone(),
230 request.table_name.clone(),
231 physical_table.clone(),
232 )) {
233 continue;
234 }
235 let Ok(Some(table)) = self
236 .catalog_manager
237 .table(ctx.current_catalog(), &schema, &request.table_name, None)
238 .await
239 else {
240 if missing_selections
244 .insert(
245 (schema.clone(), request.table_name.clone()),
246 physical_table.clone(),
247 )
248 .is_some_and(|previous| previous != physical_table)
249 {
250 return false;
251 }
252 continue;
253 };
254 let info = table.table_info();
255 if info.meta.engine != METRIC_ENGINE_NAME
256 || info
257 .meta
258 .options
259 .extra_options
260 .get(LOGICAL_TABLE_METADATA_KEY)
261 .map(String::as_str)
262 != Some(physical_table.as_str())
263 {
264 return false;
265 }
266 }
267 }
268 true
269 }
270
271 pub async fn submit_with<T, F>(
274 &self,
275 requests: RowInsertRequests,
276 ctx: QueryContextRef,
277 after_prepare: impl FnOnce(RowInsertRequests) -> F,
278 ) -> Result<(u64, T)>
279 where
280 F: Future<Output = Result<T>> + Send,
281 {
282 let (table_batches, total_rows) = {
283 let _timer = PENDING_ROWS_BATCH_INGEST_STAGE_ELAPSED
284 .with_label_values(&["submit_build_and_align"])
285 .start_timer();
286 self.build_and_align_table_batches(&requests, &ctx).await?
287 };
288 let prepared = after_prepare(requests).await?;
289 if total_rows == 0 {
290 return Ok((0, prepared));
291 }
292
293 write_meter!(MeterRecord::new(
295 ctx.current_catalog().to_string(),
296 ctx.current_schema(),
297 0,
298 ctx.write_rows_to_admit(
299 ctx.current_catalog(),
300 &ctx.current_schema(),
301 total_rows as u64
302 ),
303 ctx.channel() as u8,
304 ))
305 .await
306 .context(error::WriteRejectedSnafu)?;
307
308 let permit = {
309 let _timer = PENDING_ROWS_BATCH_INGEST_STAGE_ELAPSED
310 .with_label_values(&["submit_acquire_inflight_permit"])
311 .start_timer();
312 self.request_limiter
313 .acquire()
314 .await
315 .map_err(|_| error::BatcherChannelClosedSnafu.build())?
316 };
317
318 let (response_tx, response_rx) = oneshot::channel();
319
320 let batch_key = batch_key_from_ctx(&ctx);
321 let mut cmd = Some(WorkerCommand::Submit {
322 table_batches,
323 total_rows,
324 ctx,
325 response_tx,
326 _permit: permit,
327 });
328
329 {
330 let _timer = PENDING_ROWS_BATCH_INGEST_STAGE_ELAPSED
331 .with_label_values(&["submit_send_to_worker"])
332 .start_timer();
333
334 for _ in 0..2 {
335 let worker = self.get_or_spawn_worker(batch_key.clone()).await;
336 let Some(worker_cmd) = cmd.take() else {
337 break;
338 };
339
340 match worker.tx.send(worker_cmd).await {
341 Ok(()) => break,
342 Err(err) => {
343 cmd = Some(err.0);
344 remove_worker_if_same_channel(
345 self.workers.as_ref(),
346 &batch_key,
347 &worker.tx,
348 )
349 .await;
350 }
351 }
352 }
353
354 if cmd.is_some() {
355 return Err(Error::BatcherChannelClosed);
356 }
357 }
358
359 if self.pending_rows_batch_sync {
360 let result = {
361 let _timer = PENDING_ROWS_BATCH_INGEST_STAGE_ELAPSED
362 .with_label_values(&["submit_wait_flush_result"])
363 .start_timer();
364 response_rx
365 .await
366 .map_err(|_| error::BatcherChannelClosedSnafu.build())?
367 };
368 result
369 .context(error::SubmitBatchSnafu)
370 .map(|()| (total_rows as u64, prepared))
371 } else {
372 Ok((total_rows as u64, prepared))
373 }
374 }
375}
376
377impl LogicalTablePendingRowsBatcher {
378 async fn get_or_spawn_worker(&self, key: BatchKey) -> PendingWorker {
379 let (tx, receiver) = self
380 .workers
381 .get_or_create(key.clone(), self.worker_channel_capacity)
382 .await;
383 if let Some(rx) = receiver {
384 self.spawn_worker(key, tx.clone(), rx);
385 PENDING_WORKERS.set(self.workers.len().await as i64);
386 }
387 PendingWorker { tx }
388 }
389}
390
391impl LogicalTablePendingRowsBatcher {
392 fn spawn_worker(
393 &self,
394 key: BatchKey,
395 tx: mpsc::Sender<WorkerCommand>,
396 rx: mpsc::Receiver<WorkerCommand>,
397 ) {
398 let worker_idle_timeout = self
399 .flush_interval
400 .checked_mul(WORKER_IDLE_TIMEOUT_MULTIPLIER)
401 .unwrap_or(self.flush_interval);
402
403 start_worker(
404 key,
405 tx,
406 self.workers.clone(),
407 rx,
408 self.shutdown.clone(),
409 self.partition_manager.clone(),
410 self.write_dispatcher.clone(),
411 self.catalog_manager.clone(),
412 self.flow_notification_tx.clone(),
413 worker_idle_timeout,
414 self.flush_policy,
415 self.flush_limiter.clone(),
416 );
417 }
418}
419
420impl Drop for LogicalTablePendingRowsBatcher {
421 fn drop(&mut self) {
422 let _ = self.shutdown.send(());
423 }
424}
425
426#[cfg(test)]
427mod tests {
428 use std::sync::Arc;
429
430 use arrow::array::{StringArray, TimestampMillisecondArray};
431 use arrow::datatypes::{DataType as ArrowDataType, Field, Schema as ArrowSchema};
432 use arrow::record_batch::RecordBatch;
433 use common_query::prelude::greptime_timestamp;
434
435 use crate::batcher::logical_table::batch_convert::{RecordBatchWithTsIdx, TableBatch};
436 use crate::batcher::logical_table::flow_notifier::extract_timestamps;
437 use crate::batcher::logical_table::test_util::{mock_aligned_tag_batch, mock_tag_batch};
438
439 #[test]
440 fn test_extract_timestamps_appends_non_null_batches_in_order() {
441 let table_batch = TableBatch {
442 table_name: "cpu".to_string(),
443 table_id: 42,
444 batches: vec![
445 mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0),
446 mock_aligned_tag_batch("tag1", "host-1", 2000, 2.0),
447 ],
448 row_count: 2,
449 };
450
451 assert_eq!(vec![1000, 2000], extract_timestamps(&table_batch));
452 }
453
454 #[test]
455 fn test_extract_timestamps_omits_nulls_and_retains_order() {
456 let table_batch = TableBatch {
457 table_name: "cpu".to_string(),
458 table_id: 42,
459 batches: vec![
460 mock_timestamp_batch(vec![Some(1000), None, Some(3000)]),
461 mock_timestamp_batch(vec![None, Some(5000)]),
462 ],
463 row_count: 5,
464 };
465
466 assert_eq!(vec![1000, 3000, 5000], extract_timestamps(&table_batch));
467 }
468
469 #[test]
470 fn test_record_batch_with_ts_idx_rejects_out_of_bounds_index() {
471 let batch = mock_tag_batch("tag1", "host-1", 1000, 1.0);
472
473 assert!(RecordBatchWithTsIdx::try_new(batch, 3).is_err());
474 }
475
476 #[test]
477 fn test_record_batch_with_ts_idx_rejects_non_timestamp_column() {
478 let batch = mock_tag_batch("tag1", "host-1", 1000, 1.0);
479
480 assert!(RecordBatchWithTsIdx::try_new(batch, 1).is_err());
481 }
482
483 #[test]
484 fn test_extract_timestamps_supports_per_batch_timestamp_indices() {
485 let timestamp_first = RecordBatch::try_new(
486 Arc::new(ArrowSchema::new(vec![
487 Field::new(
488 "ts",
489 ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
490 false,
491 ),
492 Field::new("host", ArrowDataType::Utf8, true),
493 ])),
494 vec![
495 Arc::new(TimestampMillisecondArray::from(vec![1000, 2000])),
496 Arc::new(StringArray::from(vec!["host-1", "host-2"])),
497 ],
498 )
499 .unwrap();
500 let timestamp_second = RecordBatch::try_new(
501 Arc::new(ArrowSchema::new(vec![
502 Field::new("host", ArrowDataType::Utf8, true),
503 Field::new(
504 "timestamp",
505 ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
506 false,
507 ),
508 ])),
509 vec![
510 Arc::new(StringArray::from(vec!["host-3", "host-4"])),
511 Arc::new(TimestampMillisecondArray::from(vec![3000, 4000])),
512 ],
513 )
514 .unwrap();
515 let table_batch = TableBatch {
516 table_name: "cpu".to_string(),
517 table_id: 42,
518 batches: vec![
519 RecordBatchWithTsIdx::try_new(timestamp_first, 0).unwrap(),
520 RecordBatchWithTsIdx::try_new(timestamp_second, 1).unwrap(),
521 ],
522 row_count: 4,
523 };
524
525 assert_eq!(
526 vec![1000, 2000, 3000, 4000],
527 extract_timestamps(&table_batch)
528 );
529 }
530
531 fn mock_timestamp_batch(timestamps: Vec<Option<i64>>) -> RecordBatchWithTsIdx {
532 let batch = RecordBatch::try_new(
533 Arc::new(ArrowSchema::new(vec![Field::new(
534 greptime_timestamp(),
535 ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
536 true,
537 )])),
538 vec![Arc::new(TimestampMillisecondArray::from(timestamps))],
539 )
540 .unwrap();
541 RecordBatchWithTsIdx::try_new(batch, 0).unwrap()
542 }
543 #[test]
544 fn test_batch_key_groups_by_skip_wal() {
545 use std::collections::HashMap;
546
547 use crate::batcher::logical_table::batch_key_from_ctx;
548
549 let wal_ctx = session::context::QueryContext::arc();
550 let skip_wal_ctx = session::context::QueryContext::arc();
551 skip_wal_ctx.set_skip_wal(true);
552 let another_skip_wal_ctx = session::context::QueryContext::arc();
553 another_skip_wal_ctx.set_skip_wal(true);
554
555 let mut batches = HashMap::new();
556 *batches.entry(batch_key_from_ctx(&wal_ctx)).or_insert(0) += 1;
557 *batches
558 .entry(batch_key_from_ctx(&skip_wal_ctx))
559 .or_insert(0) += 1;
560 *batches
561 .entry(batch_key_from_ctx(&another_skip_wal_ctx))
562 .or_insert(0) += 1;
563 *batches
564 .entry(batch_key_from_ctx(&session::context::QueryContext::arc()))
565 .or_insert(0) += 1;
566
567 assert_eq!(batches.len(), 2);
568 assert_eq!(batches[&batch_key_from_ctx(&wal_ctx)], 2);
569 assert_eq!(batches[&batch_key_from_ctx(&skip_wal_ctx)], 2);
570 }
571}