Skip to main content

risingwave_stream/executor/dispatch/
dispatch_sync_log_store.rs

1// Copyright 2026 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::VecDeque;
16use std::future::{Future, pending};
17use std::pin::Pin;
18use std::time::Duration;
19
20use anyhow::anyhow;
21use futures::future::{Either, select};
22use pin_project::pin_project;
23use risingwave_common::bitmap::Bitmap;
24use risingwave_common::must_match;
25use risingwave_pb::stream_plan;
26use risingwave_storage::StateStore;
27use risingwave_storage::store::StateStoreRead;
28use rw_futures_util::drop_either_future;
29use tokio::sync::mpsc::UnboundedReceiver;
30
31use super::{DispatchExecutor, DispatchExecutorInner, dispatch_message_batch};
32use crate::common::log_store_impl::kv_log_store::reader::LogStoreReadStateStreamRangeStart;
33use crate::common::log_store_impl::kv_log_store::serde::LogStoreRowSerde;
34use crate::common::log_store_impl::kv_log_store::state::LogStoreReadState;
35use crate::common::log_store_impl::kv_log_store::{
36    FIRST_SEQ_ID, KV_LOG_STORE_V2_INFO, LogStoreVnodeProgress,
37};
38use crate::executor::prelude::*;
39use crate::executor::sync_kv_log_store::{
40    ReadFuture, SyncKvLogStoreContext, SyncedKvLogStoreExecutor, SyncedLogStoreBuffer, WriteFuture,
41    WriteFutureEvent,
42};
43use crate::executor::{MessageBatch, StreamConsumer, SyncedKvLogStoreMetrics};
44use crate::task::NewOutputRequest;
45
46/// Executor that pairs a synced KV log store with a dispatcher so the log store can
47/// advance and write independently of downstream backpressure.
48///
49/// This keeps upstream executors being polled even when downstream is slow or blocked, decoupling
50/// log store progress from downstream polling.
51pub struct SyncLogStoreDispatchExecutor<S: StateStore> {
52    pub(super) input: Executor,
53    pub(super) inner: DispatchExecutorInner,
54    pub(super) log_store_context: SyncKvLogStoreContext<S>,
55}
56
57impl<S: StateStore> SyncLogStoreDispatchExecutor<S> {
58    pub(crate) async fn new(
59        input: Executor,
60        new_output_request_rx: UnboundedReceiver<(ActorId, NewOutputRequest)>,
61        dispatchers: Vec<stream_plan::Dispatcher>,
62        actor_context: &ActorContextRef,
63        sync: &stream_plan::SyncLogStoreNode,
64        vnode_bitmap: Option<Bitmap>,
65        state_store: S,
66    ) -> StreamResult<Self> {
67        let chunk_size = actor_context.config.developer.chunk_size;
68        let fragment_id = actor_context.fragment_id;
69        let log_store_metrics = SyncedKvLogStoreMetrics::new(
70            &actor_context.streaming_metrics,
71            actor_context.id,
72            fragment_id,
73            "sync_log_store_dispatch",
74            "sync_log_store_dispatch",
75        );
76
77        let table = sync
78            .log_store_table
79            .as_ref()
80            .ok_or_else(|| anyhow!("missing log_store_table in SyncLogStoreNode"))?;
81
82        let pause_duration_ms = actor_context
83            .config
84            .developer
85            .sync_log_store_pause_duration_ms;
86        let max_buffer_size = actor_context.config.developer.sync_log_store_buffer_size;
87
88        let serde =
89            LogStoreRowSerde::new(table, vnode_bitmap.map(Into::into), &KV_LOG_STORE_V2_INFO);
90        let log_store_context = SyncKvLogStoreContext {
91            table_id: table.id,
92            fragment_id,
93            serde,
94            state_store,
95            max_buffer_size,
96            pause_duration_ms: Duration::from_millis(pause_duration_ms as _),
97            aligned: sync.aligned,
98            chunk_size,
99            metrics: log_store_metrics,
100        };
101
102        Self::new_with_log_store_context(
103            input,
104            new_output_request_rx,
105            dispatchers,
106            actor_context,
107            log_store_context,
108        )
109        .await
110    }
111
112    async fn new_with_log_store_context(
113        input: Executor,
114        new_output_request_rx: UnboundedReceiver<(ActorId, NewOutputRequest)>,
115        dispatchers: Vec<stream_plan::Dispatcher>,
116        actor_context: &ActorContextRef,
117        log_store_context: SyncKvLogStoreContext<S>,
118    ) -> StreamResult<Self> {
119        let DispatchExecutor { input, inner } =
120            DispatchExecutor::new(input, new_output_request_rx, dispatchers, actor_context).await?;
121
122        tracing::info!(
123            actor_id = %actor_context.id,
124            "synclogstore dispatch executor info"
125        );
126
127        Ok(Self {
128            input,
129            inner,
130            log_store_context,
131        })
132    }
133}
134
135type DispatchingFuture =
136    impl Future<Output = (DispatchExecutorInner, StreamResult<Option<Barrier>>)> + 'static;
137
138#[define_opaque(DispatchingFuture)]
139fn dispatching_future(mut inner: DispatchExecutorInner, message: Message) -> DispatchingFuture {
140    async move {
141        let batch: MessageBatch = message.into();
142        let r = dispatch_message_batch(&mut inner, batch)
143            .await
144            .map(|barrier_batch| {
145                barrier_batch.map(|mut barrier_batch| {
146                    debug_assert_eq!(barrier_batch.len(), 1);
147                    barrier_batch
148                        .pop()
149                        .expect("barrier batch should contain one barrier")
150                })
151            });
152        (inner, r)
153    }
154}
155
156/// State machine for the consumer side, which reads chunks from the log store and dispatches
157/// chunks or barriers downstream.
158#[pin_project(project = ConsumerFutureProj, project_replace = ConsumerFutureProjReplace)]
159enum ConsumerFuture {
160    /// Polls the log store for the next chunk. The read future is kept outside this state machine
161    /// so both meaningful states can share it.
162    ReadingChunk { inner: DispatchExecutorInner },
163    /// Dispatches the current message downstream. Any barrier received while dispatching is queued
164    /// here and dispatched before reading another chunk.
165    Dispatching {
166        #[pin]
167        future: DispatchingFuture,
168        barrier_queue: VecDeque<Message>,
169    },
170    /// Temporary placeholder used while moving fields out during state transitions.
171    PlaceHolder,
172}
173
174enum ConsumerFutureEvent {
175    BarrierDispatched(Barrier),
176    CleanStateReached,
177}
178
179impl ConsumerFuture {
180    fn dispatch(inner: DispatchExecutorInner, message: Message) -> Self {
181        tracing::trace!("consumer_future: dispatching future created");
182        Self::Dispatching {
183            future: dispatching_future(inner, message),
184            barrier_queue: VecDeque::new(),
185        }
186    }
187
188    fn read_chunk(inner: DispatchExecutorInner) -> Self {
189        tracing::trace!("consumer_future: reading chunk future created");
190        Self::ReadingChunk { inner }
191    }
192
193    fn push_barrier(mut self: Pin<&mut Self>, barrier: Barrier) {
194        let message = Message::Barrier(barrier);
195        match self.as_mut().project() {
196            ConsumerFutureProj::ReadingChunk { .. } => {
197                let inner = must_match!(
198                    self.as_mut().project_replace(ConsumerFuture::PlaceHolder),
199                    ConsumerFutureProjReplace::ReadingChunk { inner } => inner
200                );
201                self.set(Self::dispatch(inner, message));
202            }
203            ConsumerFutureProj::Dispatching { barrier_queue, .. } => {
204                barrier_queue.push_front(message);
205            }
206            ConsumerFutureProj::PlaceHolder => {
207                unreachable!("ConsumerFuture::PlaceHolder should be handled!")
208            }
209        }
210    }
211
212    #[expect(clippy::too_many_arguments)]
213    async fn next_event<S: StateStoreRead>(
214        mut self: Pin<&mut Self>,
215        read_future: &mut ReadFuture<S>,
216        read_paused: bool,
217        clean_state: &mut bool,
218        progress: &mut LogStoreVnodeProgress,
219        read_state: &LogStoreReadState<S>,
220        buffer: &mut SyncedLogStoreBuffer,
221        metrics: &SyncedKvLogStoreMetrics,
222    ) -> StreamResult<ConsumerFutureEvent> {
223        loop {
224            match self.as_mut().project() {
225                ConsumerFutureProj::ReadingChunk { .. } => {
226                    if read_paused {
227                        pending().await
228                    }
229
230                    let message = read_future
231                        .next_message(progress, read_state, buffer, metrics)
232                        .await?;
233                    if let Message::Chunk(chunk) = &message {
234                        metrics.total_read_count.inc_by(chunk.cardinality() as _);
235                    }
236
237                    let clean_state_reached =
238                        read_future.mark_clean_state(clean_state, buffer, metrics);
239                    let inner = must_match!(
240                        self.as_mut().project_replace(ConsumerFuture::PlaceHolder),
241                        ConsumerFutureProjReplace::ReadingChunk { inner } => inner
242                    );
243                    self.set(Self::dispatch(inner, message));
244
245                    if clean_state_reached {
246                        return Ok(ConsumerFutureEvent::CleanStateReached);
247                    }
248                    continue;
249                }
250                ConsumerFutureProj::Dispatching {
251                    future,
252                    barrier_queue,
253                } => {
254                    let (inner, result) = future.await;
255                    let barrier = result?;
256
257                    if let Some(next_barrier) = barrier_queue.pop_back() {
258                        tracing::trace!("consumer_future: dispatching future created");
259                        let ConsumerFutureProj::Dispatching { mut future, .. } =
260                            self.as_mut().project()
261                        else {
262                            unreachable!("ConsumerFuture::ReadingChunk should be handled!")
263                        };
264                        future.set(dispatching_future(inner, next_barrier));
265                    } else {
266                        self.set(Self::read_chunk(inner));
267                    }
268
269                    if let Some(barrier) = barrier {
270                        return Ok(ConsumerFutureEvent::BarrierDispatched(barrier));
271                    }
272                }
273                ConsumerFutureProj::PlaceHolder => {
274                    unreachable!("ConsumerFuture::PlaceHolder should be handled!")
275                }
276            }
277        }
278    }
279}
280
281impl<S: StateStore> StreamConsumer for SyncLogStoreDispatchExecutor<S> {
282    type BarrierStream = impl Stream<Item = StreamResult<Barrier>> + Send;
283
284    fn execute(mut self: Box<Self>) -> Self::BarrierStream {
285        #[try_stream]
286        async move {
287            let actor_id = self.inner.actor_id;
288            let log_store_config = self.log_store_context;
289
290            let mut input = self.input.execute();
291
292            let first_barrier = expect_first_barrier(&mut input).await?;
293            let first_write_epoch = first_barrier.epoch;
294
295            // Dispatch the first barrier before initializing the log store states
296            let first_barrier_batch = dispatch_message_batch(
297                &mut self.inner,
298                Message::Barrier(first_barrier.clone()).into(),
299            )
300            .await?;
301            debug_assert_eq!(
302                first_barrier_batch
303                    .as_ref()
304                    .map(|barrier_batch| barrier_batch.len()),
305                Some(1)
306            );
307            yield first_barrier.clone();
308
309            let (read_state, initial_write_state) =
310                SyncedKvLogStoreExecutor::<S>::init_local_log_store_state(
311                    &log_store_config,
312                    first_write_epoch,
313                )
314                .await?;
315
316            let initial_write_epoch = first_write_epoch;
317            let mut pause_stream = first_barrier.is_pause_on_startup();
318
319            if log_store_config.aligned {
320                let aligned_stream = SyncedKvLogStoreExecutor::<S>::aligned_message_stream(
321                    actor_id,
322                    input,
323                    read_state,
324                    initial_write_state,
325                    log_store_config.metrics.clone(),
326                    initial_write_epoch,
327                );
328
329                #[for_await]
330                for message in aligned_stream {
331                    if let Some(barrier_batch) =
332                        dispatch_message_batch(&mut self.inner, message?.into()).await?
333                    {
334                        // Now Synclogstoredispatchexecutor only support sending out single barrier
335                        debug_assert_eq!(barrier_batch.len(), 1);
336                        for barrier in barrier_batch {
337                            yield barrier;
338                        }
339                    }
340                }
341                return Ok(());
342            }
343
344            let mut seq_id = FIRST_SEQ_ID;
345            let mut buffer = SyncedLogStoreBuffer::new(
346                log_store_config.max_buffer_size,
347                log_store_config.chunk_size,
348                &log_store_config.metrics,
349            );
350
351            let log_store_stream = read_state
352                .read_persisted_log_store(
353                    log_store_config.metrics.persistent_log_read_metrics.clone(),
354                    initial_write_epoch.curr,
355                    LogStoreReadStateStreamRangeStart::Unbounded,
356                )
357                .await?;
358
359            let mut log_store_stream = tokio_stream::StreamExt::peekable(log_store_stream);
360            let mut clean_state = log_store_stream.peek().await.is_none();
361            tracing::trace!(?clean_state);
362
363            let mut progress = LogStoreVnodeProgress::None;
364            let mut read_future_state = ReadFuture::ReadingPersistedStream(log_store_stream);
365            let consumer_future_state = ConsumerFuture::ReadingChunk { inner: self.inner };
366            pin_mut!(consumer_future_state);
367
368            let mut write_future_state =
369                WriteFuture::receive_from_upstream(input, initial_write_state);
370            let mut end_of_stream = false;
371
372            loop {
373                let select_result = {
374                    let consumer_future = async {
375                        consumer_future_state
376                            .as_mut()
377                            .next_event(
378                                &mut read_future_state,
379                                pause_stream,
380                                &mut clean_state,
381                                &mut progress,
382                                &read_state,
383                                &mut buffer,
384                                &log_store_config.metrics,
385                            )
386                            .await
387                    };
388                    pin_mut!(consumer_future);
389                    let write_future = async {
390                        if end_of_stream {
391                            pending().await
392                        } else {
393                            write_future_state
394                                .next_event(&log_store_config.metrics)
395                                .await
396                        }
397                    };
398                    pin_mut!(write_future);
399                    let output = select(write_future, consumer_future).await;
400                    drop_either_future(output)
401                };
402
403                match select_result {
404                    Either::Left(_write_result) => {
405                        drop(write_future_state);
406                        let (stream, mut write_state, either) = _write_result?;
407                        match either {
408                            WriteFutureEvent::UpstreamMessageReceived(msg) => match msg {
409                                Message::Chunk(chunk) => {
410                                    let (new_seq_id, next_write_future) =
411                                        SyncedKvLogStoreExecutor::<S>::process_upstream_chunk(
412                                            seq_id,
413                                            stream,
414                                            write_state,
415                                            chunk,
416                                            &mut buffer,
417                                        );
418                                    seq_id = new_seq_id;
419                                    write_future_state = next_write_future;
420                                }
421                                Message::Barrier(barrier) => {
422                                    if clean_state
423                                        && barrier.kind.is_checkpoint()
424                                        && !buffer.is_empty()
425                                    {
426                                        write_future_state = WriteFuture::paused(
427                                            log_store_config.pause_duration_ms,
428                                            barrier,
429                                            stream,
430                                            write_state,
431                                        );
432                                        clean_state = false;
433                                        log_store_config.metrics.unclean_state.inc();
434                                    } else {
435                                        SyncedKvLogStoreExecutor::<S>::apply_pause_resume_mutation(
436                                            &barrier,
437                                            &mut pause_stream,
438                                        );
439                                        let write_state_post_write_barrier =
440                                            SyncedKvLogStoreExecutor::<S>::write_barrier(
441                                                actor_id,
442                                                &mut write_state,
443                                                barrier.clone(),
444                                                &log_store_config.metrics,
445                                                progress.take(),
446                                                &mut buffer,
447                                            )
448                                            .await?;
449                                        seq_id = FIRST_SEQ_ID;
450                                        barrier.assume_no_update_vnode_bitmap(actor_id)?;
451
452                                        write_state_post_write_barrier
453                                            .post_yield_barrier(None)
454                                            .await?;
455
456                                        let is_stop_barrier = barrier.is_stop(actor_id);
457                                        if is_stop_barrier {
458                                            // Stop polling upstream after the stop barrier is
459                                            // written into the log store.
460                                            end_of_stream = true;
461                                            write_future_state = WriteFuture::Empty;
462                                        } else {
463                                            write_future_state = WriteFuture::receive_from_upstream(
464                                                stream,
465                                                write_state,
466                                            );
467                                        }
468                                        consumer_future_state.as_mut().push_barrier(barrier);
469                                    }
470                                }
471                                Message::Watermark(watermark) => {
472                                    buffer.add_watermark(write_state.epoch().curr, watermark);
473                                    write_future_state =
474                                        WriteFuture::receive_from_upstream(stream, write_state);
475                                }
476                            },
477                            WriteFutureEvent::ChunkFlushed(info) => {
478                                write_future_state =
479                                    SyncedKvLogStoreExecutor::<S>::process_flushed_chunk(
480                                        stream,
481                                        write_state,
482                                        info,
483                                        &mut buffer,
484                                        &log_store_config.metrics,
485                                    );
486                            }
487                        }
488                    }
489                    Either::Right(consumer_result) => {
490                        let event = consumer_result?;
491                        match event {
492                            ConsumerFutureEvent::CleanStateReached => {
493                                if let WriteFuture::Paused { sleep_future, .. } =
494                                    &mut write_future_state
495                                {
496                                    assert!(buffer.has_available_capacity());
497                                    *sleep_future = None;
498                                }
499                            }
500                            ConsumerFutureEvent::BarrierDispatched(barrier) => {
501                                yield barrier;
502                            }
503                        }
504                    }
505                }
506            }
507        }
508    }
509}
510
511#[cfg(test)]
512mod tests {
513    use std::sync::Arc;
514
515    use futures::StreamExt;
516    use risingwave_common::array::{StreamChunk, StreamChunkTestExt};
517    use risingwave_common::bitmap::Bitmap;
518    use risingwave_common::catalog::{Field, Schema};
519    use risingwave_common::hash::VirtualNode;
520    use risingwave_common::types::DataType;
521    use risingwave_common::util::epoch::test_epoch;
522    use risingwave_pb::stream_plan::{DispatcherType, PbDispatchOutputMapping};
523    use risingwave_storage::memory::MemoryStateStore;
524    use tokio::sync::mpsc::unbounded_channel;
525    use tokio::time::{Duration, timeout};
526
527    use super::*;
528    use crate::assert_stream_chunk_eq;
529    use crate::common::log_store_impl::kv_log_store::KV_LOG_STORE_V2_INFO;
530    use crate::common::log_store_impl::kv_log_store::serde::LogStoreRowSerde;
531    use crate::common::log_store_impl::kv_log_store::test_utils::{
532        check_stream_chunk_eq, gen_test_log_store_table,
533    };
534    use crate::executor::exchange::permit::channel_for_test;
535    use crate::executor::test_utils::MockSource;
536    use crate::executor::{ActorContext, BarrierInner as Barrier};
537    use crate::task::ActorId;
538
539    const ACTOR_ID: u32 = 4242;
540    const DOWNSTREAM_ACTOR_ID: u32 = 5252;
541
542    fn init_logger() {
543        let _ = tracing_subscriber::fmt()
544            .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
545            .with_ansi(false)
546            .try_init();
547    }
548
549    async fn run_barrier_chunk_ordering_test(aligned: bool) {
550        init_logger();
551
552        let actor_id = ActorId::new(ACTOR_ID);
553        let downstream_actor = ActorId::new(DOWNSTREAM_ACTOR_ID);
554        let (new_output_request_tx, new_output_request_rx) = unbounded_channel();
555        let (down_tx, mut down_rx) = channel_for_test();
556        new_output_request_tx
557            .send((downstream_actor, NewOutputRequest::Local(down_tx)))
558            .unwrap();
559
560        let barrier1 = Barrier::new_test_barrier(test_epoch(1));
561        let barrier2 = Barrier::new_test_barrier(test_epoch(2));
562        let chunk_1 = StreamChunk::from_pretty(
563            "  I   T
564            +  5  10
565            +  6  10
566            +  8  10
567            +  9  10
568            + 10  11",
569        );
570        let chunk_2 = StreamChunk::from_pretty(
571            "  I   T
572            -  5  10
573            -  6  10
574            -  8  10
575            U- 10  11
576            U+ 10  10",
577        );
578        let dispatcher = stream_plan::Dispatcher {
579            r#type: DispatcherType::Simple as _,
580            dispatcher_id: 7.into(),
581            downstream_actor_id: vec![DOWNSTREAM_ACTOR_ID.into()],
582            output_mapping: PbDispatchOutputMapping::identical(2).into(),
583            ..Default::default()
584        };
585        let pk_info = &KV_LOG_STORE_V2_INFO;
586        let table = gen_test_log_store_table(pk_info);
587        let vnodes = Some(Arc::new(Bitmap::ones(VirtualNode::COUNT_FOR_TEST)));
588        let serde = LogStoreRowSerde::new(&table, vnodes, pk_info);
589        let log_store_config = SyncKvLogStoreContext {
590            table_id: table.id,
591            fragment_id: 0.into(),
592            serde,
593            state_store: MemoryStateStore::new(),
594            max_buffer_size: 1024,
595            pause_duration_ms: Duration::from_millis(10),
596            aligned,
597            chunk_size: 1024,
598            metrics: SyncedKvLogStoreMetrics::for_test(),
599        };
600        let (mut input_tx, source) = MockSource::channel();
601        let input = source.into_executor(
602            Schema {
603                fields: vec![
604                    Field::unnamed(DataType::Int64),
605                    Field::unnamed(DataType::Varchar),
606                ],
607            },
608            vec![0],
609        );
610
611        let executor = SyncLogStoreDispatchExecutor::new_with_log_store_context(
612            input,
613            new_output_request_rx,
614            vec![dispatcher],
615            &ActorContext::for_test(actor_id),
616            log_store_config,
617        )
618        .await
619        .unwrap();
620
621        let (barrier_out_tx, mut barrier_out_rx) = unbounded_channel();
622        let barrier_driver = tokio::spawn(async move {
623            let barrier_stream = Box::new(executor).execute();
624            futures::pin_mut!(barrier_stream);
625            while let Some(item) = barrier_stream.next().await {
626                barrier_out_tx.send(item).ok();
627            }
628        });
629
630        input_tx.send_barrier(barrier1.clone());
631        let observed1 = timeout(Duration::from_secs(1), barrier_out_rx.recv())
632            .await
633            .unwrap()
634            .unwrap()
635            .unwrap();
636        assert_eq!(observed1.epoch.curr, test_epoch(1));
637
638        let msg = timeout(Duration::from_secs(1), down_rx.recv())
639            .await
640            .unwrap()
641            .expect("downstream should receive barrier(1)");
642        let barriers = msg.as_barrier_batch().unwrap();
643        assert_eq!(barriers.len(), 1);
644        assert_eq!(barriers[0].epoch.curr, test_epoch(1));
645
646        input_tx.push_chunk(chunk_1.clone());
647        input_tx.push_int64_watermark(0, 7);
648        input_tx.push_chunk(chunk_2.clone());
649        let msg = timeout(Duration::from_secs(1), down_rx.recv())
650            .await
651            .unwrap()
652            .expect("downstream should receive chunk(1)");
653        assert_stream_chunk_eq!(msg.as_chunk().unwrap(), chunk_1);
654
655        let msg = timeout(Duration::from_secs(1), down_rx.recv())
656            .await
657            .unwrap()
658            .expect("downstream should receive watermark");
659        assert_eq!(
660            msg.as_watermark().unwrap(),
661            &Watermark::new(0, DataType::Int64, 7_i64.into())
662        );
663
664        let msg = timeout(Duration::from_secs(1), down_rx.recv())
665            .await
666            .unwrap()
667            .expect("downstream should receive chunk(2)");
668        assert_stream_chunk_eq!(msg.as_chunk().unwrap(), chunk_2);
669
670        input_tx.send_barrier(barrier2.clone());
671        let msg = timeout(Duration::from_secs(1), down_rx.recv())
672            .await
673            .unwrap()
674            .expect("downstream should receive barrier(2)");
675        let barriers = msg.as_barrier_batch().unwrap();
676        assert_eq!(barriers.len(), 1);
677        assert_eq!(barriers[0].epoch.curr, test_epoch(2));
678
679        let observed2 = timeout(Duration::from_secs(1), barrier_out_rx.recv())
680            .await
681            .unwrap()
682            .unwrap()
683            .unwrap();
684        assert_eq!(observed2.epoch.curr, test_epoch(2));
685
686        barrier_driver.abort();
687    }
688
689    /// Mirror `sync_kv_log_store::test_barrier_persisted_read`, but assert the dispatched output
690    /// order: chunk(1) -> chunk(2) -> barrier(2), while barrier(1) is surfaced via barrier stream.
691    #[tokio::test]
692    async fn test_barrier_chunk_ordering_in_dispatch() {
693        run_barrier_chunk_ordering_test(false).await;
694    }
695
696    #[tokio::test]
697    async fn test_aligned_barrier_chunk_ordering_in_dispatch() {
698        run_barrier_chunk_ordering_test(true).await;
699    }
700}