Skip to main content

risingwave_meta/barrier/
utils.rs

1// Copyright 2024 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
16
17use itertools::Itertools;
18use risingwave_common::bitmap::Bitmap;
19use risingwave_common::catalog::TableId;
20use risingwave_common::id::FragmentId;
21use risingwave_common::util::epoch::EpochPair;
22use risingwave_common::util::stream_graph_visitor::visit_stream_node_cont;
23use risingwave_hummock_sdk::sstable_info::SstableInfo;
24use risingwave_hummock_sdk::table_stats::from_prost_table_stats_map;
25use risingwave_hummock_sdk::table_watermark::{
26    TableWatermarks, merge_multiple_new_table_watermarks,
27};
28use risingwave_hummock_sdk::vector_index::{VectorIndexAdd, VectorIndexDelta};
29use risingwave_hummock_sdk::{HummockSstableObjectId, LocalSstableInfo};
30use risingwave_meta_model::WorkerId;
31use risingwave_pb::catalog::PbTable;
32use risingwave_pb::catalog::table::PbTableType;
33use risingwave_pb::hummock::vector_index_delta::PbVectorIndexInit;
34use risingwave_pb::stream_plan::stream_node::NodeBody;
35use risingwave_pb::stream_service::BarrierCompleteResponse;
36use risingwave_pb::stream_service::barrier_complete_response::{
37    IcebergPkIndexSinkMetadata, PbTableChangeLogProgress,
38};
39
40use crate::barrier::CreateStreamingJobCommandInfo;
41use crate::barrier::command::PostCollectCommand;
42use crate::barrier::complete_task::CompleteBarrierTask;
43use crate::barrier::partial_graph::PartialGraphBarrierInfo;
44use crate::hummock::{CommittedTableChangeLogProgress, NewTableFragmentInfo};
45
46#[expect(clippy::type_complexity)]
47pub(super) fn collect_resp_info(
48    resps: Vec<BarrierCompleteResponse>,
49) -> (
50    HashMap<HummockSstableObjectId, WorkerId>,
51    Vec<LocalSstableInfo>,
52    HashMap<TableId, TableWatermarks>,
53    Vec<SstableInfo>,
54    HashMap<TableId, Vec<VectorIndexAdd>>,
55    Vec<IcebergPkIndexSinkMetadata>,
56    Vec<CommittedTableChangeLogProgress>,
57) {
58    let mut sst_to_worker: HashMap<HummockSstableObjectId, _> = HashMap::new();
59    let mut synced_ssts: Vec<LocalSstableInfo> = vec![];
60    let mut table_watermarks = Vec::with_capacity(resps.len());
61    let mut old_value_ssts = Vec::with_capacity(resps.len());
62    let mut vector_index_adds = HashMap::new();
63    let mut iceberg_pk_index_sink_metadata = Vec::new();
64    type SubscriberKey = (FragmentId, TableId, TableId);
65    struct TableChangeLogProgressAggregate {
66        vnode_bitmap: Bitmap,
67        min_truncate_epoch: u64,
68    }
69    let mut table_change_log_reports =
70        HashMap::<SubscriberKey, TableChangeLogProgressAggregate>::new();
71    let mut invalid_table_change_log_subscribers = HashSet::new();
72
73    for resp in resps {
74        let ssts_iter = resp.synced_sstables.into_iter().map(|local_sst| {
75            let sst_info = local_sst.sst.expect("field not None");
76            sst_to_worker.insert(sst_info.object_id, resp.worker_id);
77            LocalSstableInfo::new(
78                sst_info.into(),
79                from_prost_table_stats_map(local_sst.table_stats_map),
80                local_sst.created_at,
81            )
82        });
83        synced_ssts.extend(ssts_iter);
84        table_watermarks.push(resp.table_watermarks);
85        old_value_ssts.extend(resp.old_value_sstables.into_iter().map(|s| s.into()));
86        for (table_id, vector_index_add) in resp.vector_index_adds {
87            vector_index_adds
88                .try_insert(
89                    table_id,
90                    vector_index_add
91                        .adds
92                        .into_iter()
93                        .map(VectorIndexAdd::from)
94                        .collect(),
95                )
96                .expect("non-duplicate");
97        }
98        iceberg_pk_index_sink_metadata.extend(resp.iceberg_pk_index_sink_metadata);
99        for report in resp.table_change_log_progress {
100            let PbTableChangeLogProgress {
101                fragment_id,
102                upstream_table_id,
103                progress_table_id,
104                min_truncate_epoch,
105                vnode_bitmap,
106            } = report;
107            let key = (fragment_id, upstream_table_id, progress_table_id);
108            let Some(vnode_bitmap) = vnode_bitmap else {
109                tracing::warn!(
110                    %fragment_id,
111                    %upstream_table_id,
112                    %progress_table_id,
113                    "ignore missing table change log progress vnode bitmap"
114                );
115                invalid_table_change_log_subscribers.insert(key);
116                continue;
117            };
118            let vnode_bitmap = Bitmap::from(&vnode_bitmap);
119            if !vnode_bitmap.any() {
120                tracing::warn!(
121                    %fragment_id,
122                    %upstream_table_id,
123                    %progress_table_id,
124                    "ignore empty table change log progress vnode bitmap"
125                );
126                invalid_table_change_log_subscribers.insert(key);
127                continue;
128            }
129            match table_change_log_reports.entry(key) {
130                std::collections::hash_map::Entry::Vacant(entry) => {
131                    entry.insert(TableChangeLogProgressAggregate {
132                        vnode_bitmap,
133                        min_truncate_epoch,
134                    });
135                }
136                std::collections::hash_map::Entry::Occupied(mut entry) => {
137                    let progress = entry.get_mut();
138                    if progress.vnode_bitmap.len() != vnode_bitmap.len()
139                        || (&progress.vnode_bitmap & &vnode_bitmap).any()
140                    {
141                        tracing::warn!(
142                            %fragment_id,
143                            %upstream_table_id,
144                            %progress_table_id,
145                            "ignore inconsistent table change log progress vnode ownership"
146                        );
147                        invalid_table_change_log_subscribers.insert(key);
148                        continue;
149                    }
150                    progress.vnode_bitmap |= &vnode_bitmap;
151                    progress.min_truncate_epoch =
152                        progress.min_truncate_epoch.min(min_truncate_epoch);
153                }
154            }
155        }
156    }
157
158    let table_change_log_progress = table_change_log_reports
159        .into_iter()
160        .filter_map(
161            |(key @ (fragment_id, upstream_table_id, progress_table_id), progress)| {
162                if invalid_table_change_log_subscribers.contains(&key) {
163                    return None;
164                }
165                if !progress.vnode_bitmap.all() {
166                    tracing::warn!(
167                        %fragment_id,
168                        %upstream_table_id,
169                        %progress_table_id,
170                        covered_vnode_count = progress.vnode_bitmap.count_ones(),
171                        vnode_count = progress.vnode_bitmap.len(),
172                        "ignore incomplete table change log progress vnode coverage"
173                    );
174                    return None;
175                }
176                Some(CommittedTableChangeLogProgress {
177                    upstream_table_id,
178                    progress_table_id,
179                    truncate_epoch: progress.min_truncate_epoch,
180                })
181            },
182        )
183        .collect();
184
185    (
186        sst_to_worker,
187        synced_ssts,
188        merge_multiple_new_table_watermarks(
189            table_watermarks
190                .into_iter()
191                .map(|watermarks| {
192                    watermarks
193                        .into_iter()
194                        .map(|(table_id, watermarks)| {
195                            (table_id, TableWatermarks::from(&watermarks))
196                        })
197                        .collect()
198                })
199                .collect_vec(),
200        ),
201        old_value_ssts,
202        vector_index_adds,
203        iceberg_pk_index_sink_metadata,
204        table_change_log_progress,
205    )
206}
207
208pub(super) fn collect_new_vector_index_info(
209    info: &CreateStreamingJobCommandInfo,
210) -> Option<&PbTable> {
211    let mut vector_index_table = None;
212    {
213        for fragment in info.stream_job_fragments.fragments.values() {
214            visit_stream_node_cont(&fragment.nodes, |node| {
215                match node.node_body.as_ref().unwrap() {
216                    NodeBody::VectorIndexWrite(vector_index_write) => {
217                        let index_table = vector_index_write.table.as_ref().unwrap();
218                        assert_eq!(index_table.table_type, PbTableType::VectorIndex as i32);
219                        vector_index_table = Some(index_table);
220                        false
221                    }
222                    _ => true,
223                }
224            })
225        }
226        vector_index_table
227    }
228}
229
230pub(super) fn collect_independent_job_commit_epoch_info(
231    task: &mut CompleteBarrierTask,
232    epoch: u64,
233    resps: Vec<BarrierCompleteResponse>,
234    barrier_info: &PartialGraphBarrierInfo,
235) {
236    let (
237        sst_to_context,
238        sstables,
239        new_table_watermarks,
240        old_value_sst,
241        vector_index_adds,
242        iceberg_pk_index_sink_metadata,
243        table_change_log_progress,
244    ) = collect_resp_info(resps);
245    assert!(old_value_sst.is_empty());
246    let commit_info = &mut task.commit_info;
247    commit_info.sst_to_context.extend(sst_to_context);
248    commit_info.sstables.extend(sstables);
249    commit_info
250        .new_table_watermarks
251        .extend(new_table_watermarks);
252    for (table_id, vector_index_adds) in vector_index_adds {
253        commit_info
254            .vector_index_delta
255            .try_insert(table_id, VectorIndexDelta::Adds(vector_index_adds))
256            .expect("non-duplicate");
257    }
258    barrier_info
259        .table_ids_to_commit
260        .iter()
261        .for_each(|table_id| {
262            commit_info
263                .tables_to_commit
264                .try_insert(*table_id, epoch)
265                .expect("non duplicate");
266        });
267    if let PostCollectCommand::CreateStreamingJob { info, .. } = &barrier_info.post_collect_command
268    {
269        commit_info
270            .new_table_fragment_infos
271            .push(NewTableFragmentInfo {
272                table_ids: barrier_info.table_ids_to_commit.clone(),
273            });
274        if let Some(index_table) = collect_new_vector_index_info(info) {
275            commit_info
276                .vector_index_delta
277                .try_insert(
278                    index_table.id,
279                    VectorIndexDelta::Init(PbVectorIndexInit {
280                        info: Some(index_table.vector_index_info.unwrap()),
281                    }),
282                )
283                .expect("non-duplicate");
284        }
285    };
286    task.iceberg_pk_index_pre_commit_metadata
287        .extend(iceberg_pk_index_sink_metadata.into_iter().map(Into::into));
288    commit_info
289        .table_change_log_progress
290        .extend(table_change_log_progress);
291}
292
293pub(super) type NodeToCollect = HashSet<WorkerId>;
294pub(super) fn is_valid_after_worker_err(
295    node_to_collect: &NodeToCollect,
296    worker_id: WorkerId,
297) -> bool {
298    !node_to_collect.contains(&worker_id)
299}
300
301#[derive(Debug)]
302struct InflightBarrierNode<K, Item, Info> {
303    epoch: EpochPair,
304    to_collect: HashSet<K>,
305    collected: HashMap<K, Item>,
306    info: Info,
307}
308
309#[derive(Debug)]
310struct CollectedBarrierNode<K, Item, Info> {
311    epoch: EpochPair,
312    collected: HashMap<K, Item>,
313    info: Info,
314}
315
316#[derive(Debug)]
317pub(super) struct BarrierItemCollector<K, Item, Info> {
318    /// `prev_epoch` -> barrier
319    inflight_barriers: BTreeMap<u64, InflightBarrierNode<K, Item, Info>>,
320    /// newer epoch at the back. `push_back` and `pop_front`
321    collected_barriers: VecDeque<CollectedBarrierNode<K, Item, Info>>,
322    /// Whether successive barriers are expected to be epoch-contiguous (the `curr` epoch of one
323    /// barrier equals the `prev` epoch of the next). This holds for the main per-barrier collector,
324    /// where every injected barrier is enqueued, but NOT for collectors that are only fed a sparse
325    /// subset of barriers (e.g. the finishing-jobs collector, enqueued only when a snapshot-backfill
326    /// job finishes). When `false`, only monotonicity and non-duplication are checked.
327    require_contiguous: bool,
328}
329
330impl<K: std::fmt::Debug + Eq + std::hash::Hash, Item: std::fmt::Debug, Info: std::fmt::Debug>
331    BarrierItemCollector<K, Item, Info>
332{
333    pub(super) fn new(require_contiguous: bool) -> Self {
334        Self {
335            inflight_barriers: Default::default(),
336            collected_barriers: Default::default(),
337            require_contiguous,
338        }
339    }
340
341    pub(super) fn is_empty(&self) -> bool {
342        self.inflight_barriers.is_empty() && self.collected_barriers.is_empty()
343    }
344
345    pub(super) fn inflight_barrier_num(&self) -> usize {
346        self.inflight_barriers.len()
347    }
348
349    pub(super) fn collected_barrier_num(&self) -> usize {
350        self.collected_barriers.len()
351    }
352
353    pub(super) fn enqueue(&mut self, epoch: EpochPair, to_collect: HashSet<K>, info: Info) {
354        assert!(!to_collect.is_empty());
355        if let Some((last_prev_epoch, last_barrier)) = self.inflight_barriers.last_key_value() {
356            if self.require_contiguous {
357                assert_eq!(last_barrier.epoch.curr, epoch.prev);
358            }
359            assert!(*last_prev_epoch < epoch.prev);
360        }
361        self.inflight_barriers
362            .try_insert(
363                epoch.prev,
364                InflightBarrierNode {
365                    epoch,
366                    to_collect,
367                    collected: Default::default(),
368                    info,
369                },
370            )
371            .expect("non-duplicated");
372    }
373
374    pub(super) fn collect(&mut self, prev_epoch: u64, key: K, item: Item) {
375        let inflight_barrier = self
376            .inflight_barriers
377            .get_mut(&prev_epoch)
378            .expect("should exist");
379        assert!(inflight_barrier.to_collect.remove(&key));
380        inflight_barrier
381            .collected
382            .try_insert(key, item)
383            .expect("non-duplicate");
384    }
385
386    pub(super) fn barrier_collected(&mut self) -> Option<(EpochPair, &Info)> {
387        if let Some(entry) = self.inflight_barriers.first_entry()
388            && entry.get().to_collect.is_empty()
389        {
390            let InflightBarrierNode {
391                epoch,
392                collected,
393                info,
394                ..
395            } = entry.remove();
396            if self.require_contiguous
397                && let Some(prev_barrier) = self.collected_barriers.back()
398            {
399                assert_eq!(prev_barrier.epoch.curr, epoch.prev);
400            }
401            self.collected_barriers.push_back(CollectedBarrierNode {
402                epoch,
403                collected,
404                info,
405            });
406            Some((
407                epoch,
408                &self.collected_barriers.back().expect("non-empty").info,
409            ))
410        } else {
411            None
412        }
413    }
414
415    pub(super) fn advance_collected(&mut self) {
416        while self.barrier_collected().is_some() {}
417    }
418
419    pub(super) fn first_inflight_epoch(&self) -> Option<EpochPair> {
420        self.inflight_barriers
421            .first_key_value()
422            .map(|(_, barrier)| barrier.epoch)
423    }
424
425    pub(super) fn last_collected(&self) -> Option<(EpochPair, &HashMap<K, Item>, &Info)> {
426        self.collected_barriers
427            .back()
428            .map(|barrier| (barrier.epoch, &barrier.collected, &barrier.info))
429    }
430
431    pub(super) fn iter_infos(&self) -> impl Iterator<Item = &Info> + '_ {
432        // Preserve epoch order: collected barriers are older and kept from old to new,
433        // followed by inflight barriers ordered by `prev_epoch` in the BTreeMap.
434        self.collected_barriers
435            .iter()
436            .map(|barrier| &barrier.info)
437            .chain(self.inflight_barriers.values().map(|barrier| &barrier.info))
438    }
439
440    pub(super) fn into_infos(self) -> impl Iterator<Item = Info> {
441        self.inflight_barriers
442            .into_values()
443            .map(|barrier| barrier.info)
444            .chain(
445                self.collected_barriers
446                    .into_iter()
447                    .map(|barrier| barrier.info),
448            )
449    }
450
451    pub(super) fn iter_to_collect(&self) -> impl Iterator<Item = &HashSet<K>> + '_ {
452        self.inflight_barriers
453            .values()
454            .map(|barrier| &barrier.to_collect)
455    }
456
457    pub(super) fn take_collected_if(
458        &mut self,
459        cond: impl FnOnce(EpochPair) -> bool,
460    ) -> Option<(EpochPair, HashMap<K, Item>, Info)> {
461        self.collected_barriers
462            .pop_front_if(move |barrier| cond(barrier.epoch))
463            .map(|barrier| (barrier.epoch, barrier.collected, barrier.info))
464    }
465}
466
467#[cfg(test)]
468mod tests {
469    use std::collections::HashSet;
470
471    use risingwave_common::bitmap::Bitmap;
472    use risingwave_common::catalog::TableId;
473    use risingwave_common::id::FragmentId;
474    use risingwave_common::util::epoch::EpochPair;
475    use risingwave_pb::stream_service::BarrierCompleteResponse;
476    use risingwave_pb::stream_service::barrier_complete_response::PbTableChangeLogProgress;
477
478    use super::{BarrierItemCollector, collect_resp_info};
479
480    #[test]
481    fn table_change_log_progress_requires_complete_vnode_coverage() {
482        let report = |vnodes: &[usize], truncate_epoch| PbTableChangeLogProgress {
483            fragment_id: FragmentId::new(10),
484            upstream_table_id: TableId::new(20),
485            progress_table_id: TableId::new(30),
486            min_truncate_epoch: truncate_epoch,
487            vnode_bitmap: Some(Bitmap::from_indices(4, vnodes).to_protobuf()),
488        };
489        let responses = vec![
490            BarrierCompleteResponse {
491                table_change_log_progress: vec![report(&[0, 1], 100)],
492                ..Default::default()
493            },
494            BarrierCompleteResponse {
495                table_change_log_progress: vec![report(&[2, 3], 80)],
496                ..Default::default()
497            },
498        ];
499
500        let (_, _, _, _, _, _, progress) = collect_resp_info(responses);
501        assert_eq!(progress.len(), 1);
502        let progress = &progress[0];
503        assert_eq!(progress.upstream_table_id, TableId::new(20));
504        assert_eq!(progress.progress_table_id, TableId::new(30));
505        assert_eq!(progress.truncate_epoch, 80);
506
507        let incomplete_responses = vec![BarrierCompleteResponse {
508            table_change_log_progress: vec![report(&[0, 1], 100)],
509            ..Default::default()
510        }];
511        let (_, _, _, _, _, _, progress) = collect_resp_info(incomplete_responses);
512        assert!(progress.is_empty());
513
514        let overlapping_responses = vec![
515            BarrierCompleteResponse {
516                table_change_log_progress: vec![report(&[0, 1, 2], 100)],
517                ..Default::default()
518            },
519            BarrierCompleteResponse {
520                table_change_log_progress: vec![report(&[2, 3], 80)],
521                ..Default::default()
522            },
523        ];
524        let (_, _, _, _, _, _, progress) = collect_resp_info(overlapping_responses);
525        assert!(progress.is_empty());
526    }
527
528    /// The finishing-jobs collector is fed only on the sparse subset of barriers where a
529    /// snapshot-backfill job finishes, so enqueued epochs are not contiguous. Enqueuing a barrier
530    /// whose `prev` does not match the previous barrier's `curr` must be tolerated.
531    #[test]
532    fn non_contiguous_enqueue_allowed_when_not_required() {
533        let mut collector = BarrierItemCollector::<u32, (), ()>::new(false);
534        collector.enqueue(EpochPair::new(200, 100), HashSet::from([1]), ());
535        // gap: prev (500) != previous curr (200)
536        collector.enqueue(EpochPair::new(600, 500), HashSet::from([2]), ());
537        assert_eq!(collector.inflight_barrier_num(), 2);
538    }
539
540    /// The main per-barrier collector enqueues every barrier, so contiguity is a real invariant and
541    /// must still be asserted.
542    #[test]
543    #[should_panic]
544    fn non_contiguous_enqueue_rejected_when_required() {
545        let mut collector = BarrierItemCollector::<u32, (), ()>::new(true);
546        collector.enqueue(EpochPair::new(200, 100), HashSet::from([1]), ());
547        collector.enqueue(EpochPair::new(600, 500), HashSet::from([2]), ());
548    }
549}