1mod render;
16
17use std::cmp::max;
18use std::collections::{HashMap, HashSet, VecDeque, hash_map};
19use std::mem::replace;
20use std::ops::Bound::{Excluded, Unbounded};
21use std::sync::atomic::AtomicU32;
22
23use risingwave_common::catalog::TableId;
24use risingwave_common::id::{JobId, SinkId};
25use risingwave_common::metrics::{LabelGuardedHistogram, LabelGuardedIntGauge};
26use risingwave_common::util::epoch::{Epoch, EpochPair};
27use risingwave_common::util::stream_graph_visitor::visit_stream_node_cont;
28use risingwave_meta_model::{WorkerId, streaming_job};
29use risingwave_pb::common::WorkerNode;
30use risingwave_pb::ddl_service::PbBackfillType;
31use risingwave_pb::hummock::HummockVersionStats;
32use risingwave_pb::id::{ActorId, FragmentId, IcebergCompactionTaskId};
33use risingwave_pb::stream_plan::IcebergPkIndexCompactionContext;
34use risingwave_pb::stream_plan::barrier_mutation::Mutation;
35use risingwave_pb::stream_plan::iceberg_pk_index_compaction_context::{Phase, ResolverTaskInput};
36use risingwave_pb::stream_service::BarrierCompleteResponse;
37use tracing::info;
38
39use self::render::{
40 CompactionTransition, IcebergV3FragmentPartition, RenderedResolver,
41 build_compaction_transitions, partition_resolver,
42};
43use super::{
44 BatchRefreshRenderResult, CreatingStreamingJobControl, IndependentCheckpointJob,
45 IndependentCheckpointJobControl, IndependentCheckpointJobStatus, IndependentJobControl,
46 IndependentJobInfo, InitialPartialGraphRequest, RenderedIndependentJobActors,
47 SnapshotPhaseControl, add_initial_partial_graph, build_initial_add_mutation,
48};
49use crate::MetaResult;
50use crate::barrier::command::{
51 IcebergPkIndexCompactionOverwrite, PostCollectCommand, ThrottleConfigMap,
52 UpstreamTableLogEpochs, extract_throttle_config,
53};
54use crate::barrier::context::CreateIndependentStreamingJobCommandInfo;
55use crate::barrier::info::BarrierInfo;
56use crate::barrier::partial_graph::{
57 CollectedBarrier, PartialGraphBarrierInfo, PartialGraphManager, PartialGraphRecoverer,
58 PartialGraphStat,
59};
60use crate::barrier::progress::TrackingJob;
61use crate::barrier::rpc::{ControlStreamManager, to_partial_graph_id};
62use crate::barrier::{BackfillProgress, BarrierKind, FragmentBackfillProgress, TracedEpoch};
63use crate::controller::fragment::InflightFragmentInfo;
64use crate::controller::scale::LoadedFragment;
65use crate::manager::iceberg_pk_index_sink::CompactionOverwrite;
66use crate::model::{FragmentDownstreamRelation, StreamActor};
67use crate::notification::{CollectionNotifier, NotifierStarter};
68use crate::rpc::metrics::GLOBAL_META_METRICS;
69use crate::stream::ExtendedFragmentBackfillOrder;
70
71struct IcebergV3BarrierStats {
72 consuming_snapshot_barrier_latency: LabelGuardedHistogram,
73 consuming_log_store_barrier_latency: LabelGuardedHistogram,
74 inflight_barrier_num: LabelGuardedIntGauge,
75 snapshot_epoch: u64,
76}
77
78fn synthetic_compaction_epoch(prev_epoch: Epoch, curr_epoch: Epoch) -> Epoch {
79 let synthetic_physical_time = prev_epoch
80 .physical_time()
81 .checked_add(1)
82 .expect("synthetic compaction epoch should not overflow");
83 let synthetic = Epoch::from_physical_time(synthetic_physical_time);
84 assert!(
87 synthetic > prev_epoch && synthetic < curr_epoch,
88 "synthetic compaction epoch must be between {} and {}",
89 prev_epoch.0,
90 curr_epoch.0
91 );
92 synthetic
93}
94
95impl IcebergV3BarrierStats {
96 fn new(job_id: JobId, snapshot_epoch: u64) -> Self {
97 let table_id_str = format!("{}", job_id);
98 Self {
99 snapshot_epoch,
100 consuming_snapshot_barrier_latency: GLOBAL_META_METRICS
101 .snapshot_backfill_barrier_latency
102 .with_guarded_label_values(&[table_id_str.as_str(), "consuming_snapshot"]),
103 consuming_log_store_barrier_latency: GLOBAL_META_METRICS
104 .snapshot_backfill_barrier_latency
105 .with_guarded_label_values(&[table_id_str.as_str(), "consuming_log_store"]),
106 inflight_barrier_num: GLOBAL_META_METRICS
107 .snapshot_backfill_inflight_barrier_num
108 .with_guarded_label_values(&[&table_id_str]),
109 }
110 }
111}
112
113impl PartialGraphStat for IcebergV3BarrierStats {
114 fn observe_barrier_latency(&self, epoch: EpochPair, barrier_latency_secs: f64) {
115 let barrier_latency_metrics = if epoch.prev < self.snapshot_epoch {
116 &self.consuming_snapshot_barrier_latency
117 } else {
118 &self.consuming_log_store_barrier_latency
119 };
120 barrier_latency_metrics.observe(barrier_latency_secs);
121 }
122
123 fn observe_barrier_num(&self, inflight_barrier_num: usize, _collected_barrier_num: usize) {
124 self.inflight_barrier_num.set(inflight_barrier_num as _);
125 }
126}
127
128#[derive(Debug)]
129struct IcebergV3Input {
130 phase: IcebergV3InputPhase,
131 pending_upstream_barriers: VecDeque<BarrierInfo>,
134}
135
136#[derive(Debug)]
137enum IcebergV3InputPhase {
138 Snapshot(SnapshotPhaseControl),
139 LogStore { tracking_job: Option<TrackingJob> },
140}
141
142#[derive(Debug)]
143enum IcebergV3JobStatus {
144 Running(IcebergV3Input),
145 Compacting {
146 input: IcebergV3Input,
147 apply: CompactionApply,
148 },
149}
150
151impl IcebergV3JobStatus {
152 fn next_compaction_barriers(
153 &mut self,
154 job_id: JobId,
155 upstream_barrier: &BarrierInfo,
156 ) -> MetaResult<(Vec<BarrierInfo>, BarrierInfo, BarrierInfo)> {
157 match self {
158 IcebergV3JobStatus::Running(input) => {
159 let IcebergV3Input {
160 phase,
161 pending_upstream_barriers,
162 } = input;
163 match phase {
164 IcebergV3InputPhase::Snapshot(snapshot) => {
165 pending_upstream_barriers.push_back(upstream_barrier.clone());
166 let begin = snapshot.next_fake_barrier(&BarrierKind::Checkpoint(vec![]));
167 let end = snapshot.next_fake_barrier(&BarrierKind::Checkpoint(vec![]));
168 Ok((vec![], begin, end))
169 }
170 IcebergV3InputPhase::LogStore { .. } => {
171 if !upstream_barrier.kind.is_checkpoint() {
172 return Err(anyhow::anyhow!(
173 "Iceberg pk-index compaction requires a checkpoint command barrier"
174 )
175 .into());
176 }
177 pending_upstream_barriers.push_back(upstream_barrier.clone());
178
179 let checkpoint_index = pending_upstream_barriers
180 .iter()
181 .position(|barrier| barrier.kind.is_checkpoint())
182 .expect("new upstream barrier should be a checkpoint");
183
184 let barriers_before_compaction = pending_upstream_barriers
185 .drain(..checkpoint_index)
186 .collect();
187 let BarrierInfo {
188 prev_epoch,
189 curr_epoch,
190 kind: BarrierKind::Checkpoint(checkpoint_epochs),
191 } = pending_upstream_barriers
192 .pop_front()
193 .expect("selected checkpoint should exist")
194 else {
195 unreachable!("selected barrier should be a checkpoint")
196 };
197 let synthetic_epoch = TracedEpoch::new(synthetic_compaction_epoch(
198 prev_epoch.value(),
199 curr_epoch.value(),
200 ));
201 let begin_barrier = BarrierInfo {
202 prev_epoch,
203 curr_epoch: synthetic_epoch.clone(),
204 kind: BarrierKind::Checkpoint(checkpoint_epochs),
205 };
206 let end_barrier = BarrierInfo {
207 prev_epoch: synthetic_epoch.clone(),
208 curr_epoch,
209 kind: BarrierKind::Checkpoint(vec![synthetic_epoch.value().0]),
210 };
211
212 Ok((barriers_before_compaction, begin_barrier, end_barrier))
213 }
214 }
215 }
216 IcebergV3JobStatus::Compacting { .. } => Err(anyhow::anyhow!(
217 "Iceberg V3 job {} is already applying compaction",
218 job_id
219 )
220 .into()),
221 }
222 }
223}
224
225#[derive(Debug)]
226struct CompactionApply {
227 notifier: CollectionNotifier,
228 phase: CompactionApplyPhase,
229}
230
231#[derive(Debug)]
232enum CompactionApplyPhase {
233 BeginAck {
236 task_id: IcebergCompactionTaskId,
237 begin_epoch: u64,
238 end_barrier: BarrierInfo,
239 node_actors: HashMap<WorkerId, HashSet<ActorId>>,
241 end_transition: CompactionTransition,
242 overwrite: CompactionOverwrite,
243 },
244 EndCompletion {
247 end_epoch: u64,
248 overwrite: CompactionOverwrite,
249 },
250 EndAck { end_epoch: u64 },
252}
253
254#[derive(Debug)]
255pub(crate) struct IcebergV3RenderResult {
256 active: BatchRefreshRenderResult,
257 active_stream_actors: HashMap<FragmentId, Vec<StreamActor>>,
258 active_downstreams: FragmentDownstreamRelation,
259 resolver: RenderedResolver,
260 partition: IcebergV3FragmentPartition,
261}
262
263impl IcebergV3RenderResult {
264 pub(crate) fn active_fragment_infos(&self) -> &HashMap<FragmentId, InflightFragmentInfo> {
265 &self.active.fragment_infos
266 }
267
268 pub(crate) fn active_downstreams(&self) -> &FragmentDownstreamRelation {
269 &self.active_downstreams
270 }
271
272 pub(crate) fn all_fragment_infos(&self) -> impl Iterator<Item = &InflightFragmentInfo> + '_ {
273 self.active
274 .fragment_infos
275 .values()
276 .chain(std::iter::once(&self.resolver.fragment_info))
277 }
278}
279
280pub(crate) struct IcebergV3JobCheckpointControl {
283 control: IndependentJobControl,
284 status: IcebergV3JobStatus,
285 fragment_infos: HashMap<FragmentId, InflightFragmentInfo>,
286 node_actors: HashMap<WorkerId, HashSet<ActorId>>,
287 max_pending_barrier_num: usize,
288 upstream_lag: risingwave_common::metrics::LabelGuardedIntGauge,
289 active_stream_actors: HashMap<FragmentId, Vec<StreamActor>>,
292 active_downstreams: FragmentDownstreamRelation,
295 resolver: RenderedResolver,
296 partition: IcebergV3FragmentPartition,
297}
298
299impl IcebergV3JobCheckpointControl {
300 pub(crate) fn render_actors_and_build_job_info(
301 fragments: &HashMap<FragmentId, LoadedFragment>,
302 downstreams: &FragmentDownstreamRelation,
303 definition: &str,
304 actor_id_generator: &AtomicU32,
305 worker_nodes: &HashMap<WorkerId, WorkerNode>,
306 control_stream_manager: &ControlStreamManager,
307 database_resource_group: &str,
308 streaming_job_model: &streaming_job::Model,
309 partial_graph_id: risingwave_pb::id::PartialGraphId,
310 ) -> MetaResult<IcebergV3RenderResult> {
311 let rendered = RenderedIndependentJobActors::render(
312 fragments,
313 downstreams,
314 definition,
315 actor_id_generator,
316 worker_nodes,
317 database_resource_group,
318 streaming_job_model,
319 )?;
320 let mut active_downstreams = downstreams.clone();
321 let (active, resolver, partition) = partition_resolver(rendered, &mut active_downstreams)?;
322 let active_stream_actors = active.stream_actors.clone();
323 let active = active.build_job_info(
324 &active_downstreams,
325 partial_graph_id,
326 control_stream_manager,
327 )?;
328 Ok(IcebergV3RenderResult {
329 active,
330 active_stream_actors,
331 active_downstreams,
332 resolver,
333 partition,
334 })
335 }
336
337 pub(crate) fn create<'a>(
338 entry: hash_map::VacantEntry<'a, JobId, IndependentCheckpointJobControl>,
339 create_info: CreateIndependentStreamingJobCommandInfo,
340 notifier: Option<&mut NotifierStarter>,
341 snapshot_backfill_upstream_tables: HashSet<TableId>,
342 snapshot_epoch: u64,
343 version_stat: &HummockVersionStats,
344 term_id: &str,
345 partial_graph_manager: &mut PartialGraphManager,
346 render_result: IcebergV3RenderResult,
347 ) -> MetaResult<&'a mut Self> {
348 let info = create_info.info.clone();
349 let job_id = info.stream_job_fragments.stream_job_id();
350 let database_id = info.streaming_job.database_id();
351 let partial_graph_id = to_partial_graph_id(database_id, Some(job_id));
352
353 let IcebergV3RenderResult {
354 active:
355 BatchRefreshRenderResult {
356 fragment_infos,
357 node_actors,
358 actors_to_create,
359 },
360 active_stream_actors,
361 active_downstreams,
362 resolver,
363 partition,
364 } = render_result;
365 let (snapshot, initial_barrier_info) =
366 SnapshotPhaseControl::for_new_job(snapshot_epoch, &fragment_infos, &info, version_stat);
367 assert!(
368 info.cdc_table_snapshot_splits.is_none(),
369 "should not have cdc backfill for snapshot backfill job"
370 );
371 let initial_mutation = build_initial_add_mutation(
372 &fragment_infos,
373 &info.fragment_backfill_ordering,
374 Default::default(),
375 );
376
377 let max_pending_barrier_num = super::snapshot_backfill_max_pending_barrier_num(
378 &partial_graph_manager.control_stream_manager().env.opts,
379 );
380
381 let iceberg_job = Self {
382 control: IndependentJobControl::new(IndependentJobInfo::from_fragment_infos(
383 database_id,
384 job_id,
385 snapshot_epoch,
386 snapshot_backfill_upstream_tables,
387 fragment_infos
388 .values()
389 .chain(std::iter::once(&resolver.fragment_info)),
390 )),
391 status: IcebergV3JobStatus::Running(IcebergV3Input {
392 phase: IcebergV3InputPhase::Snapshot(snapshot),
393 pending_upstream_barriers: VecDeque::new(),
394 }),
395 fragment_infos,
396 upstream_lag: GLOBAL_META_METRICS
397 .snapshot_backfill_lag
398 .with_guarded_label_values(&[&format!("{}", job_id)]),
399 node_actors,
400 max_pending_barrier_num,
401 active_stream_actors,
402 active_downstreams,
403 resolver,
404 partition,
405 };
406
407 let mut graph_adder = partial_graph_manager.add_partial_graph(
408 partial_graph_id,
409 term_id,
410 IcebergV3BarrierStats::new(job_id, snapshot_epoch),
411 );
412 if let Err(error) = add_initial_partial_graph(
413 &mut graph_adder,
414 partial_graph_id,
415 InitialPartialGraphRequest {
416 node_actors: &iceberg_job.node_actors,
417 state_table_ids: &iceberg_job.control.info.state_table_ids,
418 barrier_info: initial_barrier_info,
419 actors_to_create,
420 mutation: initial_mutation,
421 notifier,
422 create_info,
423 },
424 ) {
425 graph_adder.failed();
426 entry.insert(IndependentCheckpointJobControl::resetting(
427 iceberg_job.control.info.snapshot_backfill_upstream_tables,
428 ));
429 return Err(error);
430 }
431 graph_adder.added();
432
433 let control = entry.insert(IndependentCheckpointJobControl::iceberg_v3(
434 job_id,
435 partial_graph_id,
436 IndependentCheckpointJobStatus::Initial { snapshot_epoch },
437 iceberg_job,
438 ));
439 let Some(IndependentCheckpointJob::IcebergV3(job)) = control.running_mut() else {
440 unreachable!()
441 };
442 Ok(job)
443 }
444
445 fn recover_snapshot_phase(
446 job_id: JobId,
447 snapshot_backfill_upstream_tables: &HashSet<TableId>,
448 upstream_table_log_epochs: &UpstreamTableLogEpochs,
449 snapshot_epoch: u64,
450 committed_epoch: u64,
451 upstream_barrier_info: &BarrierInfo,
452 fragment_infos: &HashMap<FragmentId, InflightFragmentInfo>,
453 backfill_order: &ExtendedFragmentBackfillOrder,
454 version_stat: &HummockVersionStats,
455 ) -> MetaResult<(IcebergV3Input, BarrierInfo)> {
456 let (snapshot, first_barrier_info) = SnapshotPhaseControl::for_recovery(
457 job_id,
458 snapshot_epoch,
459 committed_epoch,
460 fragment_infos,
461 backfill_order,
462 version_stat,
463 );
464 Ok((
465 IcebergV3Input {
466 phase: IcebergV3InputPhase::Snapshot(snapshot),
467 pending_upstream_barriers:
468 CreatingStreamingJobControl::resolve_checkpoint_aligned_upstream_log_barriers(
469 snapshot_backfill_upstream_tables,
470 upstream_table_log_epochs,
471 snapshot_epoch,
472 upstream_barrier_info,
473 )?
474 .into(),
475 },
476 first_barrier_info,
477 ))
478 }
479
480 fn recover_log_store_phase(
481 job_id: JobId,
482 snapshot_backfill_upstream_tables: &HashSet<TableId>,
483 upstream_table_log_epochs: &UpstreamTableLogEpochs,
484 committed_epoch: u64,
485 upstream_barrier_info: &BarrierInfo,
486 ) -> MetaResult<(IcebergV3Input, BarrierInfo)> {
487 let (checkpoint_epoch, upstream_log_epochs) =
488 CreatingStreamingJobControl::resolve_upstream_log_epochs(
489 snapshot_backfill_upstream_tables,
490 upstream_table_log_epochs,
491 committed_epoch,
492 upstream_barrier_info,
493 )?;
494 if checkpoint_epoch != committed_epoch {
495 info!(
496 %job_id,
497 committed_epoch,
498 preceding_checkpoint_epoch = checkpoint_epoch,
499 "recovering Iceberg V3 job from an epoch lagged from upstream"
500 );
501 }
502 let mut pending_upstream_barriers: VecDeque<_> =
503 CreatingStreamingJobControl::compose_upstream_log_barriers(
504 committed_epoch,
505 upstream_log_epochs,
506 upstream_barrier_info,
507 )?
508 .into();
509 let mut first_barrier_info = pending_upstream_barriers
510 .pop_front()
511 .expect("resolved upstream log epochs should not be empty");
512 assert!(first_barrier_info.kind.is_checkpoint());
513 first_barrier_info.kind = BarrierKind::Initial;
514 Ok((
515 IcebergV3Input {
516 phase: IcebergV3InputPhase::LogStore { tracking_job: None },
517 pending_upstream_barriers,
518 },
519 first_barrier_info,
520 ))
521 }
522
523 pub(crate) fn recover(
524 control: IndependentJobControl,
525 upstream_table_log_epochs: &UpstreamTableLogEpochs,
526 upstream_barrier_info: &BarrierInfo,
527 backfill_order: ExtendedFragmentBackfillOrder,
528 version_stat: &HummockVersionStats,
529 initial_mutation: Mutation,
530 term_id: &str,
531 partial_graph_recoverer: &mut PartialGraphRecoverer<'_>,
532 render_result: IcebergV3RenderResult,
533 ) -> MetaResult<Self> {
534 let job_id = control.info.job_id;
535 let partial_graph_id = control.info.partial_graph_id;
536 let snapshot_epoch = control.info.snapshot_epoch;
537 let committed_epoch = control
538 .max_committed_epoch
539 .expect("recovered independent job should have committed");
540 let IcebergV3RenderResult {
541 active:
542 BatchRefreshRenderResult {
543 fragment_infos,
544 node_actors,
545 actors_to_create,
546 },
547 active_stream_actors,
548 active_downstreams,
549 resolver,
550 partition,
551 } = render_result;
552
553 let (status, first_barrier_info) = if committed_epoch < snapshot_epoch {
554 Self::recover_snapshot_phase(
555 job_id,
556 &control.info.snapshot_backfill_upstream_tables,
557 upstream_table_log_epochs,
558 snapshot_epoch,
559 committed_epoch,
560 upstream_barrier_info,
561 &fragment_infos,
562 &backfill_order,
563 version_stat,
564 )?
565 } else {
566 Self::recover_log_store_phase(
567 job_id,
568 &control.info.snapshot_backfill_upstream_tables,
569 upstream_table_log_epochs,
570 committed_epoch,
571 upstream_barrier_info,
572 )?
573 };
574
575 partial_graph_recoverer.recover_graph(
576 partial_graph_id,
577 term_id,
578 initial_mutation,
579 &first_barrier_info,
580 &node_actors,
581 control.info.state_table_ids.iter().copied(),
582 actors_to_create,
583 IcebergV3BarrierStats::new(job_id, snapshot_epoch),
584 )?;
585 let max_pending_barrier_num = super::snapshot_backfill_max_pending_barrier_num(
586 &partial_graph_recoverer.control_stream_manager().env.opts,
587 );
588 Ok(Self {
589 control,
590 status: IcebergV3JobStatus::Running(status),
591 fragment_infos,
592 node_actors,
593 max_pending_barrier_num,
594 upstream_lag: GLOBAL_META_METRICS
595 .snapshot_backfill_lag
596 .with_guarded_label_values(&[&format!("{}", job_id)]),
597 active_stream_actors,
598 active_downstreams,
599 resolver,
600 partition,
601 })
602 }
603
604 pub(crate) fn pinned_upstream_tables(&self) -> &HashSet<TableId> {
605 &self.control.info.snapshot_backfill_upstream_tables
606 }
607
608 pub(crate) fn gen_backfill_progress(&self) -> Option<BackfillProgress> {
609 match &self.status {
610 IcebergV3JobStatus::Running(IcebergV3Input {
611 phase: IcebergV3InputPhase::Snapshot(snapshot),
612 ..
613 })
614 | IcebergV3JobStatus::Compacting {
615 input:
616 IcebergV3Input {
617 phase: IcebergV3InputPhase::Snapshot(snapshot),
618 ..
619 },
620 ..
621 } => {
622 let progress = if snapshot.create_mview_tracker.is_finished() {
623 "Snapshot finished".to_owned()
624 } else {
625 format!(
626 "Snapshot [{}]",
627 snapshot.create_mview_tracker.gen_backfill_progress()
628 )
629 };
630 Some(BackfillProgress {
631 progress,
632 backfill_type: PbBackfillType::SnapshotBackfill,
633 })
634 }
635 IcebergV3JobStatus::Running(IcebergV3Input {
636 phase: IcebergV3InputPhase::LogStore { .. },
637 ..
638 })
639 | IcebergV3JobStatus::Compacting {
640 input:
641 IcebergV3Input {
642 phase: IcebergV3InputPhase::LogStore { .. },
643 ..
644 },
645 ..
646 } => None,
647 }
648 }
649
650 pub(crate) fn gen_fragment_backfill_progress(&self) -> Vec<FragmentBackfillProgress> {
651 match &self.status {
652 IcebergV3JobStatus::Running(IcebergV3Input {
653 phase: IcebergV3InputPhase::Snapshot(snapshot),
654 ..
655 })
656 | IcebergV3JobStatus::Compacting {
657 input:
658 IcebergV3Input {
659 phase: IcebergV3InputPhase::Snapshot(snapshot),
660 ..
661 },
662 ..
663 } => snapshot
664 .create_mview_tracker
665 .collect_fragment_progress(&self.fragment_infos, true),
666 IcebergV3JobStatus::Running(IcebergV3Input {
667 phase: IcebergV3InputPhase::LogStore { .. },
668 ..
669 })
670 | IcebergV3JobStatus::Compacting {
671 input:
672 IcebergV3Input {
673 phase: IcebergV3InputPhase::LogStore { .. },
674 ..
675 },
676 ..
677 } => vec![],
678 }
679 }
680
681 pub(crate) fn fragment_infos(&self) -> &HashMap<FragmentId, InflightFragmentInfo> {
682 &self.fragment_infos
683 }
684
685 pub(crate) fn resolver_fragment_info(&self) -> &InflightFragmentInfo {
686 &self.resolver.fragment_info
687 }
688
689 pub(crate) fn on_new_upstream_barrier(
690 &mut self,
691 partial_graph_manager: &mut PartialGraphManager,
692 barrier_info: &BarrierInfo,
693 mutation: Option<(
694 risingwave_pb::stream_plan::barrier_mutation::Mutation,
695 Option<&mut NotifierStarter>,
696 )>,
697 ) -> MetaResult<()> {
698 match &mut self.status {
699 IcebergV3JobStatus::Running(input) => {
700 let (mut mutation, mut notifier) = mutation
701 .map(|(mutation, notifier)| (Some(mutation), notifier))
702 .unwrap_or_default();
703 let progress_epoch = self
704 .control
705 .max_committed_epoch
706 .map_or(self.control.info.snapshot_epoch, |committed_epoch| {
707 max(committed_epoch, self.control.info.snapshot_epoch)
708 });
709 self.upstream_lag.set(
710 barrier_info
711 .prev_epoch
712 .value()
713 .0
714 .saturating_sub(progress_epoch) as _,
715 );
716 let available = self.max_pending_barrier_num.saturating_sub(
717 partial_graph_manager.pending_barrier_num(self.control.info.partial_graph_id),
718 );
719 input
720 .pending_upstream_barriers
721 .push_back(barrier_info.clone());
722 let barriers_to_inject = match &mut input.phase {
723 IcebergV3InputPhase::Snapshot(snapshot) => {
724 mutation = mutation.or_else(|| snapshot.take_start_backfill_mutation());
725 if available == 0 && mutation.is_none() {
726 vec![]
727 } else {
728 vec![snapshot.next_fake_barrier(&barrier_info.kind)]
729 }
730 }
731 IcebergV3InputPhase::LogStore { .. } => {
732 let barrier_num_to_inject = available.max(usize::from(mutation.is_some()));
733 let barrier_num_to_inject =
734 barrier_num_to_inject.min(input.pending_upstream_barriers.len());
735 input
736 .pending_upstream_barriers
737 .drain(..barrier_num_to_inject)
738 .collect()
739 }
740 };
741
742 for barrier_to_inject in barriers_to_inject {
743 let barrier_mutation = mutation.take();
744 let barrier_notifier = notifier.take();
745 partial_graph_manager.inject_barrier(
746 self.control.info.partial_graph_id,
747 barrier_mutation,
748 &self.node_actors,
749 self.control.info.state_table_ids.iter().copied(),
750 self.node_actors.keys().copied(),
751 None,
752 PartialGraphBarrierInfo::new(
753 PostCollectCommand::barrier(),
754 barrier_to_inject,
755 barrier_notifier,
756 self.control.info.state_table_ids.clone(),
757 ),
758 )?;
759 }
760 }
761 IcebergV3JobStatus::Compacting { input, .. } => {
762 assert!(
763 mutation.is_none(),
764 "active Iceberg compaction must not receive a second mutation"
765 );
766 input
767 .pending_upstream_barriers
768 .push_back(barrier_info.clone());
769 }
770 }
771 Ok(())
772 }
773
774 pub(crate) fn pre_apply_throttle(
775 &mut self,
776 throttle_config: &mut ThrottleConfigMap,
777 ) -> Option<risingwave_pb::stream_plan::barrier_mutation::Mutation> {
778 let mutation = extract_throttle_config(throttle_config, |fragment_id, stream_node| {
779 if let Some(fragment_info) = self.fragment_infos.get_mut(&fragment_id) {
780 fragment_info.nodes = stream_node.clone();
781 true
782 } else {
783 false
784 }
785 });
786 match &self.status {
787 IcebergV3JobStatus::Running(_) => mutation,
788 IcebergV3JobStatus::Compacting { .. } => None,
791 }
792 }
793
794 pub(crate) fn collect(&mut self, collected_barrier: CollectedBarrier<'_>) -> bool {
795 let (input, can_finish_snapshot) = match &mut self.status {
796 IcebergV3JobStatus::Running(input) => (input, true),
797 IcebergV3JobStatus::Compacting { input, .. } => (input, false),
798 };
799 match &mut input.phase {
800 IcebergV3InputPhase::Snapshot(snapshot) => {
801 let progress = collected_barrier
802 .resps
803 .values()
804 .flat_map(|response| &response.create_mview_progress);
805 let snapshot_finished = snapshot.apply_progress(progress);
806 if can_finish_snapshot && snapshot_finished {
810 let IcebergV3InputPhase::Snapshot(mut snapshot) = replace(
811 &mut input.phase,
812 IcebergV3InputPhase::LogStore { tracking_job: None },
813 ) else {
814 unreachable!("snapshot finished outside the snapshot phase")
815 };
816 input
817 .pending_upstream_barriers
818 .push_front(snapshot.finish_snapshot_barrier());
819 input.phase = IcebergV3InputPhase::LogStore {
820 tracking_job: Some(snapshot.create_mview_tracker.into_tracking_job()),
821 };
822 }
823 }
824 IcebergV3InputPhase::LogStore { .. } => {}
825 }
826 false
829 }
830
831 pub(crate) fn start_apply_compaction(
832 &mut self,
833 partial_graph_manager: &mut PartialGraphManager,
834 upstream_barrier: &BarrierInfo,
835 task_id: IcebergCompactionTaskId,
836 overwrite: IcebergPkIndexCompactionOverwrite,
837 notifier: &mut NotifierStarter,
838 ) -> MetaResult<()> {
839 let output_data_file_paths = output_file_paths(&overwrite.output_result.data_files)?;
840 let transitions = build_compaction_transitions(
841 &self.fragment_infos,
842 &self.active_stream_actors,
843 &self.active_downstreams,
844 &self.resolver,
845 &self.partition,
846 self.control.info.partial_graph_id,
847 partial_graph_manager.control_stream_manager(),
848 )?;
849 let (barriers_before_compaction, begin_barrier, end_barrier) =
850 self.next_compaction_barriers(upstream_barrier)?;
851 for barrier in barriers_before_compaction {
852 partial_graph_manager.inject_barrier(
853 self.control.info.partial_graph_id,
854 None, &self.node_actors,
856 self.control.info.state_table_ids.iter().copied(),
857 self.node_actors.keys().copied(),
858 None, PartialGraphBarrierInfo::new(
860 PostCollectCommand::barrier(),
861 barrier,
862 None, self.control.info.state_table_ids.clone(),
864 ),
865 )?;
866 }
867 let sink_id = SinkId::new(self.control.info.job_id.as_raw_id());
868 let resolver_task_input = ResolverTaskInput {
869 output_data_file_paths,
870 input_data_file_paths: overwrite.input_file_paths.clone(),
871 read_snapshot_id: overwrite.read_snapshot_id,
872 };
873 let compaction_overwrite = CompactionOverwrite {
874 sink_id,
875 epoch: end_barrier.prev_epoch(),
876 schema_id: overwrite.output_result.schema_id,
877 partition_spec_id: overwrite.output_result.partition_spec_id,
878 output_files: overwrite.output_result.data_files,
879 input_file_paths: overwrite.input_file_paths,
880 read_snapshot_id: overwrite.read_snapshot_id,
881 };
882 let mut mutation = transitions.begin_compaction.mutation;
883 mutation.iceberg_pk_index_compaction = Some(IcebergPkIndexCompactionContext {
884 sink_id,
885 task_id,
886 phase: Phase::Begin as i32,
887 resolver_task_input: Some(resolver_task_input),
888 });
889 partial_graph_manager.inject_barrier(
890 self.control.info.partial_graph_id,
891 Some(Mutation::Update(mutation)),
892 &transitions.node_actors,
893 self.control.info.state_table_ids.iter().copied(),
894 transitions.node_actors.keys().copied(),
895 Some(transitions.begin_compaction.actors_to_create),
896 PartialGraphBarrierInfo::new(
897 PostCollectCommand::barrier(),
898 begin_barrier.clone(),
899 None,
900 self.control.info.state_table_ids.clone(),
901 ),
902 )?;
903
904 let input = match std::mem::replace(
907 &mut self.status,
908 IcebergV3JobStatus::Running(IcebergV3Input {
909 phase: IcebergV3InputPhase::LogStore { tracking_job: None },
910 pending_upstream_barriers: VecDeque::new(),
911 }),
912 ) {
913 IcebergV3JobStatus::Running(input) => input,
914 IcebergV3JobStatus::Compacting { .. } => {
915 unreachable!("compacting status was rejected before barrier injection")
916 }
917 };
918 self.status = IcebergV3JobStatus::Compacting {
919 input,
920 apply: CompactionApply {
921 notifier: notifier.add_notify(),
922 phase: CompactionApplyPhase::BeginAck {
923 task_id,
924 begin_epoch: begin_barrier.prev_epoch(),
925 end_barrier,
926 node_actors: transitions.node_actors,
927 end_transition: transitions.end_compaction,
928 overwrite: compaction_overwrite,
929 },
930 },
931 };
932 Ok(())
933 }
934
935 fn next_compaction_barriers(
936 &mut self,
937 upstream_barrier: &BarrierInfo,
938 ) -> MetaResult<(Vec<BarrierInfo>, BarrierInfo, BarrierInfo)> {
939 self.status
940 .next_compaction_barriers(self.control.info.job_id, upstream_barrier)
941 }
942
943 #[expect(clippy::type_complexity)]
944 pub(crate) fn start_completing(
945 &mut self,
946 partial_graph_manager: &mut PartialGraphManager,
947 min_upstream_inflight_barrier: Option<u64>,
948 ) -> Option<(
949 u64,
950 HashMap<WorkerId, BarrierCompleteResponse>,
951 PartialGraphBarrierInfo,
952 Option<TrackingJob>,
953 Option<CompactionOverwrite>,
954 )> {
955 let epoch_end_bound = min_upstream_inflight_barrier
956 .map(Excluded)
957 .unwrap_or(Unbounded);
958 let (epoch, responses, info) = partial_graph_manager.start_completing(
959 self.control.info.partial_graph_id,
960 epoch_end_bound,
961 |_non_checkpoint_epoch, _, _| {},
962 )?;
963
964 let tracking_job = match &mut self.status {
965 IcebergV3JobStatus::Running(IcebergV3Input {
966 phase: IcebergV3InputPhase::LogStore { tracking_job },
967 ..
968 })
969 | IcebergV3JobStatus::Compacting {
970 input:
971 IcebergV3Input {
972 phase: IcebergV3InputPhase::LogStore { tracking_job },
973 ..
974 },
975 ..
976 } if epoch == self.control.info.snapshot_epoch => tracking_job.take(),
977 _ => None,
978 };
979
980 let overwrite = match &mut self.status {
981 IcebergV3JobStatus::Running(_) => None,
982 IcebergV3JobStatus::Compacting {
983 apply: CompactionApply { phase, .. },
984 ..
985 } => match phase {
986 CompactionApplyPhase::BeginAck { begin_epoch, .. } => {
987 if epoch <= *begin_epoch {
988 None
989 } else {
990 unreachable!(
991 "collected epoch {epoch} passed unacknowledged compaction Begin epoch {begin_epoch}"
992 )
993 }
994 }
995 CompactionApplyPhase::EndCompletion { end_epoch, .. } => {
996 let end_epoch = *end_epoch;
997 match epoch.cmp(&end_epoch) {
998 std::cmp::Ordering::Less => None,
999 std::cmp::Ordering::Equal => {
1000 let CompactionApplyPhase::EndCompletion { overwrite, .. } =
1001 std::mem::replace(
1002 phase,
1003 CompactionApplyPhase::EndAck { end_epoch },
1004 )
1005 else {
1006 unreachable!()
1007 };
1008 Some(overwrite)
1009 }
1010 std::cmp::Ordering::Greater => unreachable!(
1011 "collected epoch {epoch} passed compaction End epoch {end_epoch}"
1012 ),
1013 }
1014 }
1015 CompactionApplyPhase::EndAck { end_epoch } => unreachable!(
1016 "started another completion while compaction End epoch {end_epoch} is committing"
1017 ),
1018 },
1019 };
1020 Some((epoch, responses, info, tracking_job, overwrite))
1021 }
1022
1023 pub(crate) fn ack_completed(
1024 &mut self,
1025 partial_graph_manager: &mut PartialGraphManager,
1026 completed_epoch: u64,
1027 ) -> MetaResult<()> {
1028 match &mut self.status {
1029 IcebergV3JobStatus::Running(input) => {
1030 partial_graph_manager
1031 .ack_completed(self.control.info.partial_graph_id, completed_epoch);
1032 self.control.ack_completed(completed_epoch);
1033 if completed_epoch == self.control.info.snapshot_epoch {
1034 let IcebergV3InputPhase::LogStore { tracking_job } = &input.phase else {
1035 unreachable!("snapshot epoch completed outside the log-store phase")
1036 };
1037 assert!(
1038 tracking_job.is_none(),
1039 "tracking job should have been taken at start_completing"
1040 );
1041 }
1042 }
1043 IcebergV3JobStatus::Compacting {
1044 apply:
1045 CompactionApply {
1046 phase: CompactionApplyPhase::BeginAck { begin_epoch, .. },
1047 ..
1048 },
1049 ..
1050 } if completed_epoch < *begin_epoch => {
1051 partial_graph_manager
1052 .ack_completed(self.control.info.partial_graph_id, completed_epoch);
1053 self.control.ack_completed(completed_epoch);
1054 }
1055 IcebergV3JobStatus::Compacting {
1056 apply:
1057 CompactionApply {
1058 phase:
1059 CompactionApplyPhase::BeginAck {
1060 task_id,
1061 begin_epoch,
1062 end_barrier,
1063 node_actors,
1064 end_transition,
1065 ..
1066 },
1067 ..
1068 },
1069 ..
1070 } if completed_epoch == *begin_epoch => {
1071 partial_graph_manager
1072 .ack_completed(self.control.info.partial_graph_id, completed_epoch);
1073 self.control.ack_completed(completed_epoch);
1074
1075 let end_epoch = end_barrier.prev_epoch();
1076 let sink_id = SinkId::new(self.control.info.job_id.as_raw_id());
1077 let mut mutation = end_transition.mutation.clone();
1078 mutation.iceberg_pk_index_compaction = Some(IcebergPkIndexCompactionContext {
1079 sink_id,
1080 task_id: *task_id,
1081 phase: Phase::End as i32,
1082 resolver_task_input: None,
1083 });
1084 partial_graph_manager.inject_barrier(
1085 self.control.info.partial_graph_id,
1086 Some(Mutation::Update(mutation)),
1087 node_actors,
1088 self.control.info.state_table_ids.iter().copied(),
1089 node_actors.keys().copied(),
1090 Some(end_transition.actors_to_create.clone()),
1091 PartialGraphBarrierInfo::new(
1092 PostCollectCommand::barrier(),
1093 end_barrier.clone(),
1094 None,
1095 self.control.info.state_table_ids.clone(),
1096 ),
1097 )?;
1098
1099 let IcebergV3JobStatus::Compacting { input, apply } = std::mem::replace(
1102 &mut self.status,
1103 IcebergV3JobStatus::Running(IcebergV3Input {
1104 phase: IcebergV3InputPhase::LogStore { tracking_job: None },
1105 pending_upstream_barriers: VecDeque::new(),
1106 }),
1107 ) else {
1108 unreachable!()
1109 };
1110 let CompactionApplyPhase::BeginAck { overwrite, .. } = apply.phase else {
1111 unreachable!()
1112 };
1113 self.status = IcebergV3JobStatus::Compacting {
1114 input,
1115 apply: CompactionApply {
1116 notifier: apply.notifier,
1117 phase: CompactionApplyPhase::EndCompletion {
1118 end_epoch,
1119 overwrite,
1120 },
1121 },
1122 };
1123 }
1124 IcebergV3JobStatus::Compacting {
1125 apply:
1126 CompactionApply {
1127 phase: CompactionApplyPhase::BeginAck { begin_epoch, .. },
1128 ..
1129 },
1130 ..
1131 } => unreachable!(
1132 "acknowledged epoch {completed_epoch} after compaction Begin epoch {begin_epoch}"
1133 ),
1134 IcebergV3JobStatus::Compacting {
1135 apply:
1136 CompactionApply {
1137 phase: CompactionApplyPhase::EndCompletion { end_epoch, .. },
1138 ..
1139 },
1140 ..
1141 } => unreachable!(
1142 "acknowledged epoch {completed_epoch} before compaction End epoch {end_epoch} entered the completion task"
1143 ),
1144 IcebergV3JobStatus::Compacting {
1145 apply:
1146 CompactionApply {
1147 phase: CompactionApplyPhase::EndAck { end_epoch },
1148 ..
1149 },
1150 ..
1151 } if completed_epoch < *end_epoch => {
1152 partial_graph_manager
1153 .ack_completed(self.control.info.partial_graph_id, completed_epoch);
1154 self.control.ack_completed(completed_epoch);
1155 }
1156 IcebergV3JobStatus::Compacting {
1157 apply:
1158 CompactionApply {
1159 phase: CompactionApplyPhase::EndAck { end_epoch },
1160 ..
1161 },
1162 ..
1163 } if completed_epoch == *end_epoch => {
1164 partial_graph_manager
1165 .ack_completed(self.control.info.partial_graph_id, completed_epoch);
1166 self.control.ack_completed(completed_epoch);
1167 let IcebergV3JobStatus::Compacting { input, apply } = std::mem::replace(
1168 &mut self.status,
1169 IcebergV3JobStatus::Running(IcebergV3Input {
1170 phase: IcebergV3InputPhase::LogStore { tracking_job: None },
1171 pending_upstream_barriers: VecDeque::new(),
1172 }),
1173 ) else {
1174 unreachable!()
1175 };
1176 self.status = IcebergV3JobStatus::Running(input);
1177 apply.notifier.notify_collected();
1178 }
1179 IcebergV3JobStatus::Compacting {
1180 apply:
1181 CompactionApply {
1182 phase: CompactionApplyPhase::EndAck { end_epoch },
1183 ..
1184 },
1185 ..
1186 } => unreachable!(
1187 "acknowledged epoch {completed_epoch} after compaction End epoch {end_epoch}"
1188 ),
1189 }
1190 Ok(())
1191 }
1192}
1193
1194fn output_file_paths(files: &[iceberg::spec::SerializedDataFile]) -> MetaResult<Vec<String>> {
1195 files
1196 .iter()
1197 .map(|file| {
1198 let value = serde_json::to_value(file).map_err(anyhow::Error::from)?;
1199 value
1200 .get("file_path")
1201 .and_then(|path| path.as_str())
1202 .map(str::to_owned)
1203 .ok_or_else(|| {
1204 anyhow::anyhow!("compaction output file is missing file_path").into()
1205 })
1206 })
1207 .collect()
1208}
1209
1210pub(crate) fn is_iceberg_v3_fragment_nodes<'a>(
1211 nodes: impl IntoIterator<Item = &'a risingwave_pb::stream_plan::PbStreamNode>,
1212) -> bool {
1213 nodes.into_iter().any(|root| {
1214 let mut found = false;
1215 visit_stream_node_cont(root, |node| {
1216 if matches!(
1217 node.node_body,
1218 Some(
1219 risingwave_pb::stream_plan::stream_node::NodeBody::IcebergWithPkIndexWriter(_)
1220 )
1221 ) {
1222 found = true;
1223 }
1224 !found
1225 });
1226 found
1227 })
1228}
1229
1230#[cfg(test)]
1231mod tests {
1232 use risingwave_common::util::epoch::EPOCH_SPILL_TIME_MASK;
1233
1234 use super::*;
1235
1236 fn barrier_info(prev_physical: u64, curr_physical: u64, kind: BarrierKind) -> BarrierInfo {
1237 BarrierInfo {
1238 prev_epoch: TracedEpoch::new(Epoch::from_physical_time(prev_physical)),
1239 curr_epoch: TracedEpoch::new(Epoch::from_physical_time(curr_physical)),
1240 kind,
1241 }
1242 }
1243
1244 fn assert_epoch(barrier: &BarrierInfo, prev_physical: u64, curr_physical: u64) {
1245 assert_eq!(
1246 barrier.epoch(),
1247 EpochPair::new(
1248 Epoch::from_physical_time(curr_physical).0,
1249 Epoch::from_physical_time(prev_physical).0,
1250 )
1251 );
1252 }
1253
1254 #[test]
1255 fn test_synthetic_compaction_epoch() {
1256 let prev_epoch = Epoch::from_physical_time(10);
1257 let curr_epoch = Epoch::from_physical_time(12);
1258
1259 let synthetic = synthetic_compaction_epoch(prev_epoch, curr_epoch);
1260
1261 assert_eq!(synthetic, Epoch::from_physical_time(11));
1262 assert_eq!(synthetic.0 & EPOCH_SPILL_TIME_MASK, 0);
1263 assert!(prev_epoch < synthetic);
1264 assert!(synthetic < curr_epoch);
1265 }
1266
1267 #[test]
1268 #[should_panic(expected = "synthetic compaction epoch must be between")]
1269 fn test_synthetic_compaction_epoch_rejects_tight_gap() {
1270 let prev_epoch = Epoch::from_physical_time(10);
1271 let curr_epoch = Epoch::from_physical_time(11);
1272
1273 synthetic_compaction_epoch(prev_epoch, curr_epoch);
1274 }
1275
1276 #[test]
1277 fn test_recover_log_store_phase_from_synthetic_epoch() {
1278 let table_id = TableId::new(1);
1279 let upstream_tables = HashSet::from([table_id]);
1280 let upstream_log_epochs = HashMap::from([(
1281 table_id,
1282 vec![
1283 (vec![], Epoch::from_physical_time(10).0),
1284 (vec![], Epoch::from_physical_time(12).0),
1285 ],
1286 )]);
1287 let upstream_barrier = barrier_info(12, 14, BarrierKind::Checkpoint(vec![]));
1288
1289 let (input, initial_barrier) = IcebergV3JobCheckpointControl::recover_log_store_phase(
1290 JobId::new(1),
1291 &upstream_tables,
1292 &upstream_log_epochs,
1293 Epoch::from_physical_time(11).0,
1294 &upstream_barrier,
1295 )
1296 .unwrap();
1297
1298 assert_epoch(&initial_barrier, 11, 12);
1299 assert_eq!(initial_barrier.kind, BarrierKind::Initial);
1300 let IcebergV3Input {
1301 phase: IcebergV3InputPhase::LogStore { tracking_job },
1302 pending_upstream_barriers,
1303 } = input
1304 else {
1305 unreachable!()
1306 };
1307 assert!(tracking_job.is_none());
1308 assert_eq!(pending_upstream_barriers.len(), 1);
1309 assert_epoch(&pending_upstream_barriers[0], 12, 14);
1310 }
1311
1312 #[test]
1313 fn test_log_store_compaction_uses_first_pending_checkpoint() {
1314 let mut status = IcebergV3JobStatus::Running(IcebergV3Input {
1315 phase: IcebergV3InputPhase::LogStore { tracking_job: None },
1316 pending_upstream_barriers: VecDeque::from([
1317 barrier_info(10, 12, BarrierKind::Barrier),
1318 barrier_info(12, 14, BarrierKind::Checkpoint(vec![10])),
1319 barrier_info(14, 16, BarrierKind::Barrier),
1320 ]),
1321 });
1322 let upstream = barrier_info(16, 18, BarrierKind::Checkpoint(vec![14]));
1323
1324 let (before, begin, end) = status
1325 .next_compaction_barriers(JobId::new(1), &upstream)
1326 .unwrap();
1327
1328 assert_eq!(before.len(), 1);
1329 assert_epoch(&before[0], 10, 12);
1330 assert_epoch(&begin, 12, 13);
1331 assert_eq!(begin.kind, BarrierKind::Checkpoint(vec![10]));
1332 assert_epoch(&end, 13, 14);
1333 assert_eq!(
1334 end.kind,
1335 BarrierKind::Checkpoint(vec![Epoch::from_physical_time(13).0])
1336 );
1337 let IcebergV3JobStatus::Running(IcebergV3Input {
1338 pending_upstream_barriers: pending,
1339 ..
1340 }) = status
1341 else {
1342 unreachable!()
1343 };
1344 assert_eq!(pending.len(), 2);
1345 assert_epoch(&pending[0], 14, 16);
1346 assert_epoch(&pending[1], 16, 18);
1347 }
1348}