1use 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 inflight_barriers: BTreeMap<u64, InflightBarrierNode<K, Item, Info>>,
320 collected_barriers: VecDeque<CollectedBarrierNode<K, Item, Info>>,
322 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 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 #[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 collector.enqueue(EpochPair::new(600, 500), HashSet::from([2]), ());
537 assert_eq!(collector.inflight_barrier_num(), 2);
538 }
539
540 #[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}