Skip to main content

servers/batcher/logical_table/
batch.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
15use std::collections::HashSet;
16use std::time::Instant;
17
18use catalog::CatalogManagerRef;
19use common_batcher::flush_limiter::FlushLimiter;
20use common_error::ext::BoxedError;
21use common_query::{WriteProgress, prelude};
22use common_telemetry::{debug, warn};
23use operator::write_dispatcher::TrackedWrite;
24use operator::write_dispatcher::WriteDispatcherRef;
25use partition::manager::PartitionRuleManagerRef;
26use session::context::QueryContextRef;
27use snafu::{IntoError, OptionExt, ensure};
28
29use crate::batcher::flow_notifier::FlowNotifier;
30use crate::batcher::logical_table;
31use crate::batcher::logical_table::batch_convert::{self, TableBatch};
32use crate::batcher::logical_table::flow_notifier;
33use crate::batcher::logical_table::pending_worker::FlushWaiter;
34use crate::batcher::logical_table::region_write::{
35    self, CatalogManagerPhysicalFlushAdapter, PartitionManagerPhysicalFlushAdapter,
36    PhysicalFlushCatalogProvider, PhysicalFlushPartitionProvider, PhysicalFlushWriteDispatcher,
37    WriteDispatcherPhysicalFlushAdapter,
38};
39use crate::error;
40use crate::error::Result;
41use crate::metrics;
42
43/// Rows and completion waiters owned by one physical flush.
44pub(crate) struct Batch {
45    pub(crate) table_batches: Vec<TableBatch>,
46    pub(crate) total_row_count: usize,
47    pub(crate) db_string: String,
48    pub(crate) ctx: QueryContextRef,
49    pub(crate) waiters: Vec<FlushWaiter>,
50}
51
52/// Spawns a physical flush with the configured concurrency limit.
53pub(crate) async fn spawn_flush(
54    flush: Batch,
55    partition_manager: PartitionRuleManagerRef,
56    write_dispatcher: WriteDispatcherRef,
57    catalog_manager: CatalogManagerRef,
58    flow_notification_tx: FlowNotifier,
59    flush_limiter: FlushLimiter,
60) {
61    match flush_limiter.acquire().await {
62        Ok(permit) => {
63            tokio::spawn(async move {
64                let _permit = permit;
65                flush_batch_with_managers(
66                    flush,
67                    partition_manager,
68                    write_dispatcher,
69                    catalog_manager,
70                    flow_notification_tx,
71                )
72                .await;
73            });
74        }
75        Err(err) => {
76            warn!(err; "Flush semaphore closed, flushing inline");
77            flush_batch_with_managers(
78                flush,
79                partition_manager,
80                write_dispatcher,
81                catalog_manager,
82                flow_notification_tx,
83            )
84            .await;
85        }
86    }
87}
88
89/// Connects shared routing and dispatch dependencies to a physical flush.
90pub(crate) async fn flush_batch_with_managers(
91    flush: Batch,
92    partition_manager: PartitionRuleManagerRef,
93    write_dispatcher: WriteDispatcherRef,
94    catalog_manager: CatalogManagerRef,
95    flow_notification_tx: FlowNotifier,
96) {
97    let partition_provider = PartitionManagerPhysicalFlushAdapter { partition_manager };
98    let write_dispatcher = WriteDispatcherPhysicalFlushAdapter { write_dispatcher };
99    let catalog_provider = CatalogManagerPhysicalFlushAdapter { catalog_manager };
100    flush_batch(
101        flush,
102        &partition_provider,
103        &write_dispatcher,
104        &catalog_provider,
105        flow_notification_tx,
106    )
107    .await;
108}
109
110/// Flushes a batch and reports completion to its waiters.
111pub(crate) async fn flush_batch(
112    flush: Batch,
113    partition_manager: &(impl PhysicalFlushPartitionProvider + ?Sized),
114    write_dispatcher: &(impl PhysicalFlushWriteDispatcher + ?Sized),
115    catalog_manager: &(impl PhysicalFlushCatalogProvider + ?Sized),
116    flow_notification_tx: FlowNotifier,
117) {
118    let Batch {
119        table_batches,
120        total_row_count,
121        db_string,
122        ctx,
123        waiters,
124    } = flush;
125    let start = Instant::now();
126
127    // Physical-table-level flush: transform all logical table batches
128    // into physical format and write them together.
129    let physical_table_name = ctx
130        .extension(logical_table::PHYSICAL_TABLE_KEY)
131        .unwrap_or(prelude::GREPTIME_PHYSICAL_TABLE)
132        .to_string();
133    let result = flush_batch_physical_tracked(
134        &table_batches,
135        &physical_table_name,
136        &ctx,
137        partition_manager,
138        write_dispatcher,
139        catalog_manager,
140        waiters.len().max(
141            table_batches
142                .iter()
143                .flat_map(|table| &table.batches)
144                .map(|batch| batch.owner + 1)
145                .max()
146                .unwrap_or(0),
147        ),
148    )
149    .await;
150
151    let elapsed = start.elapsed().as_secs_f64();
152    metrics::FLUSH_ELAPSED.observe(elapsed);
153
154    debug!(
155        "Pending rows batch flushed, total rows: {}, elapsed time: {}s",
156        total_row_count, elapsed
157    );
158
159    match result {
160        Ok(progress) => {
161            if progress.iter().any(|result| result.error.is_some()) {
162                metrics::FLUSH_FAILURES.inc();
163            } else {
164                metrics::FLUSH_TOTAL.inc();
165                metrics::FLUSH_ROWS.observe(total_row_count as f64);
166            }
167            let affected_rows = progress.iter().map(|result| result.affected_rows).sum();
168            metrics::FLUSH_DROPPED_ROWS
169                .inc_by(total_row_count.saturating_sub(affected_rows) as u64);
170            let successful = progress
171                .iter()
172                .enumerate()
173                .filter_map(|(owner, result)| result.error.is_none().then_some(owner))
174                .collect::<HashSet<_>>();
175            for (waiter, result) in waiters.into_iter().zip(progress) {
176                operator::metrics::DIST_INGEST_ROW_COUNT
177                    .with_label_values(&[db_string.as_str()])
178                    .inc_by(result.affected_rows as u64);
179                let _ = waiter.response_tx.send(result);
180            }
181            let accepted = table_batches
182                .into_iter()
183                .filter_map(|mut table| {
184                    table
185                        .batches
186                        .retain(|batch| successful.contains(&batch.owner));
187                    table.row_count = table
188                        .batches
189                        .iter()
190                        .map(|batch| batch.batch.num_rows())
191                        .sum();
192                    (!table.batches.is_empty()).then_some(table)
193                })
194                .collect();
195            flow_notifier::enqueue_flow_notifications(accepted, &flow_notification_tx);
196        }
197        Err(err) => {
198            metrics::FLUSH_FAILURES.inc();
199            metrics::FLUSH_DROPPED_ROWS.inc_by(total_row_count as u64);
200            notify_waiters(waiters, Err(err));
201        }
202    }
203}
204
205/// Flushes a batch of logical table rows by transforming them into the physical table format
206/// and writing them to the appropriate datanode regions.
207///
208/// This function performs the end-to-end physical flush pipeline:
209/// 1. Resolves the physical table metadata and column ID mapping.
210/// 2. Fetches the physical table's partition rule.
211/// 3. Transforms each logical table batch into the physical (sparse primary key) format.
212/// 4. Concatenates all transformed batches into a single combined batch.
213/// 5. Splits the combined batch by partition rule and sends region write requests
214///    concurrently to the target datanodes.
215pub async fn flush_batch_physical(
216    table_batches: &[TableBatch],
217    physical_table_name: &str,
218    ctx: &QueryContextRef,
219    partition_manager: &(impl PhysicalFlushPartitionProvider + ?Sized),
220    write_dispatcher: &(impl PhysicalFlushWriteDispatcher + ?Sized),
221    catalog_manager: &(impl PhysicalFlushCatalogProvider + ?Sized),
222) -> Result<usize> {
223    let inputs = table_batches
224        .iter()
225        .flat_map(|table| &table.batches)
226        .map(|batch| batch.owner + 1)
227        .max()
228        .unwrap_or(1);
229    let progress = flush_batch_physical_tracked(
230        table_batches,
231        physical_table_name,
232        ctx,
233        partition_manager,
234        write_dispatcher,
235        catalog_manager,
236        inputs,
237    )
238    .await?;
239    let mut result = WriteProgress::default();
240    for part in progress {
241        result.merge(part);
242    }
243    result
244        .into_result()
245        .map_err(|source| error::ExecuteGrpcQuerySnafu.into_error(source))
246}
247
248async fn flush_batch_physical_tracked(
249    table_batches: &[TableBatch],
250    physical_table_name: &str,
251    ctx: &QueryContextRef,
252    partition_manager: &(impl PhysicalFlushPartitionProvider + ?Sized),
253    write_dispatcher: &(impl PhysicalFlushWriteDispatcher + ?Sized),
254    catalog_manager: &(impl PhysicalFlushCatalogProvider + ?Sized),
255    inputs: usize,
256) -> Result<Vec<WriteProgress>> {
257    // 1. Resolve the physical table and get column ID mapping
258    let physical_table = {
259        let _timer = metrics::PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
260            .with_label_values(&["flush_physical_resolve_table"])
261            .start_timer();
262        catalog_manager
263            .physical_table(
264                ctx.current_catalog(),
265                &ctx.current_schema(),
266                physical_table_name,
267                ctx.as_ref(),
268            )
269            .await?
270            .with_context(|| error::InternalSnafu {
271                err_msg: format!(
272                    "Physical table '{}' not found during pending flush",
273                    physical_table_name
274                ),
275            })?
276    };
277
278    let physical_table_info = physical_table.table_info;
279    let name_to_ids = physical_table
280        .col_name_to_ids
281        .with_context(|| error::InternalSnafu {
282            err_msg: format!(
283                "Physical table '{}' has no column IDs for pending flush",
284                physical_table_name
285            ),
286        })?;
287
288    // 2. Get the physical table's partition rule (one lookup instead of N)
289    let (partition_rule, partition_versions) = {
290        let _timer = metrics::PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
291            .with_label_values(&["flush_physical_fetch_partition_rule"])
292            .start_timer();
293        partition_manager
294            .find_table_partition_rule(physical_table_info.as_ref())
295            .await?
296    };
297    let partition_columns = partition_rule.partition_columns();
298    let partition_columns_set: HashSet<&str> =
299        partition_columns.iter().map(String::as_str).collect();
300
301    // 3. Transform each logical table batch into physical format
302    let modified_batches = batch_convert::transform_logical_batches_to_physical(
303        table_batches,
304        &name_to_ids,
305        &partition_columns_set,
306    )?;
307
308    // 4. Concatenate all modified batches (all share the same physical schema)
309    let combined_batch = batch_convert::concat_modified_batches(&modified_batches)?;
310    // 5. Split by physical partition rule and send to regions
311    let physical_table_id = physical_table_info.table_id();
312    let row_count = table_batches
313        .iter()
314        .flat_map(|table| &table.batches)
315        .map(|batch| batch.batch.num_rows())
316        .sum::<usize>();
317    ensure!(
318        row_count == combined_batch.num_rows(),
319        error::InternalSnafu {
320            err_msg: "Physical transformation changed row ownership"
321        }
322    );
323    let masks =
324        region_write::split_combined_batch_by_region(&combined_batch, partition_rule.as_ref())?;
325    let mut writes = Vec::new();
326    for (region, mask) in masks {
327        let mut selected = Vec::with_capacity(mask.selected_rows());
328        let bits = mask.array().values();
329        let mut offset = 0;
330        // Filtering preserves batch order, and each original batch has one owner.
331        for batch in table_batches.iter().flat_map(|table| &table.batches) {
332            let num_rows = batch.batch.num_rows();
333            let selected_rows = if num_rows == 1 {
334                // Avoid bitmap range setup for single-row submissions.
335                usize::from(bits.value(offset))
336            } else {
337                // `offset` is mask-relative; the raw buffer also needs the mask's slice offset.
338                bits.inner()
339                    .count_set_bits_offset(bits.offset() + offset, num_rows)
340            };
341            selected.extend(std::iter::repeat_n(batch.owner, selected_rows));
342            offset += num_rows;
343        }
344        if let Some(batch) =
345            region_write::plan_region_batch(&combined_batch, physical_table_id, region, &mask)?
346        {
347            let request = region_write::encode_region_write_request(
348                batch,
349                ctx.skip_wal(),
350                &partition_versions,
351            )?;
352            writes.push(TrackedWrite {
353                request,
354                owners: selected,
355            });
356        }
357    }
358    let progress = write_dispatcher.dispatch_tracked(writes, inputs).await;
359    // A submission can contribute several logical batches; credit its counter once.
360    let counters = table_batches
361        .iter()
362        .flat_map(|table| &table.batches)
363        .filter_map(|batch| {
364            batch
365                .progress
366                .as_ref()
367                .map(|counter| (batch.owner, counter))
368        })
369        .collect::<std::collections::HashMap<_, _>>();
370    for (owner, counter) in counters {
371        counter.add_written(progress[owner].affected_rows);
372    }
373    Ok(progress)
374}
375
376pub(in crate::batcher::logical_table) fn notify_waiters(
377    waiters: Vec<FlushWaiter>,
378    result: Result<()>,
379) {
380    let progress = match result {
381        Ok(()) => WriteProgress::default(),
382        Err(error) => WriteProgress::failed(BoxedError::new(error)),
383    };
384    for waiter in waiters {
385        let _ = waiter.response_tx.send(progress.clone());
386    }
387}
388
389#[cfg(test)]
390mod tests {
391    use std::collections::HashMap;
392    use std::future::poll_fn;
393    use std::sync::Arc;
394    use std::sync::atomic::{AtomicUsize, Ordering};
395    use std::task::Poll;
396    use std::time::Duration;
397
398    use api::v1::meta::Peer;
399    use api::v1::region::RegionRequest;
400    use arrow::record_batch::RecordBatch;
401    use async_trait::async_trait;
402    use catalog::error::Result as CatalogResult;
403    use common_batcher::request_limiter::RequestLimiter;
404    use common_meta::cache::TableFlownodeSetCacheRef;
405    use common_meta::node_manager::NodeManagerRef;
406    use datatypes::schema::{ColumnSchema as DtColumnSchema, Schema as DtSchema};
407    use partition::error::Result as PartitionResult;
408    use partition::partition::{PartitionRule, PartitionRuleRef, RegionMask};
409    use table::metadata::TableId;
410    use table::test_util::table_info::test_table_info;
411    use tokio::sync::{mpsc, oneshot};
412
413    use crate::batcher::flow_notifier::{FlowNotifier, start_flow_notification_worker};
414    use crate::batcher::logical_table::batch::{
415        Batch, flush_batch, flush_batch_physical, notify_waiters,
416    };
417    use crate::batcher::logical_table::batch_convert::TableBatch;
418    use crate::batcher::logical_table::pending_worker::FlushWaiter;
419    use crate::batcher::logical_table::region_write::{
420        PhysicalFlushCatalogProvider, PhysicalFlushPartitionProvider, PhysicalFlushWriteDispatcher,
421        PhysicalTableMetadata,
422    };
423    use crate::batcher::logical_table::test_util::{
424        FlowNotificationMockNodeManager, RecordingFlownode, mock_aligned_tag_batch,
425    };
426    use crate::batcher::test_util::mock_table_flownode_cache;
427    use crate::error;
428    use crate::error::Error;
429    use crate::metrics::{self, FLOW_NOTIFICATION_DROPPED};
430
431    #[tokio::test]
432    async fn test_flush_batch_notifies_flownode_after_successful_physical_write() {
433        let table_id = 42;
434        let peer = Peer {
435            id: 7,
436            addr: "flow-7".to_string(),
437        };
438        let cache = mock_table_flownode_cache(table_id, vec![(0, peer.clone()), (1, peer)]).await;
439        let (requests_tx, mut requests_rx) = mpsc::unbounded_channel();
440        let _requests_tx = requests_tx.clone();
441        let flow_node_manager: NodeManagerRef = Arc::new(FlowNotificationMockNodeManager {
442            flownode: Arc::new(RecordingFlownode { requests_tx }),
443        });
444        let flow_notification_tx = mock_flow_notification_sender(cache, flow_node_manager.clone());
445        let ctx = session::context::QueryContext::arc();
446        let table_batches = vec![TableBatch {
447            table_name: "cpu".to_string(),
448            table_id,
449            batches: vec![mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0)],
450            row_count: 1,
451        }];
452        let writes = Arc::new(AtomicUsize::new(0));
453
454        flush_batch(
455            Batch {
456                table_batches,
457                total_row_count: 1,
458                db_string: ctx.get_db_string(),
459                ctx,
460                waiters: Vec::new(),
461            },
462            &MockFlushPartitionProvider {
463                partition_rule_calls: Arc::new(AtomicUsize::new(0)),
464            },
465            &MockFlushWriteDispatcher {
466                writes: writes.clone(),
467                fail: false,
468            },
469            &MockFlushCatalogProvider {
470                table: Some(mock_physical_table_metadata(1024)),
471            },
472            flow_notification_tx,
473        )
474        .await;
475
476        assert_eq!(1, writes.load(Ordering::SeqCst));
477        let requests = tokio::time::timeout(Duration::from_secs(1), requests_rx.recv())
478            .await
479            .unwrap()
480            .unwrap();
481        assert_eq!(
482            vec![api::v1::flow::DirtyWindowRequest {
483                table_id,
484                timestamps: vec![1000],
485                time_ranges: Vec::new(),
486            }],
487            requests.requests
488        );
489    }
490
491    #[tokio::test]
492    async fn test_flush_batch_does_not_notify_flownode_after_physical_write_error() {
493        let table_id = 42;
494        let peer = Peer {
495            id: 7,
496            addr: "flow-7".to_string(),
497        };
498        let cache = mock_table_flownode_cache(table_id, vec![(0, peer.clone()), (1, peer)]).await;
499        let (requests_tx, mut requests_rx) = mpsc::unbounded_channel();
500        let _requests_tx = requests_tx.clone();
501        let flow_node_manager: NodeManagerRef = Arc::new(FlowNotificationMockNodeManager {
502            flownode: Arc::new(RecordingFlownode { requests_tx }),
503        });
504        let flow_notification_tx = mock_flow_notification_sender(cache, flow_node_manager.clone());
505        let ctx = session::context::QueryContext::arc();
506        let table_batches = vec![TableBatch {
507            table_name: "cpu".to_string(),
508            table_id,
509            batches: vec![mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0)],
510            row_count: 1,
511        }];
512        let writes = Arc::new(AtomicUsize::new(0));
513
514        flush_batch(
515            Batch {
516                table_batches,
517                total_row_count: 1,
518                db_string: ctx.get_db_string(),
519                ctx,
520                waiters: Vec::new(),
521            },
522            &MockFlushPartitionProvider {
523                partition_rule_calls: Arc::new(AtomicUsize::new(0)),
524            },
525            &MockFlushWriteDispatcher {
526                writes: writes.clone(),
527                fail: true,
528            },
529            &MockFlushCatalogProvider {
530                table: Some(mock_physical_table_metadata(1024)),
531            },
532            flow_notification_tx,
533        )
534        .await;
535
536        assert_eq!(1, writes.load(Ordering::SeqCst));
537        assert!(
538            tokio::time::timeout(Duration::from_millis(50), requests_rx.recv())
539                .await
540                .is_err()
541        );
542    }
543
544    #[tokio::test]
545    async fn test_cancelled_waiter_retains_request_slot_until_notification() {
546        let limiter = RequestLimiter::try_new(1).unwrap();
547        let (response_tx, response_rx) = oneshot::channel();
548        let waiter = FlushWaiter {
549            _completion: None,
550            response_tx,
551            _permit: limiter.acquire().await.unwrap(),
552        };
553        drop(response_rx);
554        let next = limiter.acquire();
555        tokio::pin!(next);
556        assert!(poll_fn(|cx| Poll::Ready(next.as_mut().poll(cx).is_pending())).await);
557        notify_waiters(vec![waiter], Ok(()));
558        let _permit = next.await.unwrap();
559    }
560
561    #[tokio::test]
562    async fn test_flush_batch_physical_uses_mockable_trait_dependencies() {
563        let table_batches = vec![TableBatch {
564            table_name: "t1".to_string(),
565            table_id: 11,
566            batches: vec![mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0)],
567            row_count: 1,
568        }];
569        let partition_calls = Arc::new(AtomicUsize::new(0));
570        let dispatcher = MockFlushWriteDispatcher::default();
571        let ctx = session::context::QueryContext::arc();
572
573        flush_batch_physical(
574            &table_batches,
575            "phy",
576            &ctx,
577            &MockFlushPartitionProvider {
578                partition_rule_calls: partition_calls.clone(),
579            },
580            &dispatcher,
581            &MockFlushCatalogProvider {
582                table: Some(mock_physical_table_metadata(1024)),
583            },
584        )
585        .await
586        .unwrap();
587
588        assert_eq!(1, partition_calls.load(Ordering::SeqCst));
589        assert_eq!(1, dispatcher.writes.load(Ordering::SeqCst));
590    }
591
592    #[tokio::test]
593    async fn test_flush_batch_physical_rejects_unattributable_affected_rows() {
594        let table_batches = vec![TableBatch {
595            table_name: "t1".to_string(),
596            table_id: 11,
597            batches: vec![mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0)],
598            row_count: 1,
599        }];
600        let ctx = session::context::QueryContext::arc();
601
602        let error = flush_batch_physical(
603            &table_batches,
604            "phy",
605            &ctx,
606            &MockFlushPartitionProvider {
607                partition_rule_calls: Arc::new(AtomicUsize::new(0)),
608            },
609            &AffectedRowsFlushWriteDispatcher { affected_rows: 7 },
610            &MockFlushCatalogProvider {
611                table: Some(mock_physical_table_metadata(1024)),
612            },
613        )
614        .await
615        .unwrap_err();
616
617        assert!(format!("{error:?}").contains("Unattributable"));
618    }
619
620    #[tokio::test]
621    async fn test_exemplar_coalesced_logical_flush_retains_each_requests_progress() {
622        use common_error::ext::BoxedError;
623        use common_query::WriteProgress;
624        use operator::write_dispatcher::TrackedWrite;
625
626        struct ProgressDispatcher([(usize, bool); 2]);
627        #[async_trait]
628        impl PhysicalFlushWriteDispatcher for ProgressDispatcher {
629            async fn dispatch(&self, _: Vec<RegionRequest>) -> error::Result<usize> {
630                unreachable!()
631            }
632            async fn dispatch_tracked(
633                &self,
634                requests: Vec<TrackedWrite>,
635                inputs: usize,
636            ) -> Vec<WriteProgress> {
637                assert_eq!(inputs, 2);
638                assert_eq!(
639                    requests
640                        .iter()
641                        .flat_map(|r| &r.owners)
642                        .copied()
643                        .collect::<Vec<_>>(),
644                    vec![0, 1, 1]
645                );
646                self.0
647                    .iter()
648                    .map(|&(affected_rows, failed)| WriteProgress {
649                        affected_rows,
650                        error: failed.then(|| {
651                            Arc::new(BoxedError::new(
652                                error::InternalSnafu {
653                                    err_msg: "lost acknowledgement",
654                                }
655                                .build(),
656                            ))
657                        }),
658                    })
659                    .collect()
660            }
661        }
662        let ctx = session::context::QueryContext::arc();
663        let ingested = operator::metrics::DIST_INGEST_ROW_COUNT
664            .with_label_values(&[ctx.get_db_string().as_str()]);
665        for (expected_progress, successful_flush, expected_timestamps) in [
666            ([(1, false), (2, false)], true, vec![1000; 3]),
667            ([(1, false), (1, true)], false, vec![1000]),
668            ([(0, true), (0, true)], false, vec![]),
669        ] {
670            let limiter = RequestLimiter::try_new(2).unwrap();
671            let mut waiters = Vec::new();
672            let mut receivers = Vec::new();
673            let counters =
674                [0, 1].map(|_| Arc::new(session::write_progress::WriteProgress::default()));
675            for counter in &counters {
676                let (response_tx, response_rx) = oneshot::channel();
677                waiters.push(FlushWaiter {
678                    _completion: Some(counter.start()),
679                    response_tx,
680                    _permit: limiter.acquire().await.unwrap(),
681                });
682                receivers.push(response_rx);
683            }
684            let batches = [0, 1, 1]
685                .into_iter()
686                .map(|owner| {
687                    let mut batch = mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0);
688                    batch.owner = owner;
689                    batch.progress = Some(counters[owner].clone());
690                    batch
691                })
692                .collect();
693            let (notifier, mut notifications) =
694                FlowNotifier::try_new(1, FLOW_NOTIFICATION_DROPPED.clone()).unwrap();
695            let successes = metrics::FLUSH_TOTAL.get();
696            let failures = metrics::FLUSH_FAILURES.get();
697            let samples = metrics::FLUSH_ROWS.get_sample_count();
698            let sum = metrics::FLUSH_ROWS.get_sample_sum();
699            let dropped = metrics::FLUSH_DROPPED_ROWS.get();
700            let written = ingested.get();
701            flush_batch(
702                Batch {
703                    table_batches: vec![TableBatch {
704                        table_name: "cpu".into(),
705                        table_id: 42,
706                        batches,
707                        row_count: 3,
708                    }],
709                    total_row_count: 3,
710                    db_string: ctx.get_db_string(),
711                    ctx: ctx.clone(),
712                    waiters,
713                },
714                &MockFlushPartitionProvider {
715                    partition_rule_calls: Arc::new(AtomicUsize::new(0)),
716                },
717                &ProgressDispatcher(expected_progress),
718                &MockFlushCatalogProvider {
719                    table: Some(mock_physical_table_metadata(1024)),
720                },
721                notifier,
722            )
723            .await;
724            let expected_written: usize = expected_progress.iter().map(|(rows, _)| rows).sum();
725            assert_eq!(
726                metrics::FLUSH_TOTAL.get(),
727                successes + u64::from(successful_flush)
728            );
729            assert_eq!(
730                metrics::FLUSH_FAILURES.get(),
731                failures + u64::from(!successful_flush)
732            );
733            assert_eq!(
734                metrics::FLUSH_ROWS.get_sample_count(),
735                samples + u64::from(successful_flush)
736            );
737            assert_eq!(
738                metrics::FLUSH_ROWS.get_sample_sum(),
739                sum + if successful_flush { 3.0 } else { 0.0 }
740            );
741            assert_eq!(
742                metrics::FLUSH_DROPPED_ROWS.get(),
743                dropped + (3 - expected_written) as u64
744            );
745            assert_eq!(ingested.get(), written + expected_written as u64);
746            for (counter, (affected_rows, _)) in counters.iter().zip(expected_progress) {
747                counter.wait().await;
748                assert_eq!(counter.written(), affected_rows as u64);
749            }
750            for (receiver, (affected_rows, failed)) in receivers.into_iter().zip(expected_progress)
751            {
752                let result = receiver.await.unwrap();
753                assert_eq!(result.affected_rows, affected_rows);
754                assert_eq!(result.error.is_some(), failed);
755            }
756            if !expected_timestamps.is_empty() {
757                let notification = notifications.try_recv().unwrap();
758                assert_eq!(notification.table_id, 42);
759                assert_eq!(notification.timestamps, expected_timestamps);
760            }
761            assert!(notifications.try_recv().is_err());
762        }
763    }
764
765    #[tokio::test]
766    async fn test_flush_batch_physical_preserves_owners_with_sliced_masks() {
767        use std::sync::Mutex;
768
769        use api::v1::region::{bulk_insert_request, region_request};
770        use arrow::array::{BooleanArray, Float64Array, StringArray, TimestampMillisecondArray};
771        use common_grpc::flight::FlightDecoder;
772        use common_query::WriteProgress;
773        use operator::write_dispatcher::TrackedWrite;
774
775        #[derive(Clone)]
776        struct MaskPartitions {
777            offset: usize,
778            pattern: usize,
779        }
780
781        impl PartitionRule for MaskPartitions {
782            fn as_any(&self) -> &dyn std::any::Any {
783                self
784            }
785
786            fn partition_columns(&self) -> &[String] {
787                &[]
788            }
789
790            fn find_region(&self, _: &[datatypes::prelude::Value]) -> PartitionResult<u32> {
791                unreachable!()
792            }
793
794            fn split_record_batch(
795                &self,
796                batch: &RecordBatch,
797            ) -> PartitionResult<HashMap<u32, RegionMask>> {
798                Ok([false, true]
799                    .into_iter()
800                    .enumerate()
801                    .map(|(region, selected)| {
802                        let values = std::iter::repeat_n(true, self.offset)
803                            .chain((0..batch.num_rows()).map(|row| {
804                                let matches = match self.pattern {
805                                    0 => false,
806                                    1 => true,
807                                    _ => row % 3 != 0,
808                                };
809                                matches == selected
810                            }))
811                            .chain([true; 8])
812                            .collect::<Vec<_>>();
813                        let mask = BooleanArray::from(values).slice(self.offset, batch.num_rows());
814                        (region as u32, RegionMask::from(mask))
815                    })
816                    .collect())
817            }
818        }
819
820        #[async_trait]
821        impl PhysicalFlushPartitionProvider for MaskPartitions {
822            async fn find_table_partition_rule(
823                &self,
824                _: &table::metadata::TableInfo,
825            ) -> PartitionResult<(PartitionRuleRef, HashMap<u32, Option<u64>>)> {
826                Ok((Arc::new(self.clone()), HashMap::new()))
827            }
828        }
829
830        #[derive(Default)]
831        struct RecordingDispatcher(Mutex<Vec<TrackedWrite>>);
832
833        #[async_trait]
834        impl PhysicalFlushWriteDispatcher for RecordingDispatcher {
835            async fn dispatch(&self, _: Vec<RegionRequest>) -> error::Result<usize> {
836                unreachable!()
837            }
838
839            async fn dispatch_tracked(
840                &self,
841                requests: Vec<TrackedWrite>,
842                inputs: usize,
843            ) -> Vec<WriteProgress> {
844                let mut progress = vec![WriteProgress::default(); inputs];
845                for owner in requests.iter().flat_map(|request| &request.owners) {
846                    progress[*owner].affected_rows += 1;
847                }
848                *self.0.lock().unwrap() = requests;
849                progress
850            }
851        }
852
853        let ctx = session::context::QueryContext::arc();
854        let catalog = MockFlushCatalogProvider {
855            table: Some(mock_physical_table_metadata(1024)),
856        };
857        for lengths in [[0; 7], [1; 7], [0, 3, 65, 1, 127, 0, 64]] {
858            let mut expected = Vec::new();
859            let tables = lengths
860                .chunks(4)
861                .enumerate()
862                .map(|(table, lengths)| {
863                    let batches = lengths
864                        .iter()
865                        .enumerate()
866                        .map(|(index, &rows)| {
867                            let owner = [2, 0, 1, 0, 2, 1, 0][table * 4 + index];
868                            let start = expected.len();
869                            expected.extend(std::iter::repeat_n(owner, rows));
870                            let mut batch = mock_aligned_tag_batch("tag1", "host-1", 0, 1.0);
871                            batch.batch = RecordBatch::try_new(
872                                batch.batch.schema(),
873                                vec![
874                                    Arc::new(TimestampMillisecondArray::from_iter_values(
875                                        (start..start + rows).map(|row| row as i64),
876                                    )),
877                                    Arc::new(Float64Array::from(vec![1.0; rows])),
878                                    Arc::new(StringArray::from(vec!["host-1"; rows])),
879                                ],
880                            )
881                            .unwrap();
882                            batch.owner = owner;
883                            batch
884                        })
885                        .collect();
886                    TableBatch {
887                        table_name: format!("logical_{table}"),
888                        table_id: 100 + table as u32,
889                        batches,
890                        row_count: lengths.iter().sum(),
891                    }
892                })
893                .collect::<Vec<_>>();
894            for offset in [0, 1, 7, 65] {
895                for pattern in 0..3 {
896                    let dispatcher = RecordingDispatcher::default();
897                    let affected = flush_batch_physical(
898                        &tables,
899                        "phy",
900                        &ctx,
901                        &MaskPartitions { offset, pattern },
902                        &dispatcher,
903                        &catalog,
904                    )
905                    .await
906                    .unwrap();
907                    assert_eq!(affected, expected.len());
908                    let mut actual = Vec::new();
909                    for write in dispatcher.0.into_inner().unwrap() {
910                        let Some(region_request::Body::BulkInsert(request)) = write.request.body
911                        else {
912                            panic!("expected bulk request")
913                        };
914                        let Some(bulk_insert_request::Body::ArrowIpc(ipc)) = request.body else {
915                            panic!("expected Arrow IPC")
916                        };
917                        let batch = FlightDecoder::try_from_schema_bytes(&ipc.schema)
918                            .unwrap()
919                            .try_decode_record_batch(&ipc.data_header, &ipc.payload)
920                            .unwrap();
921                        assert_eq!(batch.num_rows(), write.owners.len());
922                        let timestamps = batch
923                            .column_by_name("greptime_timestamp")
924                            .unwrap()
925                            .as_any()
926                            .downcast_ref::<TimestampMillisecondArray>()
927                            .unwrap();
928                        assert!(timestamps.values().windows(2).all(|pair| pair[0] < pair[1]));
929                        actual.extend(timestamps.values().iter().copied().zip(write.owners));
930                    }
931                    actual.sort_unstable();
932                    assert_eq!(
933                        actual,
934                        expected
935                            .iter()
936                            .enumerate()
937                            .map(|(row, owner)| (row as i64, *owner))
938                            .collect::<Vec<_>>(),
939                    );
940                }
941            }
942        }
943    }
944
945    #[tokio::test]
946    async fn test_flush_batch_physical_stops_before_partition_and_dispatch_when_table_missing() {
947        let table_batches = vec![TableBatch {
948            table_name: "t1".to_string(),
949            table_id: 11,
950            batches: vec![mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0)],
951            row_count: 1,
952        }];
953        let partition_calls = Arc::new(AtomicUsize::new(0));
954        let dispatcher = MockFlushWriteDispatcher::default();
955        let ctx = session::context::QueryContext::arc();
956
957        let err = flush_batch_physical(
958            &table_batches,
959            "missing_phy",
960            &ctx,
961            &MockFlushPartitionProvider {
962                partition_rule_calls: partition_calls.clone(),
963            },
964            &dispatcher,
965            &MockFlushCatalogProvider { table: None },
966        )
967        .await
968        .unwrap_err();
969
970        assert!(
971            err.to_string()
972                .contains("Physical table 'missing_phy' not found")
973        );
974        assert_eq!(0, partition_calls.load(Ordering::SeqCst));
975        assert_eq!(0, dispatcher.writes.load(Ordering::SeqCst));
976    }
977
978    #[tokio::test]
979    async fn test_flush_batch_physical_aborts_immediately_on_transform_error() {
980        let table_batches = vec![
981            TableBatch {
982                table_name: "broken".to_string(),
983                table_id: 11,
984                batches: vec![mock_aligned_tag_batch("unknown_tag", "host-1", 1000, 1.0)],
985                row_count: 1,
986            },
987            TableBatch {
988                table_name: "healthy".to_string(),
989                table_id: 12,
990                batches: vec![mock_aligned_tag_batch("tag1", "host-2", 2000, 2.0)],
991                row_count: 1,
992            },
993        ];
994        let partition_calls = Arc::new(AtomicUsize::new(0));
995        let dispatcher = MockFlushWriteDispatcher::default();
996        let ctx = session::context::QueryContext::arc();
997
998        let err = flush_batch_physical(
999            &table_batches,
1000            "phy",
1001            &ctx,
1002            &MockFlushPartitionProvider {
1003                partition_rule_calls: partition_calls.clone(),
1004            },
1005            &dispatcher,
1006            &MockFlushCatalogProvider {
1007                table: Some(mock_physical_table_metadata(1024)),
1008            },
1009        )
1010        .await
1011        .unwrap_err();
1012
1013        assert!(err.to_string().contains("unknown_tag"));
1014        assert_eq!(1, partition_calls.load(Ordering::SeqCst));
1015        assert_eq!(0, dispatcher.writes.load(Ordering::SeqCst));
1016    }
1017
1018    fn mock_physical_table_metadata(table_id: TableId) -> PhysicalTableMetadata {
1019        let schema = Arc::new(
1020            DtSchema::try_new(vec![
1021                DtColumnSchema::new(
1022                    "__primary_key",
1023                    datatypes::prelude::ConcreteDataType::binary_datatype(),
1024                    false,
1025                ),
1026                DtColumnSchema::new(
1027                    "greptime_timestamp",
1028                    datatypes::prelude::ConcreteDataType::timestamp_millisecond_datatype(),
1029                    false,
1030                ),
1031                DtColumnSchema::new(
1032                    "greptime_value",
1033                    datatypes::prelude::ConcreteDataType::float64_datatype(),
1034                    true,
1035                ),
1036                DtColumnSchema::new(
1037                    "tag1",
1038                    datatypes::prelude::ConcreteDataType::string_datatype(),
1039                    true,
1040                ),
1041            ])
1042            .unwrap(),
1043        );
1044        let mut table_info = test_table_info(table_id, "phy", "public", "greptime", schema);
1045        table_info.meta.column_ids = vec![0, 1, 2, 3];
1046
1047        PhysicalTableMetadata {
1048            table_info: Arc::new(table_info),
1049            col_name_to_ids: Some(HashMap::from([("tag1".to_string(), 3)])),
1050        }
1051    }
1052
1053    struct MockFlushCatalogProvider {
1054        table: Option<PhysicalTableMetadata>,
1055    }
1056
1057    #[async_trait]
1058    impl PhysicalFlushCatalogProvider for MockFlushCatalogProvider {
1059        async fn physical_table(
1060            &self,
1061            _catalog: &str,
1062            _schema: &str,
1063            _table_name: &str,
1064            _query_ctx: &session::context::QueryContext,
1065        ) -> CatalogResult<Option<PhysicalTableMetadata>> {
1066            Ok(self.table.clone())
1067        }
1068    }
1069
1070    struct SingleRegionPartitionRule;
1071
1072    impl PartitionRule for SingleRegionPartitionRule {
1073        fn as_any(&self) -> &dyn std::any::Any {
1074            self
1075        }
1076
1077        fn partition_columns(&self) -> &[String] {
1078            &[]
1079        }
1080
1081        fn find_region(
1082            &self,
1083            _values: &[datatypes::prelude::Value],
1084        ) -> partition::error::Result<store_api::storage::RegionNumber> {
1085            unimplemented!()
1086        }
1087
1088        fn split_record_batch(
1089            &self,
1090            record_batch: &RecordBatch,
1091        ) -> partition::error::Result<HashMap<store_api::storage::RegionNumber, RegionMask>>
1092        {
1093            Ok(HashMap::from([(
1094                1,
1095                RegionMask::new(
1096                    arrow::array::BooleanArray::from(vec![true; record_batch.num_rows()]),
1097                    record_batch.num_rows(),
1098                ),
1099            )]))
1100        }
1101    }
1102
1103    struct MockFlushPartitionProvider {
1104        partition_rule_calls: Arc<AtomicUsize>,
1105    }
1106
1107    #[async_trait]
1108    impl PhysicalFlushPartitionProvider for MockFlushPartitionProvider {
1109        async fn find_table_partition_rule(
1110            &self,
1111            _table_info: &table::metadata::TableInfo,
1112        ) -> PartitionResult<(PartitionRuleRef, HashMap<u32, Option<u64>>)> {
1113            self.partition_rule_calls.fetch_add(1, Ordering::SeqCst);
1114            Ok((Arc::new(SingleRegionPartitionRule), HashMap::new()))
1115        }
1116    }
1117
1118    #[derive(Default)]
1119    struct MockFlushWriteDispatcher {
1120        writes: Arc<AtomicUsize>,
1121        fail: bool,
1122    }
1123
1124    #[async_trait]
1125    impl PhysicalFlushWriteDispatcher for MockFlushWriteDispatcher {
1126        async fn dispatch(&self, _requests: Vec<RegionRequest>) -> error::Result<usize> {
1127            self.writes.fetch_add(1, Ordering::SeqCst);
1128            if self.fail {
1129                return Err(Error::Internal {
1130                    err_msg: "physical write failed".to_string(),
1131                });
1132            }
1133            Ok(1)
1134        }
1135    }
1136
1137    fn mock_flow_notification_sender(
1138        cache: TableFlownodeSetCacheRef,
1139        node_manager: NodeManagerRef,
1140    ) -> FlowNotifier {
1141        let (tx, rx) = FlowNotifier::try_new(16, FLOW_NOTIFICATION_DROPPED.clone()).unwrap();
1142        start_flow_notification_worker(rx, cache, node_manager);
1143        tx
1144    }
1145
1146    #[derive(Default)]
1147    struct AffectedRowsFlushWriteDispatcher {
1148        affected_rows: usize,
1149    }
1150
1151    #[async_trait]
1152    impl PhysicalFlushWriteDispatcher for AffectedRowsFlushWriteDispatcher {
1153        async fn dispatch(&self, _requests: Vec<RegionRequest>) -> error::Result<usize> {
1154            Ok(self.affected_rows)
1155        }
1156    }
1157}