Skip to main content

risingwave_stream/executor/
locality_provider.rs

1// Copyright 2025 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::HashMap;
16use std::sync::Arc;
17
18use futures::future::{Either as FutureEither, select};
19use futures::{StreamExt, TryStreamExt, pin_mut};
20use futures_async_stream::try_stream;
21use itertools::Itertools;
22use risingwave_common::array::{DataChunk, Op, StreamChunk};
23use risingwave_common::catalog::Schema;
24use risingwave_common::hash::{VirtualNode, VnodeBitmapExt};
25use risingwave_common::row::{OwnedRow, Row, RowExt};
26use risingwave_common::types::{Datum, ToOwnedDatum};
27use risingwave_common::util::chunk_coalesce::DataChunkBuilder;
28use risingwave_common::util::sort_util::cmp_datum_iter;
29use risingwave_common_rate_limit::RateLimit;
30use risingwave_storage::StateStore;
31use risingwave_storage::store::PrefetchOptions;
32
33use crate::common::table::state_table::{FlushedStateTableReader, StateTable};
34use crate::executor::backfill::utils::create_builder;
35use crate::executor::prelude::*;
36use crate::task::{CreateMviewProgressReporter, FragmentId};
37
38type Builders = HashMap<VirtualNode, DataChunkBuilder>;
39
40/// Progress state for tracking backfill per vnode
41#[derive(Clone, Debug, PartialEq, Eq)]
42enum LocalityBackfillProgress {
43    /// Backfill not started for this vnode
44    NotStarted,
45    /// Backfill in progress, tracking current position
46    InProgress {
47        /// Current position in the locality-ordered scan
48        current_pos: OwnedRow,
49        /// Number of rows processed for this vnode
50        processed_rows: u64,
51    },
52    /// Backfill completed for this vnode
53    Completed {
54        /// Final position reached
55        final_pos: OwnedRow,
56        /// Total rows processed for this vnode
57        total_rows: u64,
58    },
59}
60
61/// State management for locality provider backfill process
62#[derive(Clone, Debug)]
63struct LocalityBackfillState {
64    /// Progress per vnode
65    per_vnode: HashMap<VirtualNode, LocalityBackfillProgress>,
66    /// Total snapshot rows read across all vnodes
67    total_snapshot_rows: u64,
68}
69
70impl LocalityBackfillState {
71    fn new(vnodes: impl Iterator<Item = VirtualNode>) -> Self {
72        let per_vnode = vnodes
73            .map(|vnode| (vnode, LocalityBackfillProgress::NotStarted))
74            .collect();
75        Self {
76            per_vnode,
77            total_snapshot_rows: 0,
78        }
79    }
80
81    fn is_completed(&self) -> bool {
82        self.per_vnode
83            .values()
84            .all(|progress| matches!(progress, LocalityBackfillProgress::Completed { .. }))
85    }
86
87    fn vnodes(&self) -> impl Iterator<Item = (VirtualNode, &LocalityBackfillProgress)> {
88        self.per_vnode
89            .iter()
90            .map(|(&vnode, progress)| (vnode, progress))
91    }
92
93    fn has_progress(&self) -> bool {
94        self.per_vnode
95            .values()
96            .any(|progress| matches!(progress, LocalityBackfillProgress::InProgress { .. }))
97    }
98
99    fn update_progress(&mut self, vnode: VirtualNode, new_pos: OwnedRow, row_count_delta: u64) {
100        let progress = self.per_vnode.get_mut(&vnode).unwrap();
101        match progress {
102            LocalityBackfillProgress::NotStarted => {
103                *progress = LocalityBackfillProgress::InProgress {
104                    current_pos: new_pos,
105                    processed_rows: row_count_delta,
106                };
107            }
108            LocalityBackfillProgress::InProgress { processed_rows, .. } => {
109                *progress = LocalityBackfillProgress::InProgress {
110                    current_pos: new_pos,
111                    processed_rows: *processed_rows + row_count_delta,
112                };
113            }
114            LocalityBackfillProgress::Completed { .. } => {
115                // Already completed, shouldn't update
116            }
117        }
118        self.total_snapshot_rows += row_count_delta;
119    }
120
121    fn finish_vnode(&mut self, vnode: VirtualNode, pk_len: usize) {
122        let progress = self.per_vnode.get_mut(&vnode).unwrap();
123        match progress {
124            LocalityBackfillProgress::NotStarted => {
125                // Create a final position with pk_len NULL values to indicate completion
126                let final_pos = OwnedRow::new(vec![None; pk_len]);
127                *progress = LocalityBackfillProgress::Completed {
128                    final_pos,
129                    total_rows: 0,
130                };
131            }
132            LocalityBackfillProgress::InProgress {
133                current_pos,
134                processed_rows,
135            } => {
136                *progress = LocalityBackfillProgress::Completed {
137                    final_pos: current_pos.clone(),
138                    total_rows: *processed_rows,
139                };
140            }
141            LocalityBackfillProgress::Completed { .. } => {
142                // Already completed
143            }
144        }
145    }
146
147    fn get_progress(&self, vnode: &VirtualNode) -> &LocalityBackfillProgress {
148        self.per_vnode.get(vnode).unwrap()
149    }
150}
151
152/// The `LocalityProviderExecutor` provides locality for operators during backfilling.
153/// It buffers input data into a state table using locality columns as primary key prefix.
154///
155/// The executor implements a proper backfill process similar to arrangement backfill:
156/// 1. Backfill phase: Buffer incoming data and provide locality-ordered snapshot reads
157/// 2. Forward phase: Once backfill is complete, forward upstream messages directly
158///
159/// Key improvements over the original implementation:
160/// - Removes arbitrary barrier buffer limit
161/// - Implements proper upstream chunk tracking during backfill
162/// - Uses per-vnode progress tracking for better state management
163pub struct LocalityProviderExecutor<S: StateStore> {
164    /// Upstream input
165    upstream: Executor,
166
167    /// Locality columns (indices in input schema)
168    #[expect(dead_code)]
169    locality_columns: Vec<usize>,
170
171    /// State table for buffering input data
172    state_table: StateTable<S>,
173
174    /// Progress table for tracking backfill progress per vnode
175    progress_table: StateTable<S>,
176
177    input_schema: Schema,
178
179    /// Progress reporter for materialized view creation
180    progress: CreateMviewProgressReporter,
181
182    fragment_id: FragmentId,
183
184    actor_id: ActorId,
185
186    /// Metrics
187    metrics: Arc<StreamingMetrics>,
188
189    /// Chunk size for output
190    chunk_size: usize,
191}
192
193impl<S: StateStore> LocalityProviderExecutor<S> {
194    #[expect(clippy::too_many_arguments)]
195    pub fn new(
196        upstream: Executor,
197        locality_columns: Vec<usize>,
198        state_table: StateTable<S>,
199        progress_table: StateTable<S>,
200        input_schema: Schema,
201        progress: CreateMviewProgressReporter,
202        metrics: Arc<StreamingMetrics>,
203        chunk_size: usize,
204        fragment_id: FragmentId,
205    ) -> Self {
206        Self {
207            upstream,
208            locality_columns,
209            state_table,
210            progress_table,
211            input_schema,
212            actor_id: progress.actor_id(),
213            progress,
214            metrics,
215            chunk_size,
216            fragment_id,
217        }
218    }
219
220    /// Creates a snapshot stream that reads from state table in locality order
221    #[try_stream(ok = (VirtualNode, OwnedRow), error = StreamExecutorError)]
222    async fn make_snapshot_stream(
223        reader: FlushedStateTableReader<S>,
224        backfill_state: LocalityBackfillState,
225    ) {
226        // Read from state table per vnode in locality order
227        for vnode in reader.vnodes().iter_vnodes() {
228            let progress = backfill_state.get_progress(&vnode);
229
230            let current_pos = match progress {
231                LocalityBackfillProgress::NotStarted => None,
232                LocalityBackfillProgress::Completed { .. } => {
233                    // Skip completed vnodes
234                    continue;
235                }
236                LocalityBackfillProgress::InProgress { current_pos, .. } => {
237                    Some(current_pos.clone())
238                }
239            };
240
241            // Compute range bounds for iteration based on current position
242            let range_bounds = if let Some(ref pos) = current_pos {
243                let start_bound = std::ops::Bound::Excluded(pos.as_inner());
244                (start_bound, std::ops::Bound::<&[Datum]>::Unbounded)
245            } else {
246                (
247                    std::ops::Bound::<&[Datum]>::Unbounded,
248                    std::ops::Bound::<&[Datum]>::Unbounded,
249                )
250            };
251
252            // Iterate over rows for this vnode
253            let iter = reader
254                .iter_with_vnode(
255                    vnode,
256                    &range_bounds,
257                    PrefetchOptions::prefetch_for_small_range_scan(),
258                )
259                .await?;
260            pin_mut!(iter);
261
262            while let Some(row) = iter.try_next().await? {
263                yield (vnode, row);
264            }
265        }
266    }
267
268    /// Persist backfill state to progress table
269    async fn persist_backfill_state(
270        progress_table: &mut StateTable<S>,
271        backfill_state: &LocalityBackfillState,
272    ) -> StreamExecutorResult<()> {
273        for (vnode, progress) in &backfill_state.per_vnode {
274            let (is_finished, current_pos, row_count) = match progress {
275                LocalityBackfillProgress::NotStarted => continue, // Don't persist NotStarted
276                LocalityBackfillProgress::InProgress {
277                    current_pos,
278                    processed_rows,
279                } => (false, current_pos.clone(), *processed_rows),
280                LocalityBackfillProgress::Completed {
281                    final_pos,
282                    total_rows,
283                } => (true, final_pos.clone(), *total_rows),
284            };
285
286            // Build progress row: vnode + current_pos + is_finished + row_count
287            let mut row_data = vec![Some(vnode.to_scalar().into())];
288            row_data.extend(current_pos);
289            row_data.push(Some(risingwave_common::types::ScalarImpl::Bool(
290                is_finished,
291            )));
292            row_data.push(Some(risingwave_common::types::ScalarImpl::Int64(
293                row_count as i64,
294            )));
295
296            let new_row = OwnedRow::new(row_data);
297
298            // Check if there's an existing row for this vnode to determine insert vs update
299            // This ensures state operation consistency - update existing rows, insert new ones
300            let key_data = vec![Some(vnode.to_scalar().into())];
301            let key = OwnedRow::new(key_data);
302
303            if let Some(existing_row) = progress_table.get_row(&key).await? {
304                // Update existing state - ensures proper state transition for recovery
305                progress_table.update(existing_row, new_row);
306            } else {
307                // Insert new state - first time persisting for this vnode
308                progress_table.insert(new_row);
309            }
310        }
311        Ok(())
312    }
313
314    /// Load backfill state from progress table
315    async fn load_backfill_state(
316        progress_table: &StateTable<S>,
317    ) -> StreamExecutorResult<LocalityBackfillState> {
318        let mut backfill_state = LocalityBackfillState::new(progress_table.vnodes().iter_vnodes());
319        let mut total_snapshot_rows = 0;
320
321        // For each vnode, try to get its progress state
322        for vnode in progress_table.vnodes().iter_vnodes() {
323            // Build key: vnode + NULL values for locality columns (to match progress table schema)
324            let key_data = vec![Some(vnode.to_scalar().into())];
325
326            let key = OwnedRow::new(key_data);
327
328            if let Some(row) = progress_table.get_row(&key).await? {
329                // Parse is_finished flag (second to last column)
330                let finished_col_idx = row.len() - 2;
331                let is_finished = row
332                    .datum_at(finished_col_idx)
333                    .map(|d| d.into_bool())
334                    .unwrap_or(false);
335
336                // Parse row count (last column)
337                let row_count = row
338                    .datum_at(row.len() - 1)
339                    .map(|d| d.into_int64() as u64)
340                    .unwrap_or(0);
341
342                let current_pos_data: Vec<Datum> = (1..finished_col_idx)
343                    .map(|i| row.datum_at(i).to_owned_datum())
344                    .collect();
345                let current_pos = OwnedRow::new(current_pos_data);
346
347                // Set progress based on is_finished flag
348                let progress = if is_finished {
349                    LocalityBackfillProgress::Completed {
350                        final_pos: current_pos,
351                        total_rows: row_count,
352                    }
353                } else {
354                    LocalityBackfillProgress::InProgress {
355                        current_pos,
356                        processed_rows: row_count,
357                    }
358                };
359
360                backfill_state.per_vnode.insert(vnode, progress);
361                total_snapshot_rows += row_count;
362            }
363            // If no row found, keep the default NotStarted state
364        }
365
366        backfill_state.total_snapshot_rows = total_snapshot_rows;
367        Ok(backfill_state)
368    }
369
370    /// Mark chunk for forwarding based on backfill progress
371    fn mark_chunk(
372        chunk: StreamChunk,
373        backfill_state: &LocalityBackfillState,
374        state_table: &StateTable<S>,
375    ) -> StreamExecutorResult<StreamChunk> {
376        let chunk = chunk.compact_vis();
377        let (data, ops) = chunk.into_parts();
378        let mut new_visibility = risingwave_common::bitmap::BitmapBuilder::with_capacity(ops.len());
379
380        let pk_indices = state_table.pk_indices();
381        let pk_order = state_table.pk_serde().get_order_types();
382
383        for row in data.rows() {
384            // Project to primary key columns for comparison
385            let pk = row.project(pk_indices);
386            let vnode = state_table.compute_vnode_by_pk(pk);
387
388            let visible = match backfill_state.get_progress(&vnode) {
389                LocalityBackfillProgress::Completed { .. } => true,
390                LocalityBackfillProgress::NotStarted => false,
391                LocalityBackfillProgress::InProgress { current_pos, .. } => {
392                    // Compare primary key with current position
393                    cmp_datum_iter(pk.iter(), current_pos.iter(), pk_order.iter().copied()).is_le()
394                }
395            };
396
397            new_visibility.append(visible);
398        }
399
400        let (columns, _) = data.into_parts();
401        let chunk = StreamChunk::with_visibility(ops, columns, new_visibility.finish());
402        Ok(chunk)
403    }
404
405    fn handle_snapshot_chunk(
406        data_chunk: DataChunk,
407        vnode: VirtualNode,
408        pk_indices: &[usize],
409        backfill_state: &mut LocalityBackfillState,
410        cur_barrier_snapshot_processed_rows: &mut u64,
411    ) -> StreamExecutorResult<StreamChunk> {
412        let chunk = StreamChunk::from_parts(vec![Op::Insert; data_chunk.cardinality()], data_chunk);
413        let chunk_cardinality = chunk.cardinality() as u64;
414
415        // Extract primary key from the last row to update progress
416        // As snapshot read streams are ordered by pk, we can use the last row to update current_pos
417        if let Some(last_row) = chunk.rows().last() {
418            let pk = last_row.1.project(pk_indices);
419            let pk_owned = pk.into_owned_row();
420            backfill_state.update_progress(vnode, pk_owned, chunk_cardinality);
421        }
422
423        *cur_barrier_snapshot_processed_rows += chunk_cardinality;
424        Ok(chunk)
425    }
426}
427
428impl<S: StateStore> Execute for LocalityProviderExecutor<S> {
429    fn execute(self: Box<Self>) -> BoxedMessageStream {
430        self.execute_inner().boxed()
431    }
432}
433
434impl<S: StateStore> LocalityProviderExecutor<S> {
435    #[try_stream(ok = Message, error = StreamExecutorError)]
436    async fn execute_inner(mut self) {
437        let mut upstream = self.upstream.execute();
438
439        // Wait for first barrier to initialize
440        let first_barrier = expect_first_barrier(&mut upstream).await?;
441        let first_epoch = first_barrier.epoch;
442
443        // Propagate the first barrier
444        yield Message::Barrier(first_barrier);
445
446        let mut state_table = self.state_table;
447        let mut progress_table = self.progress_table;
448
449        // Initialize state tables
450        state_table.init_epoch(first_epoch).await?;
451        progress_table.init_epoch(first_epoch).await?;
452
453        // Load backfill state from progress table
454        let mut backfill_state = Self::load_backfill_state(&progress_table).await?;
455
456        // Get pk info from state table
457        let pk_indices = state_table.pk_indices().iter().cloned().collect_vec();
458
459        let need_backfill = !backfill_state.is_completed();
460        let mut report_finished_on_first_barrier = !need_backfill;
461
462        let need_buffering = backfill_state
463            .per_vnode
464            .values()
465            .all(|progress| matches!(progress, LocalityBackfillProgress::NotStarted));
466        // Initial buffering phase before backfill - wait for StartFragmentBackfill mutation (if needed)
467        if need_buffering {
468            // Enter buffering phase - buffer data until StartFragmentBackfill is received
469            let mut start_backfill = false;
470
471            #[for_await]
472            for msg in upstream.by_ref() {
473                let msg = msg?;
474
475                match msg {
476                    Message::Watermark(_) => {
477                        // Ignore watermarks during initial buffering
478                    }
479                    Message::Chunk(chunk) => {
480                        state_table.write_chunk(chunk);
481                        state_table.try_flush().await?;
482                    }
483                    Message::Barrier(barrier) => {
484                        let epoch = barrier.epoch;
485
486                        // Check for StartFragmentBackfill mutation
487                        if let Some(mutation) = barrier.mutation.as_deref() {
488                            use crate::executor::Mutation;
489                            if let Mutation::StartFragmentBackfill { fragment_ids } = mutation
490                                && fragment_ids.contains(&self.fragment_id)
491                            {
492                                tracing::info!(
493                                    "Start backfill of locality provider with fragment id: {:?}",
494                                    &self.fragment_id
495                                );
496                                start_backfill = true;
497                            }
498                        }
499
500                        // Commit state tables
501                        barrier.assume_no_update_vnode_bitmap(self.actor_id)?;
502                        state_table
503                            .commit_assert_no_update_vnode_bitmap(epoch)
504                            .await?;
505                        progress_table
506                            .commit_assert_no_update_vnode_bitmap(epoch)
507                            .await?;
508
509                        yield Message::Barrier(barrier);
510
511                        // Start backfill when StartFragmentBackfill mutation is received
512                        if start_backfill {
513                            break;
514                        }
515                    }
516                }
517            }
518        }
519
520        // Locality Provider Backfill Algorithm (adapted from Arrangement Backfill):
521        //
522        //   backfill_stream
523        //  /               \
524        // upstream       snapshot (from state_table)
525        //
526        // We construct a backfill stream with upstream as its left input and locality-ordered
527        // snapshot read stream as its right input. When a chunk comes from upstream, we buffer it.
528        //
529        // When a barrier comes from upstream:
530        //  - For each row of the upstream chunk buffer, compute vnode.
531        //  - Get the `current_pos` corresponding to the vnode. Forward it to downstream if its
532        //    locality key <= `current_pos`, otherwise ignore it.
533        //  - Flush all buffered upstream_chunks to state table.
534        //  - Persist backfill progress to progress table.
535        //  - Reconstruct the snapshot read stream only if buffered upstream chunks changed the
536        //    state table. Otherwise, continue the same snapshot read stream across the barrier.
537        //
538        // When a chunk comes from snapshot, we forward it to the downstream and raise
539        // `current_pos`.
540        //
541        // When we reach the end of the snapshot read stream, it means backfill has been
542        // finished.
543        //
544        // Once the backfill loop ends, we forward the upstream directly to the downstream.
545
546        if need_backfill {
547            let mut upstream_chunk_buffer: Vec<StreamChunk> = vec![];
548
549            let metrics = self
550                .metrics
551                .new_backfill_metrics(state_table.table_id(), self.actor_id);
552
553            // Create builders for snapshot data chunks
554            let snapshot_data_types = self.input_schema.data_types();
555            let mut builders: Builders = state_table
556                .vnodes()
557                .iter_vnodes()
558                .map(|vnode| {
559                    let builder = create_builder(
560                        RateLimit::Disabled,
561                        self.chunk_size,
562                        snapshot_data_types.clone(),
563                    );
564                    (vnode, builder)
565                })
566                .collect();
567
568            let snapshot_reader = state_table.flushed_snapshot_reader();
569            let snapshot_stream =
570                Self::make_snapshot_stream(snapshot_reader.clone(), backfill_state.clone());
571            pin_mut!(snapshot_stream);
572
573            'backfill_loop: loop {
574                let mut cur_barrier_snapshot_processed_rows: u64 = 0;
575                let mut cur_barrier_upstream_processed_rows: u64 = 0;
576
577                // Prefer upstream so a ready barrier can pause snapshot output promptly, while
578                // keeping the snapshot stream itself alive across barriers with no upstream data.
579                let barrier = loop {
580                    let upstream_next = upstream.next();
581                    let mut snapshot_stream_ref = snapshot_stream.as_mut();
582                    let snapshot_next = snapshot_stream_ref.next();
583                    pin_mut!(upstream_next);
584                    pin_mut!(snapshot_next);
585
586                    match select(upstream_next, snapshot_next).await {
587                        FutureEither::Left((msg, _)) => match msg.transpose()? {
588                            Some(Message::Barrier(barrier)) => {
589                                // Process the barrier after draining the snapshot builders.
590                                break barrier;
591                            }
592                            Some(Message::Chunk(chunk)) => {
593                                // Buffer the upstream chunk.
594                                upstream_chunk_buffer.push(chunk.compact_vis());
595                            }
596                            Some(Message::Watermark(_)) => {
597                                // Ignore watermark during backfill.
598                            }
599                            None => {
600                                return Err(anyhow::anyhow!(
601                                    "locality provider upstream ended unexpectedly during backfill"
602                                )
603                                .into());
604                            }
605                        },
606                        FutureEither::Right((msg, _)) => match msg.transpose()? {
607                            Some((vnode, row)) => {
608                                // Use builder to batch rows efficiently
609                                let builder = builders.get_mut(&vnode).unwrap();
610                                if let Some(data_chunk) = builder.append_one_row(row) {
611                                    // Builder is full, handle the chunk
612                                    let chunk = Self::handle_snapshot_chunk(
613                                        data_chunk,
614                                        vnode,
615                                        &pk_indices,
616                                        &mut backfill_state,
617                                        &mut cur_barrier_snapshot_processed_rows,
618                                    )?;
619                                    yield Message::Chunk(chunk);
620                                }
621                                // If append_one_row returns None, row is buffered but no chunk is produced yet
622                                // Progress will be updated when the builder is consumed later
623                            }
624                            None => {
625                                // End of the snapshot read stream.
626                                // Consume remaining rows in the builders.
627                                for (vnode, builder) in &mut builders {
628                                    if let Some(data_chunk) = builder.consume_all() {
629                                        let chunk = Self::handle_snapshot_chunk(
630                                            data_chunk,
631                                            *vnode,
632                                            &pk_indices,
633                                            &mut backfill_state,
634                                            &mut cur_barrier_snapshot_processed_rows,
635                                        )?;
636                                        yield Message::Chunk(chunk);
637                                    }
638                                }
639
640                                // Consume remaining rows in the upstream buffer.
641                                for chunk in upstream_chunk_buffer.drain(..) {
642                                    let chunk_cardinality = chunk.cardinality() as u64;
643                                    cur_barrier_upstream_processed_rows += chunk_cardinality;
644                                    yield Message::Chunk(chunk);
645                                }
646                                metrics
647                                    .backfill_snapshot_read_row_count
648                                    .inc_by(cur_barrier_snapshot_processed_rows);
649                                metrics
650                                    .backfill_upstream_output_row_count
651                                    .inc_by(cur_barrier_upstream_processed_rows);
652                                break 'backfill_loop;
653                            }
654                        },
655                    }
656                };
657
658                // Consume remaining rows from builders at barrier
659                for (vnode, builder) in &mut builders {
660                    if let Some(data_chunk) = builder.consume_all() {
661                        let chunk = Self::handle_snapshot_chunk(
662                            data_chunk,
663                            *vnode,
664                            &pk_indices,
665                            &mut backfill_state,
666                            &mut cur_barrier_snapshot_processed_rows,
667                        )?;
668                        yield Message::Chunk(chunk);
669                    }
670                }
671
672                // Process upstream buffer chunks with marking
673                let should_refresh_snapshot = !upstream_chunk_buffer.is_empty();
674                for chunk in upstream_chunk_buffer.drain(..) {
675                    cur_barrier_upstream_processed_rows += chunk.cardinality() as u64;
676
677                    // Mark chunk based on backfill progress
678                    if backfill_state.has_progress() {
679                        let marked_chunk =
680                            Self::mark_chunk(chunk.clone(), &backfill_state, &state_table)?;
681                        yield Message::Chunk(marked_chunk);
682                    }
683
684                    // Persist buffered upstream chunk into state table so subsequent snapshot
685                    // iterations see the latest writes.
686                    state_table.write_chunk(chunk);
687                }
688
689                let barrier_epoch = barrier.epoch;
690                barrier.assume_no_update_vnode_bitmap(self.actor_id)?;
691                state_table
692                    .commit_assert_no_update_vnode_bitmap(barrier_epoch)
693                    .await?;
694
695                // Update progress with current epoch and snapshot read count
696                // Report both consumed rows and buffered rows separately for precise progress
697                let total_snapshot_processed_rows: u64 = backfill_state
698                    .vnodes()
699                    .map(|(_, progress)| match *progress {
700                        LocalityBackfillProgress::InProgress { processed_rows, .. } => {
701                            processed_rows
702                        }
703                        LocalityBackfillProgress::Completed { total_rows, .. } => total_rows,
704                        LocalityBackfillProgress::NotStarted => 0,
705                    })
706                    .sum();
707
708                self.progress.update_with_buffered_rows(
709                    barrier.epoch,
710                    barrier.epoch.curr, // Use barrier epoch as snapshot read epoch
711                    total_snapshot_processed_rows,
712                    0,
713                );
714
715                // Persist backfill progress
716                Self::persist_backfill_state(&mut progress_table, &backfill_state).await?;
717                progress_table
718                    .commit_assert_no_update_vnode_bitmap(barrier_epoch)
719                    .await?;
720
721                metrics
722                    .backfill_snapshot_read_row_count
723                    .inc_by(cur_barrier_snapshot_processed_rows);
724                metrics
725                    .backfill_upstream_output_row_count
726                    .inc_by(cur_barrier_upstream_processed_rows);
727
728                yield Message::Barrier(barrier);
729
730                if should_refresh_snapshot {
731                    snapshot_stream.set(Self::make_snapshot_stream(
732                        snapshot_reader.clone(),
733                        backfill_state.clone(),
734                    ));
735                }
736            }
737        }
738
739        tracing::debug!("Locality provider backfill finished, forwarding upstream directly");
740
741        // Wait for first barrier after backfill completion to mark progress as finished
742        if need_backfill && !backfill_state.is_completed() {
743            while let Some(Ok(msg)) = upstream.next().await {
744                match msg {
745                    Message::Barrier(barrier) => {
746                        barrier.assume_no_update_vnode_bitmap(self.actor_id)?;
747
748                        // no-op commit state table
749                        state_table
750                            .commit_assert_no_update_vnode_bitmap(barrier.epoch)
751                            .await?;
752
753                        // Mark all vnodes as completed
754                        for vnode in state_table.vnodes().iter_vnodes() {
755                            backfill_state.finish_vnode(vnode, pk_indices.len());
756                        }
757
758                        // Calculate final total processed rows
759                        let total_snapshot_processed_rows: u64 = backfill_state
760                            .vnodes()
761                            .map(|(_, progress)| match *progress {
762                                LocalityBackfillProgress::Completed { total_rows, .. } => {
763                                    total_rows
764                                }
765                                LocalityBackfillProgress::InProgress { processed_rows, .. } => {
766                                    processed_rows
767                                }
768                                LocalityBackfillProgress::NotStarted => 0,
769                            })
770                            .sum();
771
772                        // Finish progress reporting with any remaining buffered rows
773                        // At completion, we report `total_snapshot_processed_rows` as buffered rows to make progress accurate.
774                        self.progress.finish_with_buffered_rows(
775                            barrier.epoch,
776                            total_snapshot_processed_rows,
777                            total_snapshot_processed_rows,
778                        );
779
780                        // Persist final state
781                        Self::persist_backfill_state(&mut progress_table, &backfill_state).await?;
782                        progress_table
783                            .commit_assert_no_update_vnode_bitmap(barrier.epoch)
784                            .await?;
785
786                        yield Message::Barrier(barrier);
787                        break; // Exit the loop after processing the barrier
788                    }
789                    Message::Chunk(chunk) => {
790                        // Forward chunks directly during completion phase
791                        yield Message::Chunk(chunk);
792                    }
793                    Message::Watermark(watermark) => {
794                        // Forward watermarks directly during completion phase
795                        yield Message::Watermark(watermark);
796                    }
797                }
798            }
799        }
800
801        // After backfill completion, forward messages directly
802        #[for_await]
803        for msg in upstream {
804            let msg = msg?;
805
806            match msg {
807                Message::Barrier(barrier) => {
808                    barrier.assume_no_update_vnode_bitmap(self.actor_id)?;
809
810                    // Commit state tables but don't modify them
811                    state_table
812                        .commit_assert_no_update_vnode_bitmap(barrier.epoch)
813                        .await?;
814                    progress_table
815                        .commit_assert_no_update_vnode_bitmap(barrier.epoch)
816                        .await?;
817                    if report_finished_on_first_barrier {
818                        // At completion, we report `total_snapshot_rows` as buffered rows to make progress accurate.
819                        self.progress.finish_with_buffered_rows(
820                            barrier.epoch,
821                            backfill_state.total_snapshot_rows,
822                            backfill_state.total_snapshot_rows,
823                        );
824                        report_finished_on_first_barrier = false;
825                    }
826                    yield Message::Barrier(barrier);
827                }
828                _ => {
829                    // Forward all other messages directly
830                    yield msg;
831                }
832            }
833        }
834    }
835}