1use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
16
17use itertools::Itertools;
18use risingwave_common::catalog::TableId;
19use risingwave_common::util::epoch::EpochPair;
20use risingwave_common::util::stream_graph_visitor::visit_stream_node_cont;
21use risingwave_hummock_sdk::sstable_info::SstableInfo;
22use risingwave_hummock_sdk::table_stats::from_prost_table_stats_map;
23use risingwave_hummock_sdk::table_watermark::{
24 TableWatermarks, merge_multiple_new_table_watermarks,
25};
26use risingwave_hummock_sdk::vector_index::{VectorIndexAdd, VectorIndexDelta};
27use risingwave_hummock_sdk::{HummockSstableObjectId, LocalSstableInfo};
28use risingwave_meta_model::WorkerId;
29use risingwave_pb::catalog::PbTable;
30use risingwave_pb::catalog::table::PbTableType;
31use risingwave_pb::hummock::vector_index_delta::PbVectorIndexInit;
32use risingwave_pb::stream_plan::stream_node::NodeBody;
33use risingwave_pb::stream_service::BarrierCompleteResponse;
34use risingwave_pb::stream_service::barrier_complete_response::IcebergPkIndexSinkMetadata;
35
36use crate::barrier::CreateStreamingJobCommandInfo;
37use crate::barrier::command::PostCollectCommand;
38use crate::barrier::complete_task::CompleteBarrierTask;
39use crate::barrier::partial_graph::PartialGraphBarrierInfo;
40use crate::hummock::NewTableFragmentInfo;
41
42#[expect(clippy::type_complexity)]
43pub(super) fn collect_resp_info(
44 resps: Vec<BarrierCompleteResponse>,
45) -> (
46 HashMap<HummockSstableObjectId, WorkerId>,
47 Vec<LocalSstableInfo>,
48 HashMap<TableId, TableWatermarks>,
49 Vec<SstableInfo>,
50 HashMap<TableId, Vec<VectorIndexAdd>>,
51 Vec<IcebergPkIndexSinkMetadata>,
52) {
53 let mut sst_to_worker: HashMap<HummockSstableObjectId, _> = HashMap::new();
54 let mut synced_ssts: Vec<LocalSstableInfo> = vec![];
55 let mut table_watermarks = Vec::with_capacity(resps.len());
56 let mut old_value_ssts = Vec::with_capacity(resps.len());
57 let mut vector_index_adds = HashMap::new();
58 let mut iceberg_pk_index_sink_metadata = Vec::new();
59
60 for resp in resps {
61 let ssts_iter = resp.synced_sstables.into_iter().map(|local_sst| {
62 let sst_info = local_sst.sst.expect("field not None");
63 sst_to_worker.insert(sst_info.object_id, resp.worker_id);
64 LocalSstableInfo::new(
65 sst_info.into(),
66 from_prost_table_stats_map(local_sst.table_stats_map),
67 local_sst.created_at,
68 )
69 });
70 synced_ssts.extend(ssts_iter);
71 table_watermarks.push(resp.table_watermarks);
72 old_value_ssts.extend(resp.old_value_sstables.into_iter().map(|s| s.into()));
73 for (table_id, vector_index_add) in resp.vector_index_adds {
74 vector_index_adds
75 .try_insert(
76 table_id,
77 vector_index_add
78 .adds
79 .into_iter()
80 .map(VectorIndexAdd::from)
81 .collect(),
82 )
83 .expect("non-duplicate");
84 }
85 iceberg_pk_index_sink_metadata.extend(resp.iceberg_pk_index_sink_metadata);
86 }
87
88 (
89 sst_to_worker,
90 synced_ssts,
91 merge_multiple_new_table_watermarks(
92 table_watermarks
93 .into_iter()
94 .map(|watermarks| {
95 watermarks
96 .into_iter()
97 .map(|(table_id, watermarks)| {
98 (table_id, TableWatermarks::from(&watermarks))
99 })
100 .collect()
101 })
102 .collect_vec(),
103 ),
104 old_value_ssts,
105 vector_index_adds,
106 iceberg_pk_index_sink_metadata,
107 )
108}
109
110pub(super) fn collect_new_vector_index_info(
111 info: &CreateStreamingJobCommandInfo,
112) -> Option<&PbTable> {
113 let mut vector_index_table = None;
114 {
115 for fragment in info.stream_job_fragments.fragments.values() {
116 visit_stream_node_cont(&fragment.nodes, |node| {
117 match node.node_body.as_ref().unwrap() {
118 NodeBody::VectorIndexWrite(vector_index_write) => {
119 let index_table = vector_index_write.table.as_ref().unwrap();
120 assert_eq!(index_table.table_type, PbTableType::VectorIndex as i32);
121 vector_index_table = Some(index_table);
122 false
123 }
124 _ => true,
125 }
126 })
127 }
128 vector_index_table
129 }
130}
131
132pub(super) fn collect_independent_job_commit_epoch_info(
133 task: &mut CompleteBarrierTask,
134 epoch: u64,
135 resps: Vec<BarrierCompleteResponse>,
136 barrier_info: &PartialGraphBarrierInfo,
137) {
138 let (
139 sst_to_context,
140 sstables,
141 new_table_watermarks,
142 old_value_sst,
143 vector_index_adds,
144 iceberg_pk_index_sink_metadata,
145 ) = collect_resp_info(resps);
146 assert!(old_value_sst.is_empty());
147 let commit_info = &mut task.commit_info;
148 commit_info.sst_to_context.extend(sst_to_context);
149 commit_info.sstables.extend(sstables);
150 commit_info
151 .new_table_watermarks
152 .extend(new_table_watermarks);
153 for (table_id, vector_index_adds) in vector_index_adds {
154 commit_info
155 .vector_index_delta
156 .try_insert(table_id, VectorIndexDelta::Adds(vector_index_adds))
157 .expect("non-duplicate");
158 }
159 barrier_info
160 .table_ids_to_commit
161 .iter()
162 .for_each(|table_id| {
163 commit_info
164 .tables_to_commit
165 .try_insert(*table_id, epoch)
166 .expect("non duplicate");
167 });
168 if let PostCollectCommand::CreateStreamingJob { info, .. } = &barrier_info.post_collect_command
169 {
170 commit_info
171 .new_table_fragment_infos
172 .push(NewTableFragmentInfo {
173 table_ids: barrier_info.table_ids_to_commit.clone(),
174 });
175 if let Some(index_table) = collect_new_vector_index_info(info) {
176 commit_info
177 .vector_index_delta
178 .try_insert(
179 index_table.id,
180 VectorIndexDelta::Init(PbVectorIndexInit {
181 info: Some(index_table.vector_index_info.unwrap()),
182 }),
183 )
184 .expect("non-duplicate");
185 }
186 };
187 task.iceberg_pk_index_pre_commit_metadata
188 .extend(iceberg_pk_index_sink_metadata.into_iter().map(Into::into));
189}
190
191pub(super) type NodeToCollect = HashSet<WorkerId>;
192pub(super) fn is_valid_after_worker_err(
193 node_to_collect: &NodeToCollect,
194 worker_id: WorkerId,
195) -> bool {
196 !node_to_collect.contains(&worker_id)
197}
198
199#[derive(Debug)]
200struct InflightBarrierNode<K, Item, Info> {
201 epoch: EpochPair,
202 to_collect: HashSet<K>,
203 collected: HashMap<K, Item>,
204 info: Info,
205}
206
207#[derive(Debug)]
208struct CollectedBarrierNode<K, Item, Info> {
209 epoch: EpochPair,
210 collected: HashMap<K, Item>,
211 info: Info,
212}
213
214#[derive(Debug)]
215pub(super) struct BarrierItemCollector<K, Item, Info> {
216 inflight_barriers: BTreeMap<u64, InflightBarrierNode<K, Item, Info>>,
218 collected_barriers: VecDeque<CollectedBarrierNode<K, Item, Info>>,
220 require_contiguous: bool,
226}
227
228impl<K: std::fmt::Debug + Eq + std::hash::Hash, Item: std::fmt::Debug, Info: std::fmt::Debug>
229 BarrierItemCollector<K, Item, Info>
230{
231 pub(super) fn new(require_contiguous: bool) -> Self {
232 Self {
233 inflight_barriers: Default::default(),
234 collected_barriers: Default::default(),
235 require_contiguous,
236 }
237 }
238
239 pub(super) fn is_empty(&self) -> bool {
240 self.inflight_barriers.is_empty() && self.collected_barriers.is_empty()
241 }
242
243 pub(super) fn inflight_barrier_num(&self) -> usize {
244 self.inflight_barriers.len()
245 }
246
247 pub(super) fn collected_barrier_num(&self) -> usize {
248 self.collected_barriers.len()
249 }
250
251 pub(super) fn enqueue(&mut self, epoch: EpochPair, to_collect: HashSet<K>, info: Info) {
252 assert!(!to_collect.is_empty());
253 if let Some((last_prev_epoch, last_barrier)) = self.inflight_barriers.last_key_value() {
254 if self.require_contiguous {
255 assert_eq!(last_barrier.epoch.curr, epoch.prev);
256 }
257 assert!(*last_prev_epoch < epoch.prev);
258 }
259 self.inflight_barriers
260 .try_insert(
261 epoch.prev,
262 InflightBarrierNode {
263 epoch,
264 to_collect,
265 collected: Default::default(),
266 info,
267 },
268 )
269 .expect("non-duplicated");
270 }
271
272 pub(super) fn collect(&mut self, prev_epoch: u64, key: K, item: Item) {
273 let inflight_barrier = self
274 .inflight_barriers
275 .get_mut(&prev_epoch)
276 .expect("should exist");
277 assert!(inflight_barrier.to_collect.remove(&key));
278 inflight_barrier
279 .collected
280 .try_insert(key, item)
281 .expect("non-duplicate");
282 }
283
284 pub(super) fn barrier_collected(&mut self) -> Option<(EpochPair, &Info)> {
285 if let Some(entry) = self.inflight_barriers.first_entry()
286 && entry.get().to_collect.is_empty()
287 {
288 let InflightBarrierNode {
289 epoch,
290 collected,
291 info,
292 ..
293 } = entry.remove();
294 if self.require_contiguous
295 && let Some(prev_barrier) = self.collected_barriers.back()
296 {
297 assert_eq!(prev_barrier.epoch.curr, epoch.prev);
298 }
299 self.collected_barriers.push_back(CollectedBarrierNode {
300 epoch,
301 collected,
302 info,
303 });
304 Some((
305 epoch,
306 &self.collected_barriers.back().expect("non-empty").info,
307 ))
308 } else {
309 None
310 }
311 }
312
313 pub(super) fn advance_collected(&mut self) {
314 while self.barrier_collected().is_some() {}
315 }
316
317 pub(super) fn first_inflight_epoch(&self) -> Option<EpochPair> {
318 self.inflight_barriers
319 .first_key_value()
320 .map(|(_, barrier)| barrier.epoch)
321 }
322
323 pub(super) fn last_collected(&self) -> Option<(EpochPair, &HashMap<K, Item>, &Info)> {
324 self.collected_barriers
325 .back()
326 .map(|barrier| (barrier.epoch, &barrier.collected, &barrier.info))
327 }
328
329 pub(super) fn iter_infos(&self) -> impl Iterator<Item = &Info> + '_ {
330 self.collected_barriers
333 .iter()
334 .map(|barrier| &barrier.info)
335 .chain(self.inflight_barriers.values().map(|barrier| &barrier.info))
336 }
337
338 pub(super) fn into_infos(self) -> impl Iterator<Item = Info> {
339 self.inflight_barriers
340 .into_values()
341 .map(|barrier| barrier.info)
342 .chain(
343 self.collected_barriers
344 .into_iter()
345 .map(|barrier| barrier.info),
346 )
347 }
348
349 pub(super) fn iter_to_collect(&self) -> impl Iterator<Item = &HashSet<K>> + '_ {
350 self.inflight_barriers
351 .values()
352 .map(|barrier| &barrier.to_collect)
353 }
354
355 pub(super) fn take_collected_if(
356 &mut self,
357 cond: impl FnOnce(EpochPair) -> bool,
358 ) -> Option<(EpochPair, HashMap<K, Item>, Info)> {
359 self.collected_barriers
360 .pop_front_if(move |barrier| cond(barrier.epoch))
361 .map(|barrier| (barrier.epoch, barrier.collected, barrier.info))
362 }
363}
364
365#[cfg(test)]
366mod tests {
367 use std::collections::HashSet;
368
369 use risingwave_common::util::epoch::EpochPair;
370
371 use super::BarrierItemCollector;
372
373 #[test]
377 fn non_contiguous_enqueue_allowed_when_not_required() {
378 let mut collector = BarrierItemCollector::<u32, (), ()>::new(false);
379 collector.enqueue(EpochPair::new(200, 100), HashSet::from([1]), ());
380 collector.enqueue(EpochPair::new(600, 500), HashSet::from([2]), ());
382 assert_eq!(collector.inflight_barrier_num(), 2);
383 }
384
385 #[test]
388 #[should_panic]
389 fn non_contiguous_enqueue_rejected_when_required() {
390 let mut collector = BarrierItemCollector::<u32, (), ()>::new(true);
391 collector.enqueue(EpochPair::new(200, 100), HashSet::from([1]), ());
392 collector.enqueue(EpochPair::new(600, 500), HashSet::from([2]), ());
393 }
394}