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