Skip to main content

servers/batcher/
logical_table.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15mod 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
81// Requests can share a batch only when their write target and WAL policy match.
82pub(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
95/// Prometheus remote write pending rows batcher.
96pub 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    /// Creates the batcher when metric-engine batching is enabled.
115    #[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    /// Returns the physical metric table's time index unit resolved from
181    /// `ctx`, defaulting to millisecond when the table does not exist yet
182    /// (the schema alterer auto-creates it as millisecond).
183    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    /// Returns whether the bulk path can accept `batches`: every existing
202    /// destination table must be a metric logical table bound to the
203    /// physical table selected by its context. A destination bound to
204    /// another physical table would be flushed through the selected
205    /// physical's regions, silently misplacing its rows, so such requests
206    /// must stay on the ordinary insert path (which routes per destination).
207    /// New tables are always fine: they are created on the selected physical
208    /// table. Time index units need no check here — the bulk encode converts
209    /// each request to its destination's unit. Destinations are resolved
210    /// once per distinct (schema, table).
211    pub(crate) async fn accepts_bulk_destinations(
212        &self,
213        batches: impl Iterator<Item = &(QueryContextRef, RowInsertRequests)>,
214    ) -> bool {
215        // One request can select different physical tables per batch (e.g.
216        // per-series physical-table labels), so the dedupe key includes the
217        // selected physical table: every distinct (schema, table, physical)
218        // triple is validated against the table's actual binding.
219        let mut checked = HashSet::new();
220        // For missing tables, one request must not select two different
221        // physical tables: the batcher would create the table through one
222        // selection and flush its rows through the other's regions.
223        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                    // New table: created on the selected physical table, but
241                    // a conflicting selection within the same request cannot
242                    // be batched.
243                    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    /// Submits with request-level accounting after schema preparation and before
272    /// queue admission. Acknowledgement follows the global batching policy.
273    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        // Flushes dispatch directly to datanodes, so admit once before enqueueing.
294        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}