1use std::collections::hash_map::Entry;
16use std::collections::{HashMap, HashSet, VecDeque};
17use std::mem::{replace, take};
18use std::time::Duration;
19
20use itertools::Itertools;
21use risingwave_common::hash::ActorId;
22use risingwave_common::util::epoch::Epoch;
23use risingwave_pb::hummock::HummockVersionStats;
24use risingwave_pb::id::{FragmentId, PartialGraphId};
25use risingwave_pb::stream_plan::StartFragmentBackfillMutation;
26use risingwave_pb::stream_plan::barrier::PbBarrierKind;
27use risingwave_pb::stream_plan::barrier_mutation::Mutation;
28use risingwave_pb::stream_service::barrier_complete_response::{
29 CreateMviewProgress, PbCreateMviewProgress,
30};
31use tracing::warn;
32
33use crate::barrier::checkpoint::independent_job::creating_job::CreatingJobInfo;
34use crate::barrier::command::{ThrottleConfigMap, extract_throttle_config};
35use crate::barrier::notifier::CollectionNotifier;
36use crate::barrier::partial_graph::PartialGraphManager;
37use crate::barrier::progress::{CreateMviewProgressTracker, TrackingJob};
38use crate::barrier::{BarrierInfo, BarrierKind, TracedEpoch};
39use crate::controller::fragment::InflightFragmentInfo;
40
41#[derive(Debug)]
42pub(super) struct CreateMviewLogStoreProgressTracker {
43 ongoing_actors: HashMap<ActorId, u64>,
45 finished_actors: HashSet<ActorId>,
46}
47
48impl CreateMviewLogStoreProgressTracker {
49 pub(super) fn new(actors: impl Iterator<Item = ActorId>, pending_barrier_lag: u64) -> Self {
50 Self {
51 ongoing_actors: HashMap::from_iter(actors.map(|actor| (actor, pending_barrier_lag))),
52 finished_actors: HashSet::new(),
53 }
54 }
55
56 pub(super) fn gen_backfill_progress(&self) -> String {
57 let sum = self.ongoing_actors.values().sum::<u64>() as f64;
58 let count = if self.ongoing_actors.is_empty() {
59 1
60 } else {
61 self.ongoing_actors.len()
62 } as f64;
63 let avg = sum / count;
64 let avg_lag_time = Duration::from_millis(Epoch(avg as _).physical_time());
65 format!(
66 "actor: {}/{}, avg lag {:?}",
67 self.finished_actors.len(),
68 self.ongoing_actors.len() + self.finished_actors.len(),
69 avg_lag_time
70 )
71 }
72
73 fn update(&mut self, progress: impl IntoIterator<Item = &PbCreateMviewProgress>) {
74 for progress in progress {
75 match self.ongoing_actors.entry(progress.backfill_actor_id) {
76 Entry::Occupied(mut entry) => {
77 if progress.done {
78 entry.remove_entry();
79 assert!(
80 self.finished_actors.insert(progress.backfill_actor_id),
81 "non-duplicate"
82 );
83 } else {
84 *entry.get_mut() = progress.pending_epoch_lag as _;
85 }
86 }
87 Entry::Vacant(_) => {
88 if cfg!(debug_assertions) {
89 panic!(
90 "reporting progress on non-inflight actor: {:?} {:?}",
91 progress, self
92 );
93 } else {
94 warn!(?progress, progress_tracker = ?self, "reporting progress on non-inflight actor");
95 }
96 }
97 }
98 }
99 }
100
101 pub(super) fn is_finished(&self) -> bool {
102 self.ongoing_actors.is_empty()
103 }
104}
105
106#[derive(Debug)]
107pub(super) enum CreatingStreamingJobStatus {
108 ConsumingSnapshot {
112 prev_epoch_fake_physical_time: u64,
113 pending_upstream_barriers: Vec<BarrierInfo>,
114 version_stats: HummockVersionStats,
115 create_mview_tracker: CreateMviewProgressTracker,
116 snapshot_backfill_actors: HashSet<ActorId>,
117 snapshot_epoch: u64,
118 info: CreatingJobInfo,
119 pending_non_checkpoint_barriers: Vec<u64>,
121 },
122 ConsumingLogStore {
126 tracking_job: TrackingJob,
127 info: CreatingJobInfo,
128 log_store_progress_tracker: CreateMviewLogStoreProgressTracker,
129 pending_barriers: VecDeque<BarrierInfo>,
130 },
131 Finishing(u64, TrackingJob),
135 Resetting(Vec<CollectionNotifier>),
136 PlaceHolder,
137}
138
139impl CreatingStreamingJobStatus {
140 pub(super) fn update_progress(
141 &mut self,
142 create_mview_progress: impl IntoIterator<Item = &CreateMviewProgress>,
143 ) {
144 match self {
145 &mut Self::ConsumingSnapshot {
146 ref mut create_mview_tracker,
147 ref version_stats,
148 ref mut prev_epoch_fake_physical_time,
149 ref mut pending_upstream_barriers,
150 ref mut pending_non_checkpoint_barriers,
151 ref snapshot_epoch,
152 ..
153 } => {
154 for progress in create_mview_progress {
155 create_mview_tracker.apply_progress(progress, version_stats);
156 }
157 if create_mview_tracker.is_finished() {
158 pending_non_checkpoint_barriers.push(*snapshot_epoch);
159
160 let prev_epoch = Epoch::from_physical_time(*prev_epoch_fake_physical_time);
161 let pending_barriers: VecDeque<_> = [BarrierInfo {
162 curr_epoch: TracedEpoch::new(Epoch(*snapshot_epoch)),
163 prev_epoch: TracedEpoch::new(prev_epoch),
164 kind: BarrierKind::Checkpoint(take(pending_non_checkpoint_barriers)),
165 }]
166 .into_iter()
167 .chain(pending_upstream_barriers.drain(..))
168 .collect();
169
170 let CreatingStreamingJobStatus::ConsumingSnapshot {
171 create_mview_tracker,
172 info,
173 snapshot_epoch,
174 snapshot_backfill_actors,
175 ..
176 } = replace(self, CreatingStreamingJobStatus::PlaceHolder)
177 else {
178 unreachable!()
179 };
180
181 let tracking_job = create_mview_tracker.into_tracking_job();
182
183 *self = CreatingStreamingJobStatus::ConsumingLogStore {
184 tracking_job,
185 info,
186 log_store_progress_tracker: CreateMviewLogStoreProgressTracker::new(
187 snapshot_backfill_actors.iter().cloned(),
188 pending_barriers
189 .back()
190 .map(|barrier_info| {
191 barrier_info.prev_epoch().saturating_sub(snapshot_epoch)
192 })
193 .unwrap_or(0),
194 ),
195 pending_barriers,
196 };
197 }
198 }
199 CreatingStreamingJobStatus::ConsumingLogStore {
200 log_store_progress_tracker,
201 ..
202 } => {
203 log_store_progress_tracker.update(create_mview_progress);
204 }
205 CreatingStreamingJobStatus::Finishing(..)
206 | CreatingStreamingJobStatus::Resetting(..) => {}
207 CreatingStreamingJobStatus::PlaceHolder => {
208 unreachable!()
209 }
210 }
211 }
212
213 pub(super) fn start_consume_upstream(&mut self, barrier_info: &BarrierInfo) -> CreatingJobInfo {
214 match self {
215 CreatingStreamingJobStatus::ConsumingSnapshot { .. } => {
216 unreachable!(
217 "should not start consuming upstream for a job that are consuming snapshot"
218 )
219 }
220 CreatingStreamingJobStatus::ConsumingLogStore { .. } => {
221 let prev_epoch = barrier_info.prev_epoch();
222 {
223 assert!(barrier_info.kind.is_checkpoint());
224 let CreatingStreamingJobStatus::ConsumingLogStore {
225 info, tracking_job, ..
226 } = replace(self, CreatingStreamingJobStatus::PlaceHolder)
227 else {
228 unreachable!()
229 };
230 *self = CreatingStreamingJobStatus::Finishing(prev_epoch, tracking_job);
231 info
232 }
233 }
234 CreatingStreamingJobStatus::Finishing { .. } => {
235 unreachable!("should not start consuming upstream for a job again")
236 }
237 CreatingStreamingJobStatus::Resetting(..) => {
238 unreachable!("unlikely to start consume upstream when resetting")
239 }
240 CreatingStreamingJobStatus::PlaceHolder => {
241 unreachable!()
242 }
243 }
244 }
245
246 pub(super) fn on_new_upstream_epoch(
247 &mut self,
248 partial_graph_manager: &PartialGraphManager,
249 partial_graph_id: PartialGraphId,
250 max_pending_barrier_num: usize,
251 barrier_info: &BarrierInfo,
252 mutation: Option<Mutation>, ) -> Vec<(BarrierInfo, Option<Mutation>)> {
254 let resolve_initial_barrier_num_to_inject = || {
255 max_pending_barrier_num
256 .saturating_sub(partial_graph_manager.pending_barrier_num(partial_graph_id))
257 };
258 match self {
259 CreatingStreamingJobStatus::ConsumingSnapshot {
260 pending_upstream_barriers,
261 prev_epoch_fake_physical_time,
262 pending_non_checkpoint_barriers,
263 create_mview_tracker,
264 ..
265 } => {
266 let mutation = mutation.or_else(|| {
267 let pending_backfill_nodes = create_mview_tracker
268 .take_pending_backfill_nodes()
269 .collect_vec();
270 if pending_backfill_nodes.is_empty() {
271 None
272 } else {
273 Some(Mutation::StartFragmentBackfill(
274 StartFragmentBackfillMutation {
275 fragment_ids: pending_backfill_nodes,
276 },
277 ))
278 }
279 });
280 let barrier_num_to_inject = resolve_initial_barrier_num_to_inject();
281 pending_upstream_barriers.push(barrier_info.clone());
282 if barrier_num_to_inject == 0 && mutation.is_none() {
285 return vec![];
286 }
287 vec![(
288 CreatingStreamingJobStatus::new_fake_barrier(
289 prev_epoch_fake_physical_time,
290 pending_non_checkpoint_barriers,
291 match barrier_info.kind {
292 BarrierKind::Barrier => PbBarrierKind::Barrier,
293 BarrierKind::Checkpoint(_) => PbBarrierKind::Checkpoint,
294 BarrierKind::Initial => {
295 unreachable!("upstream new epoch should not be initial")
296 }
297 },
298 ),
299 mutation,
300 )]
301 }
302 CreatingStreamingJobStatus::ConsumingLogStore {
303 pending_barriers, ..
304 } => {
305 drain_pending_barriers(
309 pending_barriers,
310 barrier_info.clone(),
311 resolve_initial_barrier_num_to_inject(),
312 )
313 .into_iter()
314 .map(|barrier_info| (barrier_info, None))
315 .collect()
316 }
317 CreatingStreamingJobStatus::Finishing { .. }
318 | CreatingStreamingJobStatus::Resetting(..) => vec![],
319 CreatingStreamingJobStatus::PlaceHolder => {
320 unreachable!()
321 }
322 }
323 }
324
325 pub(super) fn new_fake_barrier(
326 prev_epoch_fake_physical_time: &mut u64,
327 pending_non_checkpoint_barriers: &mut Vec<u64>,
328 kind: PbBarrierKind,
329 ) -> BarrierInfo {
330 super::super::new_fake_barrier(
331 prev_epoch_fake_physical_time,
332 pending_non_checkpoint_barriers,
333 kind,
334 )
335 }
336
337 pub(super) fn fragment_infos(&self) -> Option<&HashMap<FragmentId, InflightFragmentInfo>> {
338 match self {
339 CreatingStreamingJobStatus::ConsumingSnapshot { info, .. }
340 | CreatingStreamingJobStatus::ConsumingLogStore { info, .. } => {
341 Some(&info.fragment_infos)
342 }
343 CreatingStreamingJobStatus::Finishing(..)
344 | CreatingStreamingJobStatus::Resetting(..) => None,
345 CreatingStreamingJobStatus::PlaceHolder => {
346 unreachable!()
347 }
348 }
349 }
350
351 pub(super) fn pre_apply_throttle(
352 &mut self,
353 config: &mut ThrottleConfigMap,
354 ) -> Option<Mutation> {
355 let fragment_infos = match self {
356 CreatingStreamingJobStatus::ConsumingSnapshot { info, .. }
357 | CreatingStreamingJobStatus::ConsumingLogStore { info, .. } => {
358 &mut info.fragment_infos
359 }
360 CreatingStreamingJobStatus::Finishing(..)
361 | CreatingStreamingJobStatus::Resetting(..) => return None,
362 CreatingStreamingJobStatus::PlaceHolder => {
363 unreachable!()
364 }
365 };
366
367 extract_throttle_config(config, |fragment_id, stream_node| {
368 if let Some(fragment_info) = fragment_infos.get_mut(&fragment_id) {
369 fragment_info.nodes = stream_node.clone();
370 true
371 } else {
372 false
373 }
374 })
375 }
376}
377
378fn drain_pending_barriers(
379 pending_barriers: &mut VecDeque<BarrierInfo>,
380 new_upstream_barrier: BarrierInfo,
381 barrier_num_to_inject: usize,
382) -> Vec<BarrierInfo> {
383 pending_barriers.push_back(new_upstream_barrier);
384 let barrier_count = pending_barriers.len().min(barrier_num_to_inject);
385 pending_barriers.drain(..barrier_count).collect()
386}
387
388#[cfg(test)]
389mod tests {
390 use risingwave_pb::stream_plan::PbStreamNode;
391
392 use super::*;
393
394 fn barrier(prev_epoch: u64, curr_epoch: u64) -> BarrierInfo {
395 BarrierInfo {
396 prev_epoch: TracedEpoch::new(Epoch(prev_epoch)),
397 curr_epoch: TracedEpoch::new(Epoch(curr_epoch)),
398 kind: BarrierKind::Barrier,
399 }
400 }
401
402 fn epochs(barriers: &[BarrierInfo]) -> Vec<(u64, u64)> {
403 barriers
404 .iter()
405 .map(|barrier| (barrier.prev_epoch(), barrier.curr_epoch()))
406 .collect()
407 }
408
409 #[test]
410 fn test_drain_pending_barriers_with_available_capacity() {
411 let mut pending_barriers = VecDeque::from([barrier(1, 2), barrier(2, 3), barrier(3, 4)]);
412
413 let injected = drain_pending_barriers(&mut pending_barriers, barrier(4, 5), 0);
414 assert!(injected.is_empty());
415 assert_eq!(
416 epochs(pending_barriers.make_contiguous()),
417 vec![(1, 2), (2, 3), (3, 4), (4, 5)]
418 );
419
420 let injected = drain_pending_barriers(&mut pending_barriers, barrier(5, 6), 2);
421 assert_eq!(epochs(&injected), vec![(1, 2), (2, 3)]);
422 assert_eq!(
423 epochs(pending_barriers.make_contiguous()),
424 vec![(3, 4), (4, 5), (5, 6)]
425 );
426
427 let injected = drain_pending_barriers(&mut pending_barriers, barrier(6, 7), 2);
428 assert_eq!(epochs(&injected), vec![(3, 4), (4, 5)]);
429 assert_eq!(
430 epochs(pending_barriers.make_contiguous()),
431 vec![(5, 6), (6, 7)]
432 );
433
434 let injected = drain_pending_barriers(&mut pending_barriers, barrier(7, 8), 2);
435 assert_eq!(epochs(&injected), vec![(5, 6), (6, 7)]);
436 assert_eq!(epochs(pending_barriers.make_contiguous()), vec![(7, 8)]);
437 }
438
439 #[test]
440 fn test_drain_pending_barriers_without_backlog() {
441 let mut pending_barriers = VecDeque::new();
442
443 let injected = drain_pending_barriers(&mut pending_barriers, barrier(1, 2), 100);
444
445 assert_eq!(epochs(&injected), vec![(1, 2)]);
446 assert!(pending_barriers.is_empty());
447 }
448
449 #[tokio::test]
450 async fn test_resetting_skips_barrier_capacity_lookup() {
451 let mut status = CreatingStreamingJobStatus::Resetting(vec![]);
452 let partial_graph_manager =
453 PartialGraphManager::uninitialized(crate::manager::MetaSrvEnv::for_test().await);
454
455 let injected = status.on_new_upstream_epoch(
456 &partial_graph_manager,
457 PartialGraphId::new(1),
458 10,
459 &barrier(1, 2),
460 None,
461 );
462
463 assert!(injected.is_empty());
464 }
465
466 #[test]
467 fn test_pre_apply_throttle_before_merge() {
468 let job_id = risingwave_common::id::JobId::new(1);
469 let fragment_id = FragmentId::new(1);
470 let old_node = PbStreamNode {
471 identity: "old".to_owned(),
472 ..Default::default()
473 };
474 let new_node = PbStreamNode {
475 identity: "new".to_owned(),
476 ..Default::default()
477 };
478 let fragment_infos = HashMap::from([(
479 fragment_id,
480 InflightFragmentInfo {
481 fragment_id,
482 distribution_type: risingwave_meta_model::fragment::DistributionType::Single,
483 fragment_type_mask: Default::default(),
484 vnode_count: 1,
485 nodes: old_node,
486 actors: Default::default(),
487 state_table_ids: Default::default(),
488 },
489 )]);
490 let mut status = CreatingStreamingJobStatus::ConsumingLogStore {
491 tracking_job: TrackingJob::recovered(job_id, &fragment_infos),
492 info: CreatingJobInfo {
493 fragment_infos,
494 upstream_fragment_downstreams: Default::default(),
495 downstreams: Default::default(),
496 snapshot_backfill_upstream_tables: Default::default(),
497 stream_actors: Default::default(),
498 },
499 log_store_progress_tracker: CreateMviewLogStoreProgressTracker::new(
500 std::iter::empty(),
501 0,
502 ),
503 pending_barriers: Default::default(),
504 };
505 let mut config = HashMap::from([(
506 fragment_id,
507 (
508 risingwave_pb::stream_plan::throttle_mutation::ThrottleConfig {
509 rate_limit: Some(1_000),
510 throttle_type: Default::default(),
511 },
512 new_node.clone(),
513 ),
514 )]);
515
516 assert!(status.pre_apply_throttle(&mut config).is_some());
517 assert!(config.is_empty());
518
519 let info = status.start_consume_upstream(&BarrierInfo {
520 prev_epoch: TracedEpoch::new(Epoch(1)),
521 curr_epoch: TracedEpoch::new(Epoch(2)),
522 kind: BarrierKind::Checkpoint(vec![1]),
523 });
524 assert_eq!(info.fragment_infos[&fragment_id].nodes, new_node);
525 }
526}