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