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