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::sync::Arc;
17use std::time::Instant;
18
19use catalog::CatalogManagerRef;
20use common_batcher::flush_limiter::FlushLimiter;
21use common_query::prelude;
22use common_telemetry::{debug, warn};
23use operator::write_dispatcher::WriteDispatcherRef;
24use partition::manager::PartitionRuleManagerRef;
25use session::context::QueryContextRef;
26use snafu::OptionExt;
27
28use crate::batcher::flow_notifier::FlowNotifier;
29use crate::batcher::logical_table;
30use crate::batcher::logical_table::batch_convert::{self, TableBatch};
31use crate::batcher::logical_table::flow_notifier;
32use crate::batcher::logical_table::pending_worker::FlushWaiter;
33use crate::batcher::logical_table::region_write::{
34    self, CatalogManagerPhysicalFlushAdapter, PartitionManagerPhysicalFlushAdapter,
35    PhysicalFlushCatalogProvider, PhysicalFlushPartitionProvider, PhysicalFlushWriteDispatcher,
36    WriteDispatcherPhysicalFlushAdapter,
37};
38use crate::error;
39use crate::error::Result;
40use crate::metrics;
41
42/// Rows and completion waiters owned by one physical flush.
43pub(crate) struct Batch {
44    pub(crate) table_batches: Vec<TableBatch>,
45    pub(crate) total_row_count: usize,
46    pub(crate) db_string: String,
47    pub(crate) ctx: QueryContextRef,
48    pub(crate) waiters: Vec<FlushWaiter>,
49}
50
51/// Spawns a physical flush with the configured concurrency limit.
52pub(crate) async fn spawn_flush(
53    flush: Batch,
54    partition_manager: PartitionRuleManagerRef,
55    write_dispatcher: WriteDispatcherRef,
56    catalog_manager: CatalogManagerRef,
57    flow_notification_tx: FlowNotifier,
58    flush_limiter: FlushLimiter,
59) {
60    match flush_limiter.acquire().await {
61        Ok(permit) => {
62            tokio::spawn(async move {
63                let _permit = permit;
64                flush_batch_with_managers(
65                    flush,
66                    partition_manager,
67                    write_dispatcher,
68                    catalog_manager,
69                    flow_notification_tx,
70                )
71                .await;
72            });
73        }
74        Err(err) => {
75            warn!(err; "Flush semaphore closed, flushing inline");
76            flush_batch_with_managers(
77                flush,
78                partition_manager,
79                write_dispatcher,
80                catalog_manager,
81                flow_notification_tx,
82            )
83            .await;
84        }
85    }
86}
87
88/// Connects shared routing and dispatch dependencies to a physical flush.
89pub(crate) async fn flush_batch_with_managers(
90    flush: Batch,
91    partition_manager: PartitionRuleManagerRef,
92    write_dispatcher: WriteDispatcherRef,
93    catalog_manager: CatalogManagerRef,
94    flow_notification_tx: FlowNotifier,
95) {
96    let partition_provider = PartitionManagerPhysicalFlushAdapter { partition_manager };
97    let write_dispatcher = WriteDispatcherPhysicalFlushAdapter { write_dispatcher };
98    let catalog_provider = CatalogManagerPhysicalFlushAdapter { catalog_manager };
99    flush_batch(
100        flush,
101        &partition_provider,
102        &write_dispatcher,
103        &catalog_provider,
104        flow_notification_tx,
105    )
106    .await;
107}
108
109/// Flushes a batch and reports completion to its waiters.
110pub(crate) async fn flush_batch(
111    flush: Batch,
112    partition_manager: &(impl PhysicalFlushPartitionProvider + ?Sized),
113    write_dispatcher: &(impl PhysicalFlushWriteDispatcher + ?Sized),
114    catalog_manager: &(impl PhysicalFlushCatalogProvider + ?Sized),
115    flow_notification_tx: FlowNotifier,
116) {
117    let Batch {
118        table_batches,
119        total_row_count,
120        db_string,
121        ctx,
122        waiters,
123    } = flush;
124    let start = Instant::now();
125
126    // Physical-table-level flush: transform all logical table batches
127    // into physical format and write them together.
128    let physical_table_name = ctx
129        .extension(logical_table::PHYSICAL_TABLE_KEY)
130        .unwrap_or(prelude::GREPTIME_PHYSICAL_TABLE)
131        .to_string();
132    let result = flush_batch_physical(
133        &table_batches,
134        &physical_table_name,
135        &ctx,
136        partition_manager,
137        write_dispatcher,
138        catalog_manager,
139    )
140    .await;
141
142    let elapsed = start.elapsed().as_secs_f64();
143    metrics::FLUSH_ELAPSED.observe(elapsed);
144
145    debug!(
146        "Pending rows batch flushed, total rows: {}, elapsed time: {}s",
147        total_row_count, elapsed
148    );
149
150    match result {
151        Ok(affected_rows) => {
152            metrics::FLUSH_TOTAL.inc();
153            metrics::FLUSH_ROWS.observe(total_row_count as f64);
154            operator::metrics::DIST_INGEST_ROW_COUNT
155                .with_label_values(&[db_string.as_str()])
156                .inc_by(affected_rows as u64);
157
158            notify_waiters(waiters, Ok(()));
159            flow_notifier::enqueue_flow_notifications(table_batches, &flow_notification_tx);
160        }
161        Err(err) => {
162            metrics::FLUSH_FAILURES.inc();
163            metrics::FLUSH_DROPPED_ROWS.inc_by(total_row_count as u64);
164            notify_waiters(waiters, Err(err));
165        }
166    }
167}
168
169/// Flushes a batch of logical table rows by transforming them into the physical table format
170/// and writing them to the appropriate datanode regions.
171///
172/// This function performs the end-to-end physical flush pipeline:
173/// 1. Resolves the physical table metadata and column ID mapping.
174/// 2. Fetches the physical table's partition rule.
175/// 3. Transforms each logical table batch into the physical (sparse primary key) format.
176/// 4. Concatenates all transformed batches into a single combined batch.
177/// 5. Splits the combined batch by partition rule and sends region write requests
178///    concurrently to the target datanodes.
179pub async fn flush_batch_physical(
180    table_batches: &[TableBatch],
181    physical_table_name: &str,
182    ctx: &QueryContextRef,
183    partition_manager: &(impl PhysicalFlushPartitionProvider + ?Sized),
184    write_dispatcher: &(impl PhysicalFlushWriteDispatcher + ?Sized),
185    catalog_manager: &(impl PhysicalFlushCatalogProvider + ?Sized),
186) -> Result<usize> {
187    // 1. Resolve the physical table and get column ID mapping
188    let physical_table = {
189        let _timer = metrics::PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
190            .with_label_values(&["flush_physical_resolve_table"])
191            .start_timer();
192        catalog_manager
193            .physical_table(
194                ctx.current_catalog(),
195                &ctx.current_schema(),
196                physical_table_name,
197                ctx.as_ref(),
198            )
199            .await?
200            .with_context(|| error::InternalSnafu {
201                err_msg: format!(
202                    "Physical table '{}' not found during pending flush",
203                    physical_table_name
204                ),
205            })?
206    };
207
208    let physical_table_info = physical_table.table_info;
209    let name_to_ids = physical_table
210        .col_name_to_ids
211        .with_context(|| error::InternalSnafu {
212            err_msg: format!(
213                "Physical table '{}' has no column IDs for pending flush",
214                physical_table_name
215            ),
216        })?;
217
218    // 2. Get the physical table's partition rule (one lookup instead of N)
219    let (partition_rule, partition_versions) = {
220        let _timer = metrics::PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
221            .with_label_values(&["flush_physical_fetch_partition_rule"])
222            .start_timer();
223        partition_manager
224            .find_table_partition_rule(physical_table_info.as_ref())
225            .await?
226    };
227    let partition_columns = partition_rule.partition_columns();
228    let partition_columns_set: HashSet<&str> =
229        partition_columns.iter().map(String::as_str).collect();
230
231    // 3. Transform each logical table batch into physical format
232    let modified_batches = batch_convert::transform_logical_batches_to_physical(
233        table_batches,
234        &name_to_ids,
235        &partition_columns_set,
236    )?;
237
238    // 4. Concatenate all modified batches (all share the same physical schema)
239    let combined_batch = batch_convert::concat_modified_batches(&modified_batches)?;
240
241    // 5. Split by physical partition rule and send to regions
242    let physical_table_id = physical_table_info.table_id();
243    let planned_batches = region_write::plan_region_batches(
244        combined_batch,
245        physical_table_id,
246        partition_rule.as_ref(),
247    )?;
248
249    let region_writes = region_write::encode_region_write_requests(
250        planned_batches,
251        ctx.skip_wal(),
252        &partition_versions,
253    )?;
254    write_dispatcher.dispatch(region_writes).await
255}
256
257pub(in crate::batcher::logical_table) fn notify_waiters(
258    waiters: Vec<FlushWaiter>,
259    result: Result<()>,
260) {
261    let shared_result = result.map_err(Arc::new);
262    for waiter in waiters {
263        let _ = waiter.response_tx.send(match &shared_result {
264            Ok(()) => Ok(()),
265            Err(error) => Err(Arc::clone(error)),
266        });
267        // waiter._permit is dropped here, releasing the inflight semaphore slot
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use std::collections::HashMap;
274    use std::future::poll_fn;
275    use std::sync::Arc;
276    use std::sync::atomic::{AtomicUsize, Ordering};
277    use std::task::Poll;
278    use std::time::Duration;
279
280    use api::v1::meta::Peer;
281    use api::v1::region::RegionRequest;
282    use arrow::record_batch::RecordBatch;
283    use async_trait::async_trait;
284    use catalog::error::Result as CatalogResult;
285    use common_batcher::request_limiter::RequestLimiter;
286    use common_meta::cache::TableFlownodeSetCacheRef;
287    use common_meta::node_manager::NodeManagerRef;
288    use datatypes::schema::{ColumnSchema as DtColumnSchema, Schema as DtSchema};
289    use partition::error::Result as PartitionResult;
290    use partition::partition::{PartitionRule, PartitionRuleRef, RegionMask};
291    use table::metadata::TableId;
292    use table::test_util::table_info::test_table_info;
293    use tokio::sync::{mpsc, oneshot};
294
295    use crate::batcher::flow_notifier::{FlowNotifier, start_flow_notification_worker};
296    use crate::batcher::logical_table::batch::{
297        Batch, flush_batch, flush_batch_physical, notify_waiters,
298    };
299    use crate::batcher::logical_table::batch_convert::TableBatch;
300    use crate::batcher::logical_table::pending_worker::FlushWaiter;
301    use crate::batcher::logical_table::region_write::{
302        PhysicalFlushCatalogProvider, PhysicalFlushPartitionProvider, PhysicalFlushWriteDispatcher,
303        PhysicalTableMetadata,
304    };
305    use crate::batcher::logical_table::test_util::{
306        FlowNotificationMockNodeManager, RecordingFlownode, mock_aligned_tag_batch,
307    };
308    use crate::batcher::test_util::mock_table_flownode_cache;
309    use crate::error;
310    use crate::error::Error;
311    use crate::metrics::FLOW_NOTIFICATION_DROPPED;
312
313    #[tokio::test]
314    async fn test_flush_batch_notifies_flownode_after_successful_physical_write() {
315        let table_id = 42;
316        let peer = Peer {
317            id: 7,
318            addr: "flow-7".to_string(),
319        };
320        let cache = mock_table_flownode_cache(table_id, vec![(0, peer.clone()), (1, peer)]).await;
321        let (requests_tx, mut requests_rx) = mpsc::unbounded_channel();
322        let _requests_tx = requests_tx.clone();
323        let flow_node_manager: NodeManagerRef = Arc::new(FlowNotificationMockNodeManager {
324            flownode: Arc::new(RecordingFlownode { requests_tx }),
325        });
326        let flow_notification_tx = mock_flow_notification_sender(cache, flow_node_manager.clone());
327        let ctx = session::context::QueryContext::arc();
328        let table_batches = vec![TableBatch {
329            table_name: "cpu".to_string(),
330            table_id,
331            batches: vec![mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0)],
332            row_count: 1,
333        }];
334        let writes = Arc::new(AtomicUsize::new(0));
335
336        flush_batch(
337            Batch {
338                table_batches,
339                total_row_count: 1,
340                db_string: ctx.get_db_string(),
341                ctx,
342                waiters: Vec::new(),
343            },
344            &MockFlushPartitionProvider {
345                partition_rule_calls: Arc::new(AtomicUsize::new(0)),
346            },
347            &MockFlushWriteDispatcher {
348                writes: writes.clone(),
349                fail: false,
350            },
351            &MockFlushCatalogProvider {
352                table: Some(mock_physical_table_metadata(1024)),
353            },
354            flow_notification_tx,
355        )
356        .await;
357
358        assert_eq!(1, writes.load(Ordering::SeqCst));
359        let requests = tokio::time::timeout(Duration::from_secs(1), requests_rx.recv())
360            .await
361            .unwrap()
362            .unwrap();
363        assert_eq!(
364            vec![api::v1::flow::DirtyWindowRequest {
365                table_id,
366                timestamps: vec![1000],
367                time_ranges: Vec::new(),
368            }],
369            requests.requests
370        );
371    }
372
373    #[tokio::test]
374    async fn test_flush_batch_does_not_notify_flownode_after_physical_write_error() {
375        let table_id = 42;
376        let peer = Peer {
377            id: 7,
378            addr: "flow-7".to_string(),
379        };
380        let cache = mock_table_flownode_cache(table_id, vec![(0, peer.clone()), (1, peer)]).await;
381        let (requests_tx, mut requests_rx) = mpsc::unbounded_channel();
382        let _requests_tx = requests_tx.clone();
383        let flow_node_manager: NodeManagerRef = Arc::new(FlowNotificationMockNodeManager {
384            flownode: Arc::new(RecordingFlownode { requests_tx }),
385        });
386        let flow_notification_tx = mock_flow_notification_sender(cache, flow_node_manager.clone());
387        let ctx = session::context::QueryContext::arc();
388        let table_batches = vec![TableBatch {
389            table_name: "cpu".to_string(),
390            table_id,
391            batches: vec![mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0)],
392            row_count: 1,
393        }];
394        let writes = Arc::new(AtomicUsize::new(0));
395
396        flush_batch(
397            Batch {
398                table_batches,
399                total_row_count: 1,
400                db_string: ctx.get_db_string(),
401                ctx,
402                waiters: Vec::new(),
403            },
404            &MockFlushPartitionProvider {
405                partition_rule_calls: Arc::new(AtomicUsize::new(0)),
406            },
407            &MockFlushWriteDispatcher {
408                writes: writes.clone(),
409                fail: true,
410            },
411            &MockFlushCatalogProvider {
412                table: Some(mock_physical_table_metadata(1024)),
413            },
414            flow_notification_tx,
415        )
416        .await;
417
418        assert_eq!(1, writes.load(Ordering::SeqCst));
419        assert!(
420            tokio::time::timeout(Duration::from_millis(50), requests_rx.recv())
421                .await
422                .is_err()
423        );
424    }
425
426    #[tokio::test]
427    async fn test_cancelled_waiter_retains_request_slot_until_notification() {
428        let limiter = RequestLimiter::try_new(1).unwrap();
429        let (response_tx, response_rx) = oneshot::channel();
430        let waiter = FlushWaiter {
431            response_tx,
432            _permit: limiter.acquire().await.unwrap(),
433        };
434        drop(response_rx);
435        let next = limiter.acquire();
436        tokio::pin!(next);
437        assert!(poll_fn(|cx| Poll::Ready(next.as_mut().poll(cx).is_pending())).await);
438        notify_waiters(vec![waiter], Ok(()));
439        let _permit = next.await.unwrap();
440    }
441
442    #[tokio::test]
443    async fn test_flush_batch_physical_uses_mockable_trait_dependencies() {
444        let table_batches = vec![TableBatch {
445            table_name: "t1".to_string(),
446            table_id: 11,
447            batches: vec![mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0)],
448            row_count: 1,
449        }];
450        let partition_calls = Arc::new(AtomicUsize::new(0));
451        let dispatcher = MockFlushWriteDispatcher::default();
452        let ctx = session::context::QueryContext::arc();
453
454        flush_batch_physical(
455            &table_batches,
456            "phy",
457            &ctx,
458            &MockFlushPartitionProvider {
459                partition_rule_calls: partition_calls.clone(),
460            },
461            &dispatcher,
462            &MockFlushCatalogProvider {
463                table: Some(mock_physical_table_metadata(1024)),
464            },
465        )
466        .await
467        .unwrap();
468
469        assert_eq!(1, partition_calls.load(Ordering::SeqCst));
470        assert_eq!(1, dispatcher.writes.load(Ordering::SeqCst));
471    }
472
473    #[tokio::test]
474    async fn test_flush_batch_physical_returns_actual_affected_rows() {
475        let table_batches = vec![TableBatch {
476            table_name: "t1".to_string(),
477            table_id: 11,
478            batches: vec![mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0)],
479            row_count: 1,
480        }];
481        let ctx = session::context::QueryContext::arc();
482
483        let affected_rows = flush_batch_physical(
484            &table_batches,
485            "phy",
486            &ctx,
487            &MockFlushPartitionProvider {
488                partition_rule_calls: Arc::new(AtomicUsize::new(0)),
489            },
490            &AffectedRowsFlushWriteDispatcher { affected_rows: 7 },
491            &MockFlushCatalogProvider {
492                table: Some(mock_physical_table_metadata(1024)),
493            },
494        )
495        .await
496        .unwrap();
497
498        assert_eq!(7, affected_rows);
499    }
500
501    #[tokio::test]
502    async fn test_flush_batch_physical_stops_before_partition_and_dispatch_when_table_missing() {
503        let table_batches = vec![TableBatch {
504            table_name: "t1".to_string(),
505            table_id: 11,
506            batches: vec![mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0)],
507            row_count: 1,
508        }];
509        let partition_calls = Arc::new(AtomicUsize::new(0));
510        let dispatcher = MockFlushWriteDispatcher::default();
511        let ctx = session::context::QueryContext::arc();
512
513        let err = flush_batch_physical(
514            &table_batches,
515            "missing_phy",
516            &ctx,
517            &MockFlushPartitionProvider {
518                partition_rule_calls: partition_calls.clone(),
519            },
520            &dispatcher,
521            &MockFlushCatalogProvider { table: None },
522        )
523        .await
524        .unwrap_err();
525
526        assert!(
527            err.to_string()
528                .contains("Physical table 'missing_phy' not found")
529        );
530        assert_eq!(0, partition_calls.load(Ordering::SeqCst));
531        assert_eq!(0, dispatcher.writes.load(Ordering::SeqCst));
532    }
533
534    #[tokio::test]
535    async fn test_flush_batch_physical_aborts_immediately_on_transform_error() {
536        let table_batches = vec![
537            TableBatch {
538                table_name: "broken".to_string(),
539                table_id: 11,
540                batches: vec![mock_aligned_tag_batch("unknown_tag", "host-1", 1000, 1.0)],
541                row_count: 1,
542            },
543            TableBatch {
544                table_name: "healthy".to_string(),
545                table_id: 12,
546                batches: vec![mock_aligned_tag_batch("tag1", "host-2", 2000, 2.0)],
547                row_count: 1,
548            },
549        ];
550        let partition_calls = Arc::new(AtomicUsize::new(0));
551        let dispatcher = MockFlushWriteDispatcher::default();
552        let ctx = session::context::QueryContext::arc();
553
554        let err = flush_batch_physical(
555            &table_batches,
556            "phy",
557            &ctx,
558            &MockFlushPartitionProvider {
559                partition_rule_calls: partition_calls.clone(),
560            },
561            &dispatcher,
562            &MockFlushCatalogProvider {
563                table: Some(mock_physical_table_metadata(1024)),
564            },
565        )
566        .await
567        .unwrap_err();
568
569        assert!(err.to_string().contains("unknown_tag"));
570        assert_eq!(1, partition_calls.load(Ordering::SeqCst));
571        assert_eq!(0, dispatcher.writes.load(Ordering::SeqCst));
572    }
573
574    fn mock_physical_table_metadata(table_id: TableId) -> PhysicalTableMetadata {
575        let schema = Arc::new(
576            DtSchema::try_new(vec![
577                DtColumnSchema::new(
578                    "__primary_key",
579                    datatypes::prelude::ConcreteDataType::binary_datatype(),
580                    false,
581                ),
582                DtColumnSchema::new(
583                    "greptime_timestamp",
584                    datatypes::prelude::ConcreteDataType::timestamp_millisecond_datatype(),
585                    false,
586                ),
587                DtColumnSchema::new(
588                    "greptime_value",
589                    datatypes::prelude::ConcreteDataType::float64_datatype(),
590                    true,
591                ),
592                DtColumnSchema::new(
593                    "tag1",
594                    datatypes::prelude::ConcreteDataType::string_datatype(),
595                    true,
596                ),
597            ])
598            .unwrap(),
599        );
600        let mut table_info = test_table_info(table_id, "phy", "public", "greptime", schema);
601        table_info.meta.column_ids = vec![0, 1, 2, 3];
602
603        PhysicalTableMetadata {
604            table_info: Arc::new(table_info),
605            col_name_to_ids: Some(HashMap::from([("tag1".to_string(), 3)])),
606        }
607    }
608
609    struct MockFlushCatalogProvider {
610        table: Option<PhysicalTableMetadata>,
611    }
612
613    #[async_trait]
614    impl PhysicalFlushCatalogProvider for MockFlushCatalogProvider {
615        async fn physical_table(
616            &self,
617            _catalog: &str,
618            _schema: &str,
619            _table_name: &str,
620            _query_ctx: &session::context::QueryContext,
621        ) -> CatalogResult<Option<PhysicalTableMetadata>> {
622            Ok(self.table.clone())
623        }
624    }
625
626    struct SingleRegionPartitionRule;
627
628    impl PartitionRule for SingleRegionPartitionRule {
629        fn as_any(&self) -> &dyn std::any::Any {
630            self
631        }
632
633        fn partition_columns(&self) -> &[String] {
634            &[]
635        }
636
637        fn find_region(
638            &self,
639            _values: &[datatypes::prelude::Value],
640        ) -> partition::error::Result<store_api::storage::RegionNumber> {
641            unimplemented!()
642        }
643
644        fn split_record_batch(
645            &self,
646            record_batch: &RecordBatch,
647        ) -> partition::error::Result<HashMap<store_api::storage::RegionNumber, RegionMask>>
648        {
649            Ok(HashMap::from([(
650                1,
651                RegionMask::new(
652                    arrow::array::BooleanArray::from(vec![true; record_batch.num_rows()]),
653                    record_batch.num_rows(),
654                ),
655            )]))
656        }
657    }
658
659    struct MockFlushPartitionProvider {
660        partition_rule_calls: Arc<AtomicUsize>,
661    }
662
663    #[async_trait]
664    impl PhysicalFlushPartitionProvider for MockFlushPartitionProvider {
665        async fn find_table_partition_rule(
666            &self,
667            _table_info: &table::metadata::TableInfo,
668        ) -> PartitionResult<(PartitionRuleRef, HashMap<u32, Option<u64>>)> {
669            self.partition_rule_calls.fetch_add(1, Ordering::SeqCst);
670            Ok((Arc::new(SingleRegionPartitionRule), HashMap::new()))
671        }
672    }
673
674    #[derive(Default)]
675    struct MockFlushWriteDispatcher {
676        writes: Arc<AtomicUsize>,
677        fail: bool,
678    }
679
680    #[async_trait]
681    impl PhysicalFlushWriteDispatcher for MockFlushWriteDispatcher {
682        async fn dispatch(&self, _requests: Vec<RegionRequest>) -> error::Result<usize> {
683            self.writes.fetch_add(1, Ordering::SeqCst);
684            if self.fail {
685                return Err(Error::Internal {
686                    err_msg: "physical write failed".to_string(),
687                });
688            }
689            Ok(0)
690        }
691    }
692
693    fn mock_flow_notification_sender(
694        cache: TableFlownodeSetCacheRef,
695        node_manager: NodeManagerRef,
696    ) -> FlowNotifier {
697        let (tx, rx) = FlowNotifier::try_new(16, FLOW_NOTIFICATION_DROPPED.clone()).unwrap();
698        start_flow_notification_worker(rx, cache, node_manager);
699        tx
700    }
701
702    #[derive(Default)]
703    struct AffectedRowsFlushWriteDispatcher {
704        affected_rows: usize,
705    }
706
707    #[async_trait]
708    impl PhysicalFlushWriteDispatcher for AffectedRowsFlushWriteDispatcher {
709        async fn dispatch(&self, _requests: Vec<RegionRequest>) -> error::Result<usize> {
710            Ok(self.affected_rows)
711        }
712    }
713}