1use std::collections::{HashMap, HashSet};
16use std::mem::take;
17
18use risingwave_common::catalog::{DatabaseId, TableId};
19use risingwave_common::id::JobId;
20use risingwave_common::util::epoch::Epoch;
21use risingwave_meta_model::WorkerId;
22use risingwave_pb::hummock::HummockVersionStats;
23use risingwave_pb::id::{ActorId, FragmentId, PartialGraphId};
24use risingwave_pb::source::ConnectorSplits;
25use risingwave_pb::stream_plan::barrier::PbBarrierKind;
26use risingwave_pb::stream_plan::barrier_mutation::Mutation;
27use risingwave_pb::stream_plan::{
28 AddMutation, PbSubscriptionUpstreamInfo, StartFragmentBackfillMutation,
29};
30use risingwave_pb::stream_service::barrier_complete_response::CreateMviewProgress;
31
32pub(crate) mod batch_refresh_job;
33pub(crate) mod creating_job;
34pub(crate) mod iceberg_v3_job;
35pub(crate) use batch_refresh_job::{
36 BatchRefreshJobCheckpointControl, BatchRefreshJobTriggerContext, BatchRefreshLogicalFragments,
37 BatchRefreshRenderResult, RenderedIndependentJobActors,
38};
39pub(crate) use creating_job::CreatingStreamingJobControl;
40pub(crate) use iceberg_v3_job::{
41 IcebergV3JobCheckpointControl, IcebergV3RenderResult, is_iceberg_v3_fragment_nodes,
42};
43
44use crate::MetaResult;
45use crate::barrier::backfill_order_control::get_nodes_with_backfill_dependencies;
46use crate::barrier::command::{CreateStreamingJobCommandInfo, ThrottleConfigMap};
47use crate::barrier::context::CreateIndependentStreamingJobCommandInfo;
48use crate::barrier::info::BarrierInfo;
49use crate::barrier::partial_graph::{
50 CollectedBarrier, PartialGraphAdder, PartialGraphBarrierInfo, PartialGraphManager,
51};
52use crate::barrier::progress::CreateMviewProgressTracker;
53use crate::barrier::rpc::to_partial_graph_id;
54use crate::barrier::{
55 BackfillOrderState, BackfillProgress, BarrierKind, FragmentBackfillProgress, TracedEpoch,
56};
57use crate::controller::fragment::InflightFragmentInfo;
58use crate::manager::MetaOpts;
59use crate::model::StreamJobActorsToCreate;
60use crate::notification::{CollectionNotifier, NotifierStarter};
61use crate::stream::ExtendedFragmentBackfillOrder;
62
63#[derive(Debug)]
64struct SnapshotPhaseControl {
65 snapshot_epoch: u64,
67 prev_epoch_fake_physical_time: u64,
68 version_stats: HummockVersionStats,
69 create_mview_tracker: CreateMviewProgressTracker,
70 pending_non_checkpoint_barriers: Vec<u64>,
71}
72
73impl SnapshotPhaseControl {
74 fn for_new_job(
75 snapshot_epoch: u64,
76 fragment_infos: &HashMap<FragmentId, InflightFragmentInfo>,
77 create_info: &CreateStreamingJobCommandInfo,
78 version_stats: &HummockVersionStats,
79 ) -> (Self, BarrierInfo) {
80 let job_id = create_info.stream_job_fragments.stream_job_id();
81 let backfill_order_state = BackfillOrderState::new(
82 &create_info.fragment_backfill_ordering,
83 fragment_infos,
84 create_info.locality_fragment_state_table_mapping.clone(),
85 );
86 let create_mview_tracker = CreateMviewProgressTracker::recover(
87 job_id,
88 fragment_infos,
89 backfill_order_state,
90 version_stats,
91 );
92 let mut snapshot = Self {
93 snapshot_epoch,
94 prev_epoch_fake_physical_time: 0,
95 version_stats: version_stats.clone(),
96 create_mview_tracker,
97 pending_non_checkpoint_barriers: vec![],
98 };
99 let initial_barrier_info = new_fake_barrier(
100 &mut snapshot.prev_epoch_fake_physical_time,
101 &mut snapshot.pending_non_checkpoint_barriers,
102 PbBarrierKind::Checkpoint,
103 );
104 assert!(snapshot.pending_non_checkpoint_barriers.is_empty());
105 (snapshot, initial_barrier_info)
106 }
107
108 fn for_recovery(
109 job_id: JobId,
110 snapshot_epoch: u64,
111 committed_epoch: u64,
112 fragment_infos: &HashMap<FragmentId, InflightFragmentInfo>,
113 backfill_order: &ExtendedFragmentBackfillOrder,
114 version_stats: &HummockVersionStats,
115 ) -> (Self, BarrierInfo) {
116 let backfill_order_state =
117 BackfillOrderState::recover_from_fragment_infos(backfill_order, fragment_infos);
118 let create_mview_tracker = CreateMviewProgressTracker::recover(
119 job_id,
120 fragment_infos,
121 backfill_order_state,
122 version_stats,
123 );
124 let mut snapshot = Self {
125 snapshot_epoch,
126 prev_epoch_fake_physical_time: Epoch(committed_epoch).physical_time(),
127 version_stats: version_stats.clone(),
128 create_mview_tracker,
129 pending_non_checkpoint_barriers: vec![],
130 };
131 let initial_barrier_info = new_fake_barrier(
132 &mut snapshot.prev_epoch_fake_physical_time,
133 &mut snapshot.pending_non_checkpoint_barriers,
134 PbBarrierKind::Initial,
135 );
136 assert!(snapshot.pending_non_checkpoint_barriers.is_empty());
137 (snapshot, initial_barrier_info)
138 }
139
140 fn apply_progress<'a>(
141 &mut self,
142 progress: impl IntoIterator<Item = &'a CreateMviewProgress>,
143 ) -> bool {
144 for progress in progress {
145 self.create_mview_tracker
146 .apply_progress(progress, &self.version_stats);
147 }
148 self.create_mview_tracker.is_finished()
149 }
150
151 fn take_start_backfill_mutation(&mut self) -> Option<Mutation> {
152 let fragment_ids = self
153 .create_mview_tracker
154 .take_pending_backfill_nodes()
155 .collect::<Vec<_>>();
156 (!fragment_ids.is_empty()).then_some(Mutation::StartFragmentBackfill(
157 StartFragmentBackfillMutation { fragment_ids },
158 ))
159 }
160
161 fn next_fake_barrier(&mut self, upstream_kind: &BarrierKind) -> BarrierInfo {
162 let kind = match upstream_kind {
163 BarrierKind::Barrier => PbBarrierKind::Barrier,
164 BarrierKind::Checkpoint(_) => PbBarrierKind::Checkpoint,
165 BarrierKind::Initial => unreachable!("upstream new epoch should not be initial"),
166 };
167 new_fake_barrier(
168 &mut self.prev_epoch_fake_physical_time,
169 &mut self.pending_non_checkpoint_barriers,
170 kind,
171 )
172 }
173
174 fn finish_snapshot_barrier(&mut self) -> BarrierInfo {
175 self.pending_non_checkpoint_barriers
176 .push(self.snapshot_epoch);
177 BarrierInfo {
178 curr_epoch: TracedEpoch::new(Epoch(self.snapshot_epoch)),
179 prev_epoch: TracedEpoch::new(Epoch::from_physical_time(
180 self.prev_epoch_fake_physical_time,
181 )),
182 kind: BarrierKind::Checkpoint(take(&mut self.pending_non_checkpoint_barriers)),
183 }
184 }
185}
186
187fn build_initial_add_mutation(
188 fragment_infos: &HashMap<FragmentId, InflightFragmentInfo>,
189 backfill_ordering: &ExtendedFragmentBackfillOrder,
190 actor_splits: HashMap<ActorId, ConnectorSplits>,
191) -> Mutation {
192 Mutation::Add(AddMutation {
193 actor_dispatchers: Default::default(),
194 added_actors: fragment_infos
195 .values()
196 .flat_map(|fragment| fragment.actors.keys().copied())
197 .collect(),
198 actor_splits,
199 pause: false,
200 subscriptions_to_add: Default::default(),
201 backfill_nodes_to_pause: get_nodes_with_backfill_dependencies(backfill_ordering)
202 .into_iter()
203 .collect(),
204 actor_cdc_table_snapshot_splits: None,
205 new_upstream_sinks: Default::default(),
206 dropped_actors: Default::default(),
207 sink_log_store_flush: Default::default(),
208 })
209}
210
211struct InitialPartialGraphRequest<'a> {
212 node_actors: &'a HashMap<WorkerId, HashSet<ActorId>>,
213 state_table_ids: &'a HashSet<TableId>,
214 barrier_info: BarrierInfo,
215 actors_to_create: StreamJobActorsToCreate,
216 mutation: Mutation,
217 notifier: Option<&'a mut NotifierStarter>,
218 create_info: CreateIndependentStreamingJobCommandInfo,
219}
220
221fn add_initial_partial_graph(
222 graph_adder: &mut PartialGraphAdder<'_>,
223 partial_graph_id: PartialGraphId,
224 request: InitialPartialGraphRequest<'_>,
225) -> MetaResult<()> {
226 let InitialPartialGraphRequest {
227 node_actors,
228 state_table_ids,
229 barrier_info,
230 actors_to_create,
231 mutation,
232 notifier,
233 create_info,
234 } = request;
235 graph_adder.manager().inject_barrier(
236 partial_graph_id,
237 Some(mutation),
238 node_actors,
239 state_table_ids.iter().copied(),
240 node_actors.keys().copied(),
241 Some(actors_to_create),
242 PartialGraphBarrierInfo::new(
243 create_info.into_post_collect(),
244 barrier_info,
245 notifier,
246 state_table_ids.clone(),
247 ),
248 )
249}
250
251fn snapshot_backfill_max_pending_barrier_num(opts: &MetaOpts) -> usize {
252 opts.in_flight_barrier_nums
253 .saturating_mul(opts.snapshot_backfill_barrier_amplification_factor.max(1))
254}
255
256#[derive(Debug)]
257pub(crate) struct IndependentJobInfo {
258 pub(crate) job_id: JobId,
259 pub(crate) partial_graph_id: PartialGraphId,
260 pub(crate) snapshot_backfill_upstream_tables: HashSet<TableId>,
261 pub(crate) snapshot_epoch: u64,
262 pub(crate) state_table_ids: HashSet<TableId>,
263}
264
265impl IndependentJobInfo {
266 pub(crate) fn from_fragment_infos<'a>(
267 database_id: DatabaseId,
268 job_id: JobId,
269 snapshot_epoch: u64,
270 snapshot_backfill_upstream_tables: HashSet<TableId>,
271 fragment_infos: impl IntoIterator<Item = &'a InflightFragmentInfo> + 'a,
272 ) -> Self {
273 Self {
274 job_id,
275 partial_graph_id: to_partial_graph_id(database_id, Some(job_id)),
276 snapshot_backfill_upstream_tables,
277 snapshot_epoch,
278 state_table_ids: InflightFragmentInfo::existing_table_ids(fragment_infos).collect(),
279 }
280 }
281}
282
283#[derive(Debug)]
284pub(crate) struct IndependentJobControl {
285 pub(crate) info: IndependentJobInfo,
287 pub(crate) max_committed_epoch: Option<u64>,
289}
290
291impl IndependentJobControl {
292 pub(crate) fn new(info: IndependentJobInfo) -> Self {
293 Self {
294 info,
295 max_committed_epoch: None,
296 }
297 }
298
299 pub(crate) fn recovered(info: IndependentJobInfo, committed_epoch: u64) -> Self {
300 Self {
301 info,
302 max_committed_epoch: Some(committed_epoch),
303 }
304 }
305
306 pub(crate) fn ack_completed(&mut self, completed_epoch: u64) {
307 if let Some(previous_epoch) = self.max_committed_epoch.replace(completed_epoch) {
308 assert!(completed_epoch > previous_epoch);
309 }
310 }
311}
312
313fn new_fake_barrier(
317 prev_epoch_fake_physical_time: &mut u64,
318 pending_non_checkpoint_barriers: &mut Vec<u64>,
319 kind: PbBarrierKind,
320) -> BarrierInfo {
321 let prev_epoch = TracedEpoch::new(Epoch::from_physical_time(*prev_epoch_fake_physical_time));
322 *prev_epoch_fake_physical_time += 1;
323 let curr_epoch = TracedEpoch::new(Epoch::from_physical_time(*prev_epoch_fake_physical_time));
324 let kind = match kind {
325 PbBarrierKind::Unspecified => unreachable!(),
326 PbBarrierKind::Initial => {
327 assert!(pending_non_checkpoint_barriers.is_empty());
328 BarrierKind::Initial
329 }
330 PbBarrierKind::Barrier => {
331 pending_non_checkpoint_barriers.push(prev_epoch.value().0);
332 BarrierKind::Barrier
333 }
334 PbBarrierKind::Checkpoint => {
335 pending_non_checkpoint_barriers.push(prev_epoch.value().0);
336 BarrierKind::Checkpoint(take(pending_non_checkpoint_barriers))
337 }
338 };
339 BarrierInfo {
340 prev_epoch,
341 curr_epoch,
342 kind,
343 }
344}
345
346pub(crate) enum IndependentCheckpointJob {
351 CreatingStreamingJob(CreatingStreamingJobControl),
352 BatchRefresh(BatchRefreshJobCheckpointControl),
353 IcebergV3(IcebergV3JobCheckpointControl),
354}
355
356#[derive(Debug, PartialEq, Eq)]
357pub(crate) enum IndependentCheckpointJobStatus {
358 Initial { snapshot_epoch: u64 },
361 Ready,
363}
364
365pub(crate) enum IndependentCheckpointJobControl {
367 Running {
368 status: IndependentCheckpointJobStatus,
369 job_id: JobId,
370 partial_graph_id: PartialGraphId,
371 job: IndependentCheckpointJob,
372 },
373 Resetting {
374 pinned_upstream_tables: HashSet<TableId>,
377 subscriptions_to_drop: Vec<PbSubscriptionUpstreamInfo>,
378 notifiers: Vec<CollectionNotifier>,
379 },
380}
381
382impl IndependentCheckpointJob {
383 fn can_drop_independently(&self) -> bool {
384 match self {
385 Self::CreatingStreamingJob(j) => j.can_drop_independently(),
386 Self::BatchRefresh(_) => true,
387 Self::IcebergV3(_) => true,
388 }
389 }
390
391 fn can_drop(&self) -> bool {
392 match self {
393 Self::CreatingStreamingJob(j) => j.can_drop(),
394 Self::BatchRefresh(_) => true,
395 Self::IcebergV3(_) => true,
396 }
397 }
398
399 fn pinned_upstream_tables(&self) -> &HashSet<TableId> {
400 match self {
401 Self::CreatingStreamingJob(j) => j.pinned_upstream_tables(),
402 Self::BatchRefresh(j) => j.pinned_upstream_tables(),
403 Self::IcebergV3(j) => j.pinned_upstream_tables(),
404 }
405 }
406
407 pub(crate) fn pre_apply_throttle(
408 &mut self,
409 config: &mut ThrottleConfigMap,
410 ) -> Option<Mutation> {
411 match self {
412 Self::CreatingStreamingJob(job) => job.pre_apply_throttle(config),
413 Self::BatchRefresh(job) => job.pre_apply_throttle(config),
414 Self::IcebergV3(job) => job.pre_apply_throttle(config),
415 }
416 }
417
418 pub(crate) fn on_new_upstream_barrier(
419 &mut self,
420 partial_graph_manager: &mut PartialGraphManager,
421 barrier_info: &BarrierInfo,
422 mutation: Option<(Mutation, Option<&mut NotifierStarter>)>,
423 ) -> MetaResult<()> {
424 match self {
425 Self::CreatingStreamingJob(job) => {
426 job.on_new_upstream_barrier(partial_graph_manager, barrier_info, mutation)
427 }
428 Self::BatchRefresh(job) => {
429 job.on_new_upstream_barrier(partial_graph_manager, barrier_info, mutation)
430 }
431 Self::IcebergV3(job) => {
432 job.on_new_upstream_barrier(partial_graph_manager, barrier_info, mutation)
433 }
434 }
435 }
436}
437
438impl IndependentCheckpointJobStatus {
439 fn on_upstream_database_ack_completed(&mut self, committed_epoch: u64) {
440 if let Self::Initial { snapshot_epoch } = self
441 && committed_epoch >= *snapshot_epoch
442 {
443 *self = Self::Ready;
444 }
445 }
446}
447
448impl IndependentCheckpointJobControl {
449 pub(crate) fn resetting(pinned_upstream_tables: HashSet<TableId>) -> Self {
450 Self::Resetting {
451 pinned_upstream_tables,
452 subscriptions_to_drop: vec![],
453 notifiers: vec![],
454 }
455 }
456
457 pub(crate) fn creating_streaming_job(
458 job_id: JobId,
459 partial_graph_id: PartialGraphId,
460 status: IndependentCheckpointJobStatus,
461 job: CreatingStreamingJobControl,
462 ) -> Self {
463 Self::Running {
464 status,
465 job_id,
466 partial_graph_id,
467 job: IndependentCheckpointJob::CreatingStreamingJob(job),
468 }
469 }
470
471 pub(crate) fn batch_refresh(
472 job_id: JobId,
473 partial_graph_id: PartialGraphId,
474 status: IndependentCheckpointJobStatus,
475 job: BatchRefreshJobCheckpointControl,
476 ) -> Self {
477 Self::Running {
478 status,
479 job_id,
480 partial_graph_id,
481 job: IndependentCheckpointJob::BatchRefresh(job),
482 }
483 }
484
485 pub(crate) fn iceberg_v3(
486 job_id: JobId,
487 partial_graph_id: PartialGraphId,
488 status: IndependentCheckpointJobStatus,
489 job: IcebergV3JobCheckpointControl,
490 ) -> Self {
491 Self::Running {
492 status,
493 job_id,
494 partial_graph_id,
495 job: IndependentCheckpointJob::IcebergV3(job),
496 }
497 }
498
499 pub(crate) fn running(&self) -> Option<&IndependentCheckpointJob> {
500 match self {
501 Self::Running { job, .. } => Some(job),
502 Self::Resetting { .. } => None,
503 }
504 }
505
506 pub(crate) fn running_mut(&mut self) -> Option<&mut IndependentCheckpointJob> {
507 match self {
508 Self::Running { job, .. } => Some(job),
509 Self::Resetting { .. } => None,
510 }
511 }
512
513 pub(crate) fn ready_mut(&mut self) -> Option<&mut IndependentCheckpointJob> {
514 match self {
515 Self::Running {
516 status: IndependentCheckpointJobStatus::Ready,
517 job,
518 ..
519 } => Some(job),
520 Self::Running {
521 status: IndependentCheckpointJobStatus::Initial { .. },
522 ..
523 }
524 | Self::Resetting { .. } => None,
525 }
526 }
527
528 pub(crate) fn on_upstream_database_ack_completed(&mut self, committed_epoch: u64) {
529 if let Self::Running { status, .. } = self {
530 status.on_upstream_database_ack_completed(committed_epoch);
531 }
532 }
533
534 pub(crate) fn gen_backfill_progress(&self) -> Option<BackfillProgress> {
535 match self.running()? {
536 IndependentCheckpointJob::CreatingStreamingJob(j) => Some(j.gen_backfill_progress()),
537 IndependentCheckpointJob::BatchRefresh(j) => j.gen_backfill_progress(),
538 IndependentCheckpointJob::IcebergV3(j) => j.gen_backfill_progress(),
539 }
540 }
541
542 pub(crate) fn collect(&mut self, collected_barrier: CollectedBarrier<'_>) -> bool {
544 let job = self
545 .running_mut()
546 .expect("barriers should only be collected from a running partial graph");
547 match job {
548 IndependentCheckpointJob::CreatingStreamingJob(j) => j.collect(collected_barrier),
549 IndependentCheckpointJob::BatchRefresh(j) => j.collect(collected_barrier),
550 IndependentCheckpointJob::IcebergV3(j) => j.collect(collected_barrier),
551 }
552 }
553
554 pub(crate) fn gen_fragment_backfill_progress(&self) -> Vec<FragmentBackfillProgress> {
555 match self.running() {
556 Some(IndependentCheckpointJob::CreatingStreamingJob(j)) => {
557 j.gen_fragment_backfill_progress()
558 }
559 Some(IndependentCheckpointJob::BatchRefresh(j)) => j.gen_fragment_backfill_progress(),
560 Some(IndependentCheckpointJob::IcebergV3(j)) => j.gen_fragment_backfill_progress(),
561 None => vec![],
562 }
563 }
564
565 pub(crate) fn pinned_upstream_tables(&self) -> &HashSet<TableId> {
566 match self {
567 Self::Running { job, .. } => job.pinned_upstream_tables(),
568 Self::Resetting {
569 pinned_upstream_tables,
570 ..
571 } => pinned_upstream_tables,
572 }
573 }
574
575 pub(crate) fn fragment_infos(&self) -> Option<&HashMap<FragmentId, InflightFragmentInfo>> {
576 match self.running()? {
577 IndependentCheckpointJob::CreatingStreamingJob(j) => j.fragment_infos(),
578 IndependentCheckpointJob::BatchRefresh(j) => j.fragment_infos(),
579 IndependentCheckpointJob::IcebergV3(j) => Some(j.fragment_infos()),
580 }
581 }
582
583 pub(crate) fn ack_completed(
584 &mut self,
585 partial_graph_manager: &mut PartialGraphManager,
586 epoch: u64,
587 ) -> MetaResult<()> {
588 match self {
589 Self::Running {
590 status: IndependentCheckpointJobStatus::Ready,
591 job: IndependentCheckpointJob::CreatingStreamingJob(j),
592 ..
593 } => {
594 j.ack_completed(partial_graph_manager, epoch);
595 }
596 Self::Running {
597 status: IndependentCheckpointJobStatus::Ready,
598 job: IndependentCheckpointJob::BatchRefresh(j),
599 ..
600 } => {
601 j.ack_completed(partial_graph_manager, epoch);
602 }
603 Self::Running {
604 status: IndependentCheckpointJobStatus::Ready,
605 job: IndependentCheckpointJob::IcebergV3(j),
606 ..
607 } => j.ack_completed(partial_graph_manager, epoch)?,
608 Self::Running {
609 status: IndependentCheckpointJobStatus::Initial { .. },
610 ..
611 } => {
612 panic!("an initial job should transition to ready before completing a barrier")
613 }
614 Self::Resetting { .. } => {
615 }
618 }
619 Ok(())
620 }
621
622 pub(crate) fn on_partial_graph_reset(self) -> Vec<PbSubscriptionUpstreamInfo> {
623 match self {
624 Self::Resetting {
625 subscriptions_to_drop,
626 notifiers,
627 ..
628 } => {
629 for notifier in notifiers {
630 notifier.notify_collected();
631 }
632 subscriptions_to_drop
633 }
634 Self::Running { .. } => {
635 panic!("should be resetting when receiving reset partial graph resp")
636 }
637 }
638 }
639
640 pub(crate) fn can_drop(&self) -> bool {
641 match self {
642 Self::Running { job, .. } => job.can_drop(),
643 Self::Resetting { .. } => true,
644 }
645 }
646
647 pub(crate) fn drop(
648 &mut self,
649 notifier: Option<&mut NotifierStarter>,
650 partial_graph_manager: &mut PartialGraphManager,
651 ) -> bool {
652 match self {
653 Self::Resetting { notifiers, .. } => {
654 notifiers.extend(notifier.map(NotifierStarter::add_notify));
655 true
656 }
657 Self::Running { job, .. } if !job.can_drop_independently() => false,
658 Self::Running {
659 job_id,
660 partial_graph_id,
661 job,
662 ..
663 } => {
664 let subscriptions_to_drop = job
665 .pinned_upstream_tables()
666 .iter()
667 .map(|upstream_mv_table_id| PbSubscriptionUpstreamInfo {
668 subscriber_id: job_id.as_subscriber_id(),
669 upstream_mv_table_id: *upstream_mv_table_id,
670 })
671 .collect();
672 let pinned_upstream_tables = job.pinned_upstream_tables().clone();
674 partial_graph_manager.reset_partial_graphs([*partial_graph_id]);
675 *self = Self::Resetting {
676 pinned_upstream_tables,
677 subscriptions_to_drop,
678 notifiers: notifier
679 .map(NotifierStarter::add_notify)
680 .into_iter()
681 .collect(),
682 };
683 true
684 }
685 }
686 }
687
688 pub(crate) fn reset(self) -> bool {
693 match self {
694 Self::Running { .. } => false,
697 Self::Resetting { notifiers, .. } => {
698 for notifier in notifiers {
699 notifier.notify_collected();
700 }
701 true
702 }
703 }
704 }
705}
706
707#[cfg(test)]
708mod tests {
709 use super::*;
710
711 #[test]
712 fn test_resetting_has_shared_inactive_behavior_and_keeps_pin() {
713 let pinned_upstream_tables = HashSet::from([TableId::new(7)]);
714 let job = IndependentCheckpointJobControl::Resetting {
715 pinned_upstream_tables: pinned_upstream_tables.clone(),
716 subscriptions_to_drop: vec![],
717 notifiers: vec![],
718 };
719
720 assert!(job.running().is_none());
721 assert!(job.gen_backfill_progress().is_none());
722 assert!(job.gen_fragment_backfill_progress().is_empty());
723 assert_eq!(job.pinned_upstream_tables(), &pinned_upstream_tables);
724 assert!(job.fragment_infos().is_none());
725 assert!(job.reset());
726 }
727
728 #[test]
729 fn test_reset_completion_returns_subscriptions_to_drop() {
730 let subscriptions_to_drop = vec![PbSubscriptionUpstreamInfo {
731 subscriber_id: JobId::new(8).as_subscriber_id(),
732 upstream_mv_table_id: TableId::new(7),
733 }];
734 let job = IndependentCheckpointJobControl::Resetting {
735 pinned_upstream_tables: HashSet::from([TableId::new(7)]),
736 subscriptions_to_drop: subscriptions_to_drop.clone(),
737 notifiers: vec![],
738 };
739
740 assert_eq!(job.on_partial_graph_reset(), subscriptions_to_drop);
741 }
742
743 #[test]
744 fn test_initial_status_transitions_on_upstream_database_ack() {
745 let mut status = IndependentCheckpointJobStatus::Initial { snapshot_epoch: 10 };
746
747 status.on_upstream_database_ack_completed(9);
748 assert_eq!(
749 status,
750 IndependentCheckpointJobStatus::Initial { snapshot_epoch: 10 }
751 );
752 status.on_upstream_database_ack_completed(10);
753 assert_eq!(status, IndependentCheckpointJobStatus::Ready);
754 status.on_upstream_database_ack_completed(11);
755 assert_eq!(status, IndependentCheckpointJobStatus::Ready);
756 }
757}