Skip to main content

risingwave_stream/executor/
dml.rs

1// Copyright 2022 RisingWave Labs
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::BTreeMap;
16use std::future::Future;
17use std::mem;
18
19use either::Either;
20use futures::future::{Either as FutureEither, select};
21use futures::stream::FuturesOrdered;
22use futures::{StreamExt, TryStreamExt};
23use risingwave_common::catalog::{ColumnDesc, TableId, TableVersionId};
24use risingwave_common::transaction::transaction_id::TxnId;
25use risingwave_common::transaction::transaction_message::TxnMsg;
26use risingwave_common_rate_limit::{MonitoredRateLimiter, RateLimit, RateLimiter};
27use risingwave_dml::dml_manager::DmlManagerRef;
28use risingwave_expr::codegen::BoxStream;
29use risingwave_hummock_sdk::HummockReadEpoch;
30use risingwave_pb::common::ThrottleType;
31use risingwave_storage::StateStore;
32use risingwave_storage::store::TryWaitEpochOptions;
33use tokio::sync::oneshot;
34
35use crate::common::rate_limit::rate_limited_pieces;
36use crate::executor::prelude::*;
37use crate::executor::stream_reader::StreamReaderWithPause;
38
39/// [`DmlExecutor`] accepts both stream data and batch data for data manipulation on a specific
40/// table. The two streams will be merged into one and then sent to downstream.
41pub struct DmlExecutor<S: StateStore> {
42    actor_ctx: ActorContextRef,
43
44    upstream: Executor,
45
46    /// Stores the information of batch data channels.
47    dml_manager: DmlManagerRef,
48
49    // Id of the table on which DML performs.
50    table_id: TableId,
51
52    // Version of the table on which DML performs.
53    table_version_id: TableVersionId,
54
55    // Column descriptions of the table.
56    column_descs: Vec<ColumnDesc>,
57
58    chunk_size: usize,
59
60    rate_limiter: Arc<MonitoredRateLimiter>,
61
62    state_store: S,
63}
64
65/// If a transaction's data is less than `MAX_CHUNK_FOR_ATOMICITY` * `CHUNK_SIZE`, we can provide
66/// atomicity. Otherwise, it is possible that part of transaction's data is sent to the downstream
67/// without barrier boundaries. There are some cases that could cause non-atomicity for large
68/// transaction. 1. The system crashes.
69/// 2. Actor scale-in or migration.
70/// 3. Dml's batch query error occurs at the middle of its execution. (e.g. Remove UDF function
71/// server become unavailable).
72const MAX_CHUNK_FOR_ATOMICITY: usize = 32;
73
74#[derive(Debug, Default)]
75struct TxnBuffer {
76    vec: Vec<StreamChunk>,
77    // When vec size exceeds `MAX_CHUNK_FOR_ATOMICITY`, set true to `overflow`.
78    overflow: bool,
79}
80
81impl<S: StateStore> DmlExecutor<S> {
82    #[expect(clippy::too_many_arguments)]
83    pub fn new(
84        actor_ctx: ActorContextRef,
85        upstream: Executor,
86        dml_manager: DmlManagerRef,
87        table_id: TableId,
88        table_version_id: TableVersionId,
89        column_descs: Vec<ColumnDesc>,
90        chunk_size: usize,
91        rate_limit: RateLimit,
92        state_store: S,
93    ) -> Self {
94        let rate_limiter = Arc::new(RateLimiter::new(rate_limit).monitored(table_id));
95        Self {
96            actor_ctx,
97            upstream,
98            dml_manager,
99            table_id,
100            table_version_id,
101            column_descs,
102            chunk_size,
103            rate_limiter,
104            state_store,
105        }
106    }
107
108    #[try_stream(ok = Message, error = StreamExecutorError)]
109    async fn execute_inner(self: Box<Self>) {
110        let mut upstream = self.upstream.execute();
111
112        let actor_id = self.actor_ctx.id;
113
114        // The first barrier message should be propagated.
115        let barrier = expect_first_barrier(&mut upstream).await?;
116
117        // Construct the reader of batch data (DML from users). We must create a variable to hold
118        // this `Arc<TableDmlHandle>` here, or it will be dropped due to the `Weak` reference in
119        // `DmlManager`.
120        //
121        // Note(bugen): Only register after the first barrier message is received, which means the
122        // current executor is activated. This avoids the new reader overwriting the old one during
123        // the preparation of schema change.
124        let handle = self.dml_manager.register_reader(
125            self.table_id,
126            self.table_version_id,
127            &self.column_descs,
128        )?;
129        let reader = apply_dml_rate_limit(
130            handle.stream_reader().into_stream(),
131            self.rate_limiter.clone(),
132        )
133        .boxed()
134        .map_err(StreamExecutorError::from);
135
136        // Merge the two streams using `StreamReaderWithPause` because when we receive a pause
137        // barrier, we should stop receiving the data from DML. We poll data from the two streams in
138        // a round robin way.
139        let mut stream = StreamReaderWithPause::<false, TxnMsg>::new(upstream, reader);
140
141        // If the first barrier requires us to pause on startup, pause the stream.
142        if barrier.is_pause_on_startup() {
143            stream.pause_stream();
144        }
145
146        yield Message::Barrier(barrier);
147
148        // Active transactions: txn_id -> TxnBuffer with transaction chunks.
149        let mut active_txn_map: BTreeMap<TxnId, TxnBuffer> = Default::default();
150        // A batch group of small chunks.
151        let mut batch_group: Vec<StreamChunk> = vec![];
152
153        let mut builder = StreamChunkBuilder::new(
154            self.chunk_size,
155            self.column_descs
156                .iter()
157                .map(|c| c.data_type.clone())
158                .collect(),
159        );
160
161        // Notifiers from end_wait_persistence() calls in the current open epoch.
162        // Drained into a try_wait_epoch future on each barrier.
163        let mut pending_persistence_notifiers: Vec<oneshot::Sender<()>> = Vec::new();
164
165        // In-flight persistence waits, one future per closed epoch that had pending notifiers.
166        // Polled concurrently with the input stream via next_input_driving_persistence.
167        let mut persistence_futures = FuturesOrdered::new();
168
169        while let Some(input_msg) =
170            next_input_driving_persistence(&mut stream, &mut persistence_futures).await?
171        {
172            match input_msg {
173                Either::Left(msg) => {
174                    // Stream messages.
175                    if let Message::Barrier(barrier) = &msg {
176                        // Flush any pending persistence notifiers for the epoch that is closing.
177                        if !pending_persistence_notifiers.is_empty() {
178                            let notifiers = mem::take(&mut pending_persistence_notifiers);
179                            let closing_epoch = barrier.epoch.prev;
180                            let store = self.state_store.clone();
181                            let table_id = self.table_id;
182                            persistence_futures.push_back(async move {
183                                store
184                                    .try_wait_epoch(
185                                        HummockReadEpoch::Committed(closing_epoch),
186                                        TryWaitEpochOptions { table_id },
187                                    )
188                                    .await
189                                    .map_err(StreamExecutorError::from)?;
190                                for tx in notifiers {
191                                    let _ = tx.send(());
192                                }
193                                Ok(())
194                            });
195                        }
196
197                        // We should handle barrier messages here to pause or resume the data from
198                        // DML.
199                        if let Some(mutation) = barrier.mutation.as_deref() {
200                            match mutation {
201                                Mutation::Pause => stream.pause_stream(),
202                                Mutation::Resume => stream.resume_stream(),
203                                Mutation::Throttle(fragment_to_apply) => {
204                                    if let Some(entry) =
205                                        fragment_to_apply.get(&self.actor_ctx.fragment_id)
206                                        && entry.throttle_type() == ThrottleType::Dml
207                                    {
208                                        let new_rate_limit = entry.rate_limit.into();
209                                        let old_rate_limit =
210                                            self.rate_limiter.update(new_rate_limit);
211
212                                        if old_rate_limit != new_rate_limit {
213                                            tracing::info!(
214                                                old_rate_limit = ?old_rate_limit,
215                                                new_rate_limit = ?new_rate_limit,
216                                                %actor_id,
217                                                "dml rate limit changed",
218                                            );
219                                        }
220                                    }
221                                }
222                                _ => {}
223                            }
224                        }
225
226                        // Flush the remaining batch group
227                        if !batch_group.is_empty() {
228                            let vec = mem::take(&mut batch_group);
229                            for chunk in vec {
230                                for (op, row) in chunk.rows() {
231                                    if let Some(chunk) = builder.append_row(op, row) {
232                                        yield Message::Chunk(chunk);
233                                    }
234                                }
235                            }
236                            if let Some(chunk) = builder.take() {
237                                yield Message::Chunk(chunk);
238                            }
239                        }
240                    }
241                    yield msg;
242                }
243                Either::Right(txn_msg) => {
244                    // Batch data.
245                    match txn_msg {
246                        TxnMsg::Begin(txn_id) => {
247                            active_txn_map
248                                .try_insert(txn_id, TxnBuffer::default())
249                                .unwrap_or_else(|_| {
250                                    panic!("Transaction id collision txn_id = {}.", txn_id)
251                                });
252                        }
253                        TxnMsg::End(txn_id, persistence_notifier) => {
254                            if let Some(tx) = persistence_notifier {
255                                pending_persistence_notifiers.push(tx);
256                            }
257                            let mut txn_buffer = active_txn_map.remove(&txn_id)
258                                .unwrap_or_else(|| panic!("Receive an unexpected transaction end message. Active transaction map doesn't contain this transaction txn_id = {}.", txn_id));
259
260                            let txn_buffer_cardinality = txn_buffer
261                                .vec
262                                .iter()
263                                .map(|c| c.cardinality())
264                                .sum::<usize>();
265                            let batch_group_cardinality =
266                                batch_group.iter().map(|c| c.cardinality()).sum::<usize>();
267
268                            if txn_buffer_cardinality >= self.chunk_size {
269                                // txn buffer is too large, so yield batch group first to preserve the transaction order in the same session.
270                                if !batch_group.is_empty() {
271                                    let vec = mem::take(&mut batch_group);
272                                    for chunk in vec {
273                                        for (op, row) in chunk.rows() {
274                                            if let Some(chunk) = builder.append_row(op, row) {
275                                                yield Message::Chunk(chunk);
276                                            }
277                                        }
278                                    }
279                                    if let Some(chunk) = builder.take() {
280                                        yield Message::Chunk(chunk);
281                                    }
282                                }
283
284                                // txn buffer isn't small, so yield.
285                                for chunk in txn_buffer.vec {
286                                    yield Message::Chunk(chunk);
287                                }
288                            } else if txn_buffer_cardinality + batch_group_cardinality
289                                <= self.chunk_size
290                            {
291                                // txn buffer is small and batch group has space.
292                                batch_group.extend(txn_buffer.vec);
293                            } else {
294                                // txn buffer is small and batch group has no space, so yield the batch group first to preserve the transaction order in the same session.
295                                if !batch_group.is_empty() {
296                                    let vec = mem::take(&mut batch_group);
297                                    for chunk in vec {
298                                        for (op, row) in chunk.rows() {
299                                            if let Some(chunk) = builder.append_row(op, row) {
300                                                yield Message::Chunk(chunk);
301                                            }
302                                        }
303                                    }
304                                    if let Some(chunk) = builder.take() {
305                                        yield Message::Chunk(chunk);
306                                    }
307                                }
308
309                                // put txn buffer into the batch group
310                                mem::swap(&mut txn_buffer.vec, &mut batch_group);
311                            }
312                        }
313                        TxnMsg::Rollback(txn_id) => {
314                            let txn_buffer = active_txn_map.remove(&txn_id)
315                                .unwrap_or_else(|| panic!("Receive an unexpected transaction rollback message. Active transaction map doesn't contain this transaction txn_id = {}.", txn_id));
316                            if txn_buffer.overflow {
317                                tracing::warn!(
318                                    "txn_id={} large transaction tries to rollback, but part of its data has already been sent to the downstream.",
319                                    txn_id
320                                );
321                            }
322                        }
323                        TxnMsg::Data(txn_id, chunk) => {
324                            match active_txn_map.get_mut(&txn_id) {
325                                Some(txn_buffer) => {
326                                    // This transaction is too large, we can't provide atomicity,
327                                    // so yield chunk ASAP.
328                                    if txn_buffer.overflow {
329                                        yield Message::Chunk(chunk);
330                                        continue;
331                                    }
332                                    txn_buffer.vec.push(chunk);
333                                    if txn_buffer.vec.len() > MAX_CHUNK_FOR_ATOMICITY {
334                                        // Too many chunks for atomicity. Drain and yield them.
335                                        tracing::warn!(
336                                            "txn_id={} Too many chunks for atomicity. Sent them to the downstream anyway.",
337                                            txn_id
338                                        );
339                                        for chunk in txn_buffer.vec.drain(..) {
340                                            yield Message::Chunk(chunk);
341                                        }
342                                        txn_buffer.overflow = true;
343                                    }
344                                }
345                                None => panic!(
346                                    "Receive an unexpected transaction data message. Active transaction map doesn't contain this transaction txn_id = {}.",
347                                    txn_id
348                                ),
349                            };
350                        }
351                    }
352                }
353            }
354        }
355    }
356}
357
358impl<S: StateStore> Execute for DmlExecutor<S> {
359    fn execute(self: Box<Self>) -> BoxedMessageStream {
360        self.execute_inner().boxed()
361    }
362}
363
364/// Poll `stream` for the next input message while concurrently driving `persistence_futures`.
365/// Any error from a completed persistence future is propagated immediately, causing the executor
366/// (and thus the actor) to fail and trigger recovery.
367async fn next_input_driving_persistence(
368    stream: &mut StreamReaderWithPause<false, TxnMsg>,
369    persistence_futures: &mut FuturesOrdered<impl Future<Output = StreamExecutorResult<()>>>,
370) -> StreamExecutorResult<Option<Either<Message, TxnMsg>>> {
371    loop {
372        if persistence_futures.is_empty() {
373            return stream.next().await.transpose();
374        }
375
376        match select(persistence_futures.next(), stream.next()).await {
377            FutureEither::Left((Some(Ok(())), _)) => continue,
378            FutureEither::Left((Some(Err(err)), _)) => return Err(err),
379            FutureEither::Left((None, _)) => {
380                unreachable!("persistence_futures is known to be non-empty")
381            }
382            FutureEither::Right((stream_item, _)) => return stream_item.transpose(),
383        }
384    }
385}
386
387type BoxTxnMessageStream = BoxStream<'static, risingwave_dml::error::Result<TxnMsg>>;
388#[try_stream(ok = TxnMsg, error = risingwave_dml::error::DmlError)]
389async fn apply_dml_rate_limit(
390    stream: BoxTxnMessageStream,
391    rate_limiter: Arc<MonitoredRateLimiter>,
392) {
393    #[for_await]
394    for txn_msg in stream {
395        match txn_msg? {
396            TxnMsg::Begin(txn_id) => {
397                yield TxnMsg::Begin(txn_id);
398            }
399            TxnMsg::End(txn_id, persistence_notifier) => {
400                yield TxnMsg::End(txn_id, persistence_notifier);
401            }
402            TxnMsg::Rollback(txn_id) => {
403                yield TxnMsg::Rollback(txn_id);
404            }
405            TxnMsg::Data(txn_id, chunk) =>
406            {
407                #[for_await]
408                for chunk in rate_limited_pieces(&rate_limiter, chunk) {
409                    yield TxnMsg::Data(txn_id, chunk);
410                }
411            }
412        }
413    }
414}
415
416#[cfg(test)]
417mod tests {
418    use std::sync::{Arc, Mutex};
419
420    use futures::FutureExt;
421    use risingwave_common::catalog::{ColumnId, Field, INITIAL_TABLE_VERSION_ID};
422    use risingwave_common::test_prelude::StreamChunkTestExt;
423    use risingwave_common::util::epoch::test_epoch;
424    use risingwave_dml::dml_manager::DmlManager;
425    use risingwave_hummock_sdk::key::TableKeyRange;
426    use risingwave_storage::error::StorageResult;
427    use risingwave_storage::memory::MemoryStateStore;
428    use risingwave_storage::panic_store::{PanicStateStore, PanicStateStoreIter};
429    use risingwave_storage::store::*;
430
431    use super::*;
432    use crate::executor::test_utils::MockSource;
433
434    const TEST_TRANSACTION_ID: TxnId = 0;
435    const TEST_SESSION_ID: u32 = 0;
436
437    type WaitEpochCallSender = oneshot::Sender<(HummockReadEpoch, TryWaitEpochOptions)>;
438
439    #[derive(Clone)]
440    struct MockWaitEpochStateStore {
441        wait_epoch_called_tx: Arc<Mutex<Option<WaitEpochCallSender>>>,
442        wait_epoch_release_rx: Arc<tokio::sync::Mutex<Option<oneshot::Receiver<()>>>>,
443    }
444
445    impl StateStoreReadLog for MockWaitEpochStateStore {
446        type ChangeLogIter = PanicStateStoreIter<StateStoreReadLogItem>;
447
448        async fn next_epoch(&self, _epoch: u64, _options: NextEpochOptions) -> StorageResult<u64> {
449            panic!("should not read changelog from MockWaitEpochStateStore")
450        }
451
452        async fn iter_log(
453            &self,
454            _epoch_range: (u64, u64),
455            _key_range: TableKeyRange,
456            _options: ReadLogOptions,
457        ) -> StorageResult<Self::ChangeLogIter> {
458            panic!("should not read changelog from MockWaitEpochStateStore")
459        }
460    }
461
462    impl StateStore for MockWaitEpochStateStore {
463        type Local = PanicStateStore;
464        type ReadSnapshot = PanicStateStore;
465        type VectorWriter = PanicStateStore;
466
467        async fn try_wait_epoch(
468            &self,
469            epoch: HummockReadEpoch,
470            options: TryWaitEpochOptions,
471        ) -> StorageResult<()> {
472            if let Some(tx) = self.wait_epoch_called_tx.lock().unwrap().take() {
473                assert!(tx.send((epoch, options)).is_ok());
474            }
475            let rx = self.wait_epoch_release_rx.lock().await.take().unwrap();
476            rx.await.unwrap();
477            Ok(())
478        }
479
480        async fn new_local(&self, _option: NewLocalOptions) -> Self::Local {
481            panic!("should not create local state from MockWaitEpochStateStore")
482        }
483
484        async fn new_read_snapshot(
485            &self,
486            _epoch: HummockReadEpoch,
487            _options: NewReadSnapshotOptions,
488        ) -> StorageResult<Self::ReadSnapshot> {
489            panic!("should not read snapshot from MockWaitEpochStateStore")
490        }
491
492        async fn new_vector_writer(&self, _options: NewVectorWriterOptions) -> Self::VectorWriter {
493            panic!("should not create vector writer from MockWaitEpochStateStore")
494        }
495    }
496
497    #[tokio::test]
498    async fn test_dml_executor() {
499        let table_id = TableId::default();
500        let schema = Schema::new(vec![
501            Field::unnamed(DataType::Int64),
502            Field::unnamed(DataType::Int64),
503        ]);
504        let column_descs = vec![
505            ColumnDesc::unnamed(ColumnId::new(0), DataType::Int64),
506            ColumnDesc::unnamed(ColumnId::new(1), DataType::Int64),
507        ];
508        let stream_key = vec![0];
509        let dml_manager = Arc::new(DmlManager::for_test());
510
511        let (mut tx, source) = MockSource::channel();
512        let source = source.into_executor(schema, stream_key);
513
514        let dml_executor = DmlExecutor::new(
515            ActorContext::for_test(0),
516            source,
517            dml_manager.clone(),
518            table_id,
519            INITIAL_TABLE_VERSION_ID,
520            column_descs,
521            1024,
522            RateLimit::Disabled,
523            MemoryStateStore::new(),
524        );
525        let mut dml_executor = dml_executor.boxed().execute();
526
527        let stream_chunk1 = StreamChunk::from_pretty(
528            " I I
529            + 1 1
530            + 2 2
531            + 3 6",
532        );
533        let stream_chunk2 = StreamChunk::from_pretty(
534            " I I
535            + 88 43",
536        );
537        let stream_chunk3 = StreamChunk::from_pretty(
538            " I I
539            + 199 40
540            + 978 72
541            + 134 41
542            + 398 98",
543        );
544        let batch_chunk = StreamChunk::from_pretty(
545            "  I I
546            U+ 1 11
547            U+ 2 22",
548        );
549
550        // The first barrier
551        tx.push_barrier(test_epoch(1), false);
552        let msg = dml_executor.next().await.unwrap().unwrap();
553        assert!(matches!(msg, Message::Barrier(_)));
554
555        // Messages from upstream streaming executor
556        tx.push_chunk(stream_chunk1);
557        tx.push_chunk(stream_chunk2);
558        tx.push_chunk(stream_chunk3);
559
560        let table_dml_handle = dml_manager
561            .table_dml_handle(table_id, INITIAL_TABLE_VERSION_ID)
562            .unwrap();
563        let mut write_handle = table_dml_handle
564            .write_handle(TEST_SESSION_ID, TEST_TRANSACTION_ID)
565            .unwrap();
566
567        // Message from batch
568        write_handle.begin().unwrap();
569        write_handle.write_chunk(batch_chunk).await.unwrap();
570        // Since the end will wait the notifier which is sent by the reader,
571        // we need to spawn a task here to avoid dead lock.
572        tokio::spawn(async move {
573            write_handle.end().await.unwrap();
574            // a barrier to trigger batch group flush
575            tx.push_barrier(test_epoch(2), false);
576        });
577
578        // Consume the 1st message from upstream executor
579        let msg = dml_executor.next().await.unwrap().unwrap();
580        assert_eq!(
581            msg.into_chunk().unwrap(),
582            StreamChunk::from_pretty(
583                " I I
584                + 1 1
585                + 2 2
586                + 3 6",
587            )
588        );
589
590        // Consume the message from batch (because dml executor selects from the streams in a round
591        // robin way)
592
593        // TxnMsg::Begin is consumed implicitly
594
595        // Consume the 2nd message from upstream executor
596        let msg = dml_executor.next().await.unwrap().unwrap();
597        assert_eq!(
598            msg.into_chunk().unwrap(),
599            StreamChunk::from_pretty(
600                " I I
601                + 88 43",
602            )
603        );
604
605        // TxnMsg::Data is buffed
606
607        // Consume the 3rd message from upstream executor
608        let msg = dml_executor.next().await.unwrap().unwrap();
609        assert_eq!(
610            msg.into_chunk().unwrap(),
611            StreamChunk::from_pretty(
612                " I I
613                + 199 40
614                + 978 72
615                + 134 41
616                + 398 98",
617            )
618        );
619
620        // After TxnMsg::End, we can consume dml data
621        let msg = dml_executor.next().await.unwrap().unwrap();
622        assert_eq!(
623            msg.into_chunk().unwrap(),
624            StreamChunk::from_pretty(
625                "  I I
626                U+ 1 11
627                U+ 2 22",
628            )
629        );
630
631        let msg = dml_executor.next().await.unwrap().unwrap();
632        assert!(matches!(msg, Message::Barrier(_)));
633    }
634
635    #[tokio::test]
636    async fn test_dml_executor_waits_for_barrier_prev_epoch_persistence() {
637        let table_id = TableId::new(233);
638        let schema = Schema::new(vec![Field::unnamed(DataType::Int64)]);
639        let column_descs = vec![ColumnDesc::unnamed(ColumnId::new(0), DataType::Int64)];
640        let stream_key = vec![0];
641        let dml_manager = Arc::new(DmlManager::for_test());
642        let (wait_epoch_called_tx, wait_epoch_called_rx) = oneshot::channel();
643        let (wait_epoch_release_tx, wait_epoch_release_rx) = oneshot::channel();
644
645        let (mut tx, source) = MockSource::channel();
646        let source = source.into_executor(schema, stream_key);
647        let dml_executor = DmlExecutor::new(
648            ActorContext::for_test(0),
649            source,
650            dml_manager.clone(),
651            table_id,
652            INITIAL_TABLE_VERSION_ID,
653            column_descs,
654            1024,
655            RateLimit::Disabled,
656            MockWaitEpochStateStore {
657                wait_epoch_called_tx: Arc::new(Mutex::new(Some(wait_epoch_called_tx))),
658                wait_epoch_release_rx: Arc::new(tokio::sync::Mutex::new(Some(
659                    wait_epoch_release_rx,
660                ))),
661            },
662        );
663        let mut dml_executor = dml_executor.boxed().execute();
664
665        tx.push_barrier_with_prev_epoch_for_test(test_epoch(10), test_epoch(9), false);
666        let msg = dml_executor.next().await.unwrap().unwrap();
667        assert!(matches!(msg, Message::Barrier(_)));
668
669        let table_dml_handle = dml_manager
670            .table_dml_handle(table_id, INITIAL_TABLE_VERSION_ID)
671            .unwrap();
672        let mut write_handle = table_dml_handle
673            .write_handle(TEST_SESSION_ID, TEST_TRANSACTION_ID)
674            .unwrap();
675        write_handle.begin().unwrap();
676        write_handle
677            .write_chunk(StreamChunk::from_pretty(
678                " I
679                + 7",
680            ))
681            .await
682            .unwrap();
683
684        let mut persistence_future = Box::pin(write_handle.end_wait_persistence().unwrap());
685
686        // Drive the executor until all queued DML messages are consumed. The transaction is smaller
687        // than `chunk_size`, so it is buffered and no output is produced before the next barrier.
688        assert!(dml_executor.next().now_or_never().is_none());
689
690        let drain_handle = tokio::spawn(async move {
691            while let Some(msg) = dml_executor.next().await {
692                let _ = msg.unwrap();
693            }
694        });
695
696        tx.push_barrier_with_prev_epoch_for_test(test_epoch(11), test_epoch(10), false);
697
698        let (wait_epoch, options) = wait_epoch_called_rx.await.unwrap();
699        assert!(matches!(
700            wait_epoch,
701            HummockReadEpoch::Committed(epoch) if epoch == test_epoch(10)
702        ));
703        assert_eq!(options.table_id, table_id);
704        assert!(persistence_future.as_mut().now_or_never().is_none());
705
706        wait_epoch_release_tx.send(()).unwrap();
707        persistence_future.await.unwrap();
708
709        drain_handle.abort();
710    }
711}