1mod barrier_control;
16mod status;
17
18use std::cmp::max;
19use std::collections::{HashMap, HashSet, VecDeque, hash_map};
20use std::mem::take;
21use std::ops::Bound::{Excluded, Unbounded};
22use std::time::Duration;
23
24use risingwave_common::catalog::{DatabaseId, TableId};
25use risingwave_common::id::JobId;
26use risingwave_common::metrics::LabelGuardedIntGauge;
27use risingwave_common::util::epoch::Epoch;
28use risingwave_meta_model::WorkerId;
29use risingwave_pb::ddl_service::PbBackfillType;
30use risingwave_pb::hummock::HummockVersionStats;
31use risingwave_pb::id::{ActorId, FragmentId, PartialGraphId};
32use risingwave_pb::stream_plan::barrier::PbBarrierKind;
33use risingwave_pb::stream_plan::barrier_mutation::Mutation;
34use risingwave_pb::stream_plan::{AddMutation, PbStreamNode, StopMutation};
35use risingwave_pb::stream_service::BarrierCompleteResponse;
36use status::CreatingStreamingJobStatus;
37use tracing::{debug, info};
38
39use super::super::state::RenderResult;
40use super::IndependentCheckpointJobControl;
41use crate::MetaResult;
42use crate::barrier::backfill_order_control::get_nodes_with_backfill_dependencies;
43use crate::barrier::checkpoint::independent_job::creating_job::barrier_control::CreatingStreamingJobBarrierStats;
44use crate::barrier::checkpoint::independent_job::creating_job::status::CreateMviewLogStoreProgressTracker;
45use crate::barrier::command::{PostCollectCommand, TableLogEpochs, UpstreamTableLogEpochs};
46use crate::barrier::context::CreateSnapshotBackfillJobCommandInfo;
47use crate::barrier::edge_builder::FragmentEdgeBuildResult;
48use crate::barrier::info::{BarrierInfo, InflightStreamingJobInfo};
49use crate::barrier::notifier::Notifier;
50use crate::barrier::partial_graph::{
51 CollectedBarrier, PartialGraphBarrierInfo, PartialGraphManager, PartialGraphRecoverer,
52};
53use crate::barrier::progress::{CreateMviewProgressTracker, TrackingJob, collect_done_fragments};
54use crate::barrier::rpc::{build_locality_fragment_state_table_mapping, to_partial_graph_id};
55use crate::barrier::{
56 BackfillOrderState, BackfillProgress, BarrierKind, Command, FragmentBackfillProgress,
57 TracedEpoch,
58};
59use crate::controller::fragment::InflightFragmentInfo;
60use crate::manager::MetaOpts;
61use crate::model::{FragmentDownstreamRelation, StreamActor, StreamJobActorsToCreate};
62use crate::rpc::metrics::GLOBAL_META_METRICS;
63use crate::stream::source_manager::SplitAssignment;
64use crate::stream::{ExtendedFragmentBackfillOrder, build_actor_connector_splits};
65
66fn snapshot_backfill_max_pending_barrier_num(opts: &MetaOpts) -> usize {
67 opts.in_flight_barrier_nums
68 .saturating_mul(opts.snapshot_backfill_barrier_amplification_factor.max(1))
69}
70
71#[derive(Debug)]
72pub(crate) struct CreatingJobInfo {
73 pub fragment_infos: HashMap<FragmentId, InflightFragmentInfo>,
74 pub upstream_fragment_downstreams: FragmentDownstreamRelation,
75 pub downstreams: FragmentDownstreamRelation,
76 pub snapshot_backfill_upstream_tables: HashSet<TableId>,
77 pub stream_actors: HashMap<ActorId, StreamActor>,
78}
79
80#[derive(Debug)]
81pub(crate) struct CreatingStreamingJobControl {
82 job_id: JobId,
83 partial_graph_id: PartialGraphId,
84 snapshot_backfill_upstream_tables: HashSet<TableId>,
85 snapshot_epoch: u64,
86
87 node_actors: HashMap<WorkerId, HashSet<ActorId>>,
88 state_table_ids: HashSet<TableId>,
89
90 max_committed_epoch: Option<u64>,
91 status: CreatingStreamingJobStatus,
92 max_lagged_barrier_num: usize,
93 max_pending_barrier_num: usize,
94
95 upstream_lag: LabelGuardedIntGauge,
96}
97
98impl CreatingStreamingJobControl {
99 #[expect(clippy::too_many_arguments)]
100 pub(crate) fn new<'a>(
101 entry: hash_map::VacantEntry<'a, JobId, IndependentCheckpointJobControl>,
102 create_info: CreateSnapshotBackfillJobCommandInfo,
103 notifiers: Vec<Notifier>,
104 snapshot_backfill_upstream_tables: HashSet<TableId>,
105 snapshot_epoch: u64,
106 since_timestamp_upstream_log_epochs: Option<(&TableLogEpochs, PartialGraphId, u64)>,
107 version_stat: &HummockVersionStats,
108 partial_graph_manager: &mut PartialGraphManager,
109 edges: &mut FragmentEdgeBuildResult,
110 split_assignment: &SplitAssignment,
111 actors: &RenderResult,
112 ) -> MetaResult<&'a mut Self> {
113 let info = create_info.info.clone();
114 let job_id = info.stream_job_fragments.stream_job_id();
115 let database_id = info.streaming_job.database_id();
116 debug!(
117 %job_id,
118 definition = info.definition,
119 "new creating job"
120 );
121 let fragment_infos = info
122 .stream_job_fragments
123 .new_fragment_info(
124 &actors.stream_actors,
125 &actors.actor_location,
126 split_assignment,
127 )
128 .collect();
129 let snapshot_backfill_actors: HashSet<ActorId> =
130 InflightStreamingJobInfo::snapshot_backfill_actor_ids(&fragment_infos).collect();
131 let backfill_nodes_to_pause =
132 get_nodes_with_backfill_dependencies(&info.fragment_backfill_ordering)
133 .into_iter()
134 .collect();
135 let backfill_order_state = BackfillOrderState::new(
136 &info.fragment_backfill_ordering,
137 &fragment_infos,
138 info.locality_fragment_state_table_mapping.clone(),
139 );
140 let create_mview_tracker = CreateMviewProgressTracker::recover(
141 job_id,
142 &fragment_infos,
143 backfill_order_state,
144 version_stat,
145 );
146
147 let actors_to_create = Command::create_streaming_job_actors_to_create(
148 &info,
149 edges,
150 &actors.stream_actors,
151 &actors.actor_location,
152 );
153
154 let mut prev_epoch_fake_physical_time = 0;
155 let mut pending_non_checkpoint_barriers = vec![];
156
157 let (initial_barrier_info, log_store_barriers_to_inject) = if let Some((
158 upstream_log_epochs,
159 upstream_partial_graph_id,
160 new_upstream_barrier_prev_epoch,
161 )) =
162 since_timestamp_upstream_log_epochs
163 {
164 let (initial_barrier, barriers_to_inject) =
165 Self::resolve_since_timestamp_upstream_log_epochs(
166 upstream_log_epochs,
167 partial_graph_manager.pending_barrier_infos(upstream_partial_graph_id),
168 snapshot_epoch,
169 new_upstream_barrier_prev_epoch,
170 )?;
171 (initial_barrier, Some(barriers_to_inject))
172 } else {
173 (
174 CreatingStreamingJobStatus::new_fake_barrier(
175 &mut prev_epoch_fake_physical_time,
176 &mut pending_non_checkpoint_barriers,
177 PbBarrierKind::Checkpoint,
178 ),
179 None,
180 )
181 };
182
183 let added_actors: Vec<ActorId> = actors
184 .stream_actors
185 .values()
186 .flatten()
187 .map(|actor| actor.actor_id)
188 .collect();
189 let actor_splits = split_assignment
190 .values()
191 .flat_map(build_actor_connector_splits)
192 .collect();
193
194 assert!(
195 info.cdc_table_snapshot_splits.is_none(),
196 "should not have cdc backfill for snapshot backfill job"
197 );
198
199 let initial_mutation = Mutation::Add(AddMutation {
200 actor_dispatchers: Default::default(),
202 added_actors,
203 actor_splits,
204 pause: false,
206 subscriptions_to_add: Default::default(),
207 backfill_nodes_to_pause,
208 actor_cdc_table_snapshot_splits: None,
209 new_upstream_sinks: Default::default(),
210 dropped_actors: Default::default(),
211 sink_log_store_flush: Default::default(),
212 });
213
214 let node_actors = InflightFragmentInfo::actor_ids_to_collect(fragment_infos.values());
215 let state_table_ids =
216 InflightFragmentInfo::existing_table_ids(fragment_infos.values()).collect();
217
218 let partial_graph_id = to_partial_graph_id(database_id, Some(job_id));
219 let max_lagged_barrier_num = partial_graph_manager
220 .control_stream_manager()
221 .env
222 .opts
223 .snapshot_backfill_finish_max_lagged_barriers;
224 let opts = &partial_graph_manager.control_stream_manager().env.opts;
225 let max_pending_barrier_num = snapshot_backfill_max_pending_barrier_num(opts);
226
227 let IndependentCheckpointJobControl::CreatingStreamingJob(job) = entry.insert(
228 IndependentCheckpointJobControl::CreatingStreamingJob(Self {
229 partial_graph_id,
230 job_id,
231 snapshot_backfill_upstream_tables,
232 max_committed_epoch: None,
233 snapshot_epoch,
234 status: CreatingStreamingJobStatus::PlaceHolder, max_lagged_barrier_num,
236 max_pending_barrier_num,
237 upstream_lag: GLOBAL_META_METRICS
238 .snapshot_backfill_lag
239 .with_guarded_label_values(&[&format!("{}", job_id)]),
240 node_actors,
241 state_table_ids,
242 }),
243 ) else {
244 unreachable!()
245 };
246
247 let mut graph_adder = partial_graph_manager.add_partial_graph(
248 partial_graph_id,
249 CreatingStreamingJobBarrierStats::new(job_id, snapshot_epoch),
250 );
251
252 if let Err(e) = Self::inject_barrier(
253 partial_graph_id,
254 graph_adder.manager(),
255 &job.node_actors,
256 &job.state_table_ids,
257 false,
258 initial_barrier_info,
259 Some(actors_to_create),
260 Some(initial_mutation),
261 notifiers,
262 Some(create_info),
263 ) {
264 graph_adder.failed();
265 job.status = CreatingStreamingJobStatus::Resetting(vec![]);
266 Err(e)
267 } else {
268 graph_adder.added();
269 let job_info = CreatingJobInfo {
270 fragment_infos,
271 upstream_fragment_downstreams: info.upstream_fragment_downstreams.clone(),
272 downstreams: info.stream_job_fragments.downstreams,
273 snapshot_backfill_upstream_tables: job.snapshot_backfill_upstream_tables.clone(),
274 stream_actors: actors
275 .stream_actors
276 .values()
277 .flatten()
278 .map(|actor| (actor.actor_id, actor.clone()))
279 .collect(),
280 };
281 if let Some(log_store_barriers_to_inject) = log_store_barriers_to_inject {
282 let upstream_lag = log_store_barriers_to_inject
283 .last()
284 .map(|info| info.prev_epoch().saturating_sub(snapshot_epoch))
285 .unwrap_or(0);
286 job.status = CreatingStreamingJobStatus::ConsumingLogStore {
287 tracking_job: TrackingJob::recovered(job_id, &job_info.fragment_infos),
288 info: job_info,
289 log_store_progress_tracker: CreateMviewLogStoreProgressTracker::new(
290 snapshot_backfill_actors.iter().cloned(),
291 upstream_lag,
292 ),
293 pending_barriers: log_store_barriers_to_inject.into(),
294 };
295 } else {
296 assert!(pending_non_checkpoint_barriers.is_empty());
297 job.status = CreatingStreamingJobStatus::ConsumingSnapshot {
298 prev_epoch_fake_physical_time,
299 pending_upstream_barriers: vec![],
300 version_stats: version_stat.clone(),
301 create_mview_tracker,
302 snapshot_backfill_actors,
303 snapshot_epoch,
304 info: job_info,
305 pending_non_checkpoint_barriers,
306 };
307 };
308 Ok(job)
309 }
310 }
311
312 pub(super) fn gen_fragment_backfill_progress(&self) -> Vec<FragmentBackfillProgress> {
313 match &self.status {
314 CreatingStreamingJobStatus::ConsumingSnapshot {
315 create_mview_tracker,
316 info,
317 ..
318 } => create_mview_tracker.collect_fragment_progress(&info.fragment_infos, true),
319 CreatingStreamingJobStatus::ConsumingLogStore { info, .. } => {
320 collect_done_fragments(self.job_id, &info.fragment_infos)
321 }
322 CreatingStreamingJobStatus::Finishing(_, _)
323 | CreatingStreamingJobStatus::Resetting(_)
324 | CreatingStreamingJobStatus::PlaceHolder => vec![],
325 }
326 }
327
328 fn resolve_upstream_log_epochs(
329 snapshot_backfill_upstream_tables: &HashSet<TableId>,
330 upstream_table_log_epochs: &UpstreamTableLogEpochs,
331 exclusive_start_log_epoch: u64,
332 upstream_barrier_info: &BarrierInfo,
333 ) -> MetaResult<Vec<BarrierInfo>> {
334 let table_id = snapshot_backfill_upstream_tables
335 .iter()
336 .next()
337 .expect("snapshot backfill job should have upstream");
338 let epochs_iter = if let Some(epochs) = upstream_table_log_epochs.get(table_id) {
339 let mut epochs_iter = epochs.iter();
340 loop {
341 let (_, checkpoint_epoch) =
342 epochs_iter.next().expect("not reach committed epoch yet");
343 if *checkpoint_epoch < exclusive_start_log_epoch {
344 continue;
345 }
346 assert_eq!(*checkpoint_epoch, exclusive_start_log_epoch);
347 break;
348 }
349 epochs_iter
350 } else {
351 assert_eq!(
353 upstream_barrier_info.prev_epoch(),
354 exclusive_start_log_epoch
355 );
356 static EMPTY_VEC: Vec<(Vec<u64>, u64)> = Vec::new();
357 EMPTY_VEC.iter()
358 };
359
360 let mut ret = vec![];
361 let mut prev_epoch = exclusive_start_log_epoch;
362 let mut pending_non_checkpoint_barriers = vec![];
363 for (non_checkpoint_epochs, checkpoint_epoch) in epochs_iter {
364 for (i, epoch) in non_checkpoint_epochs
365 .iter()
366 .chain([checkpoint_epoch])
367 .enumerate()
368 {
369 assert!(*epoch > prev_epoch);
370 pending_non_checkpoint_barriers.push(prev_epoch);
371 ret.push(BarrierInfo {
372 prev_epoch: TracedEpoch::new(Epoch(prev_epoch)),
373 curr_epoch: TracedEpoch::new(Epoch(*epoch)),
374 kind: if i == 0 {
375 BarrierKind::Checkpoint(take(&mut pending_non_checkpoint_barriers))
376 } else {
377 BarrierKind::Barrier
378 },
379 });
380 prev_epoch = *epoch;
381 }
382 }
383 ret.push(BarrierInfo {
384 prev_epoch: TracedEpoch::new(Epoch(prev_epoch)),
385 curr_epoch: TracedEpoch::new(Epoch(upstream_barrier_info.curr_epoch())),
386 kind: BarrierKind::Checkpoint(pending_non_checkpoint_barriers),
387 });
388 Ok(ret)
389 }
390
391 fn resolve_since_timestamp_upstream_log_epochs(
420 upstream_log_epochs: &TableLogEpochs,
421 pending_upstream_barriers: impl Iterator<Item = &BarrierInfo>,
422 snapshot_epoch: u64,
423 new_upstream_barrier_prev_epoch: u64,
424 ) -> MetaResult<(BarrierInfo, Vec<BarrierInfo>)> {
425 let mut initial_barrier = None;
426 let mut barriers = vec![];
427 fn emit_barrier(
428 initial_barrier: &mut Option<BarrierInfo>,
429 barriers: &mut Vec<BarrierInfo>,
430 barrier: BarrierInfo,
431 ) {
432 if initial_barrier.is_none() {
433 *initial_barrier = Some(barrier);
434 } else {
435 barriers.push(barrier);
436 }
437 }
438
439 let mut prev_epoch = snapshot_epoch;
440 let mut pending_non_checkpoint_barriers = vec![];
441 for (non_checkpoint_epochs, checkpoint_epoch) in upstream_log_epochs {
442 for (i, epoch) in non_checkpoint_epochs
443 .iter()
444 .chain([checkpoint_epoch])
445 .enumerate()
446 {
447 assert!(
448 *epoch > prev_epoch,
449 "changelog epochs should be strictly increasing"
450 );
451 pending_non_checkpoint_barriers.push(prev_epoch);
452 emit_barrier(
453 &mut initial_barrier,
454 &mut barriers,
455 BarrierInfo {
456 prev_epoch: TracedEpoch::new(Epoch(prev_epoch)),
457 curr_epoch: TracedEpoch::new(Epoch(*epoch)),
458 kind: if i == 0 {
459 BarrierKind::Checkpoint(take(&mut pending_non_checkpoint_barriers))
460 } else {
461 BarrierKind::Barrier
462 },
463 },
464 );
465 prev_epoch = *epoch;
466 }
467 }
468
469 let mut pending_upstream_barriers = pending_upstream_barriers.peekable();
470 pending_non_checkpoint_barriers.push(prev_epoch);
471 if pending_upstream_barriers.peek().is_none() {
472 assert!(
473 new_upstream_barrier_prev_epoch > prev_epoch,
474 "new upstream barrier prev epoch should be newer than the latest changelog epoch"
475 );
476 emit_barrier(
477 &mut initial_barrier,
478 &mut barriers,
479 BarrierInfo {
480 prev_epoch: TracedEpoch::new(Epoch(prev_epoch)),
481 curr_epoch: TracedEpoch::new(Epoch(new_upstream_barrier_prev_epoch)),
482 kind: BarrierKind::Checkpoint(pending_non_checkpoint_barriers),
483 },
484 );
485 } else {
486 let first_pending_barrier = pending_upstream_barriers
487 .peek()
488 .expect("first pending upstream barrier should exist after peek");
489 assert!(
490 first_pending_barrier.prev_epoch() > prev_epoch,
491 "first pending upstream barrier should be newer than the latest resolved changelog epoch"
492 );
493 emit_barrier(
494 &mut initial_barrier,
495 &mut barriers,
496 BarrierInfo {
497 prev_epoch: TracedEpoch::new(Epoch(prev_epoch)),
498 curr_epoch: TracedEpoch::new(Epoch(first_pending_barrier.prev_epoch())),
499 kind: BarrierKind::Checkpoint(take(&mut pending_non_checkpoint_barriers)),
500 },
501 );
502 prev_epoch = first_pending_barrier.prev_epoch();
503 for pending_barrier in pending_upstream_barriers {
504 assert_eq!(
505 pending_barrier.prev_epoch(),
506 prev_epoch,
507 "pending upstream barriers should continue from resolved changelog epochs"
508 );
509 pending_non_checkpoint_barriers.push(prev_epoch);
510 emit_barrier(
511 &mut initial_barrier,
512 &mut barriers,
513 BarrierInfo {
514 prev_epoch: TracedEpoch::new(Epoch(prev_epoch)),
515 curr_epoch: TracedEpoch::new(Epoch(pending_barrier.curr_epoch())),
516 kind: if pending_barrier.kind.is_checkpoint() {
517 BarrierKind::Checkpoint(take(&mut pending_non_checkpoint_barriers))
518 } else {
519 BarrierKind::Barrier
520 },
521 },
522 );
523 prev_epoch = pending_barrier.curr_epoch();
524 }
525 assert_eq!(
526 new_upstream_barrier_prev_epoch, prev_epoch,
527 "new upstream barrier prev epoch should match the latest pending log-store epoch"
528 );
529 }
530 let Some(initial_barrier) = initial_barrier else {
531 return Err(anyhow::anyhow!(
532 "missing lagging barriers for direct log-store start from snapshot epoch {}",
533 snapshot_epoch
534 )
535 .into());
536 };
537 assert!(initial_barrier.kind.is_checkpoint());
538 Ok((initial_barrier, barriers))
539 }
540
541 fn recover_consuming_snapshot(
542 job_id: JobId,
543 upstream_table_log_epochs: &UpstreamTableLogEpochs,
544 snapshot_epoch: u64,
545 committed_epoch: u64,
546 upstream_barrier_info: &BarrierInfo,
547 info: CreatingJobInfo,
548 backfill_order_state: BackfillOrderState,
549 version_stat: &HummockVersionStats,
550 ) -> MetaResult<(CreatingStreamingJobStatus, BarrierInfo)> {
551 let mut prev_epoch_fake_physical_time = Epoch(committed_epoch).physical_time();
552 let mut pending_non_checkpoint_barriers = vec![];
553 let create_mview_tracker = CreateMviewProgressTracker::recover(
554 job_id,
555 &info.fragment_infos,
556 backfill_order_state,
557 version_stat,
558 );
559 let barrier_info = CreatingStreamingJobStatus::new_fake_barrier(
560 &mut prev_epoch_fake_physical_time,
561 &mut pending_non_checkpoint_barriers,
562 PbBarrierKind::Initial,
563 );
564 Ok((
565 CreatingStreamingJobStatus::ConsumingSnapshot {
566 prev_epoch_fake_physical_time,
567 pending_upstream_barriers: Self::resolve_upstream_log_epochs(
568 &info.snapshot_backfill_upstream_tables,
569 upstream_table_log_epochs,
570 snapshot_epoch,
571 upstream_barrier_info,
572 )?,
573 version_stats: version_stat.clone(),
574 create_mview_tracker,
575 snapshot_backfill_actors: InflightStreamingJobInfo::snapshot_backfill_actor_ids(
576 &info.fragment_infos,
577 )
578 .collect(),
579 info,
580 snapshot_epoch,
581 pending_non_checkpoint_barriers,
582 },
583 barrier_info,
584 ))
585 }
586
587 fn recover_consuming_log_store(
588 job_id: JobId,
589 upstream_table_log_epochs: &UpstreamTableLogEpochs,
590 committed_epoch: u64,
591 upstream_barrier_info: &BarrierInfo,
592 info: CreatingJobInfo,
593 ) -> MetaResult<(CreatingStreamingJobStatus, BarrierInfo)> {
594 let mut pending_barriers: VecDeque<_> = Self::resolve_upstream_log_epochs(
595 &info.snapshot_backfill_upstream_tables,
596 upstream_table_log_epochs,
597 committed_epoch,
598 upstream_barrier_info,
599 )?
600 .into();
601 let mut first_barrier = pending_barriers
602 .pop_front()
603 .expect("resolved upstream log epochs should not be empty");
604 assert!(first_barrier.kind.is_checkpoint());
605 first_barrier.kind = BarrierKind::Initial;
606
607 Ok((
608 CreatingStreamingJobStatus::ConsumingLogStore {
609 tracking_job: TrackingJob::recovered(job_id, &info.fragment_infos),
610 log_store_progress_tracker: CreateMviewLogStoreProgressTracker::new(
611 InflightStreamingJobInfo::snapshot_backfill_actor_ids(&info.fragment_infos),
612 pending_barriers
613 .back()
614 .map(|info| info.prev_epoch() - committed_epoch)
615 .unwrap_or(0),
616 ),
617 pending_barriers,
618 info,
619 },
620 first_barrier,
621 ))
622 }
623
624 #[expect(clippy::too_many_arguments)]
625 pub(crate) fn recover(
626 database_id: DatabaseId,
627 job_id: JobId,
628 snapshot_backfill_upstream_tables: HashSet<TableId>,
629 upstream_table_log_epochs: &UpstreamTableLogEpochs,
630 snapshot_epoch: u64,
631 committed_epoch: u64,
632 upstream_barrier_info: &BarrierInfo,
633 fragment_infos: HashMap<FragmentId, InflightFragmentInfo>,
634 backfill_order: ExtendedFragmentBackfillOrder,
635 fragment_relations: &FragmentDownstreamRelation,
636 version_stat: &HummockVersionStats,
637 new_actors: StreamJobActorsToCreate,
638 initial_mutation: Mutation,
639 partial_graph_recoverer: &mut PartialGraphRecoverer<'_>,
640 ) -> MetaResult<Self> {
641 info!(
642 %job_id,
643 "recovered creating snapshot backfill job"
644 );
645
646 let node_actors = InflightFragmentInfo::actor_ids_to_collect(fragment_infos.values());
647 let state_table_ids: HashSet<_> =
648 InflightFragmentInfo::existing_table_ids(fragment_infos.values()).collect();
649
650 let mut upstream_fragment_downstreams: FragmentDownstreamRelation = Default::default();
651 for (upstream_fragment_id, downstreams) in fragment_relations {
652 if fragment_infos.contains_key(upstream_fragment_id) {
653 continue;
654 }
655 for downstream in downstreams {
656 if fragment_infos.contains_key(&downstream.downstream_fragment_id) {
657 upstream_fragment_downstreams
658 .entry(*upstream_fragment_id)
659 .or_default()
660 .push(downstream.clone());
661 }
662 }
663 }
664 let downstreams = fragment_infos
665 .keys()
666 .filter_map(|fragment_id| {
667 fragment_relations
668 .get(fragment_id)
669 .map(|relation| (*fragment_id, relation.clone()))
670 })
671 .collect();
672
673 let info = CreatingJobInfo {
674 fragment_infos,
675 upstream_fragment_downstreams,
676 downstreams,
677 snapshot_backfill_upstream_tables: snapshot_backfill_upstream_tables.clone(),
678 stream_actors: new_actors
679 .values()
680 .flat_map(|fragments| {
681 fragments.values().flat_map(|(_, actors, _)| {
682 actors
683 .iter()
684 .map(|(actor, _, _)| (actor.actor_id, actor.clone()))
685 })
686 })
687 .collect(),
688 };
689
690 let (status, first_barrier_info) = if committed_epoch < snapshot_epoch {
691 let locality_fragment_state_table_mapping =
692 build_locality_fragment_state_table_mapping(&info.fragment_infos);
693 let backfill_order_state = BackfillOrderState::recover_from_fragment_infos(
694 &backfill_order,
695 &info.fragment_infos,
696 locality_fragment_state_table_mapping,
697 );
698 Self::recover_consuming_snapshot(
699 job_id,
700 upstream_table_log_epochs,
701 snapshot_epoch,
702 committed_epoch,
703 upstream_barrier_info,
704 info,
705 backfill_order_state,
706 version_stat,
707 )?
708 } else {
709 Self::recover_consuming_log_store(
710 job_id,
711 upstream_table_log_epochs,
712 committed_epoch,
713 upstream_barrier_info,
714 info,
715 )?
716 };
717
718 let partial_graph_id = to_partial_graph_id(database_id, Some(job_id));
719 let max_lagged_barrier_num = partial_graph_recoverer
720 .control_stream_manager()
721 .env
722 .opts
723 .snapshot_backfill_finish_max_lagged_barriers;
724 let opts = &partial_graph_recoverer.control_stream_manager().env.opts;
725 let max_pending_barrier_num = snapshot_backfill_max_pending_barrier_num(opts);
726
727 partial_graph_recoverer.recover_graph(
728 partial_graph_id,
729 initial_mutation,
730 &first_barrier_info,
731 &node_actors,
732 state_table_ids.iter().copied(),
733 new_actors,
734 CreatingStreamingJobBarrierStats::new(job_id, snapshot_epoch),
735 )?;
736
737 Ok(Self {
738 job_id,
739 partial_graph_id,
740 snapshot_backfill_upstream_tables,
741 snapshot_epoch,
742 node_actors,
743 state_table_ids,
744 max_committed_epoch: Some(committed_epoch),
745 status,
746 max_lagged_barrier_num,
747 max_pending_barrier_num,
748 upstream_lag: GLOBAL_META_METRICS
749 .snapshot_backfill_lag
750 .with_guarded_label_values(&[&format!("{}", job_id)]),
751 })
752 }
753
754 pub(crate) fn gen_backfill_progress(&self) -> BackfillProgress {
755 let progress = match &self.status {
756 CreatingStreamingJobStatus::ConsumingSnapshot {
757 create_mview_tracker,
758 ..
759 } => {
760 if create_mview_tracker.is_finished() {
761 "Snapshot finished".to_owned()
762 } else {
763 let progress = create_mview_tracker.gen_backfill_progress();
764 format!("Snapshot [{}]", progress)
765 }
766 }
767 CreatingStreamingJobStatus::ConsumingLogStore {
768 log_store_progress_tracker,
769 ..
770 } => {
771 format!(
772 "LogStore [{}]",
773 log_store_progress_tracker.gen_backfill_progress()
774 )
775 }
776 CreatingStreamingJobStatus::Finishing(finish_epoch, ..) => {
777 let committed_epoch = self.max_committed_epoch.expect("should have committed");
778 let lag = Duration::from_millis(
779 Epoch(*finish_epoch).physical_time() - Epoch(committed_epoch).physical_time(),
780 );
781 format!("Finishing [epoch lag: {lag:?}]",)
782 }
783 CreatingStreamingJobStatus::Resetting(_) => "Resetting".to_owned(),
784 CreatingStreamingJobStatus::PlaceHolder => {
785 unreachable!()
786 }
787 };
788 BackfillProgress {
789 progress,
790 backfill_type: PbBackfillType::SnapshotBackfill,
791 }
792 }
793
794 pub(super) fn pinned_upstream_log_epoch(&self) -> (u64, HashSet<TableId>) {
795 (
796 max(self.max_committed_epoch.unwrap_or(0), self.snapshot_epoch),
797 self.snapshot_backfill_upstream_tables.clone(),
798 )
799 }
800
801 fn inject_barrier(
802 partial_graph_id: PartialGraphId,
803 partial_graph_manager: &mut PartialGraphManager,
804 node_actors: &HashMap<WorkerId, HashSet<ActorId>>,
805 state_table_ids: &HashSet<TableId>,
806 is_finishing: bool,
807 barrier_info: BarrierInfo,
808 new_actors: Option<StreamJobActorsToCreate>,
809 mutation: Option<Mutation>,
810 notifiers: Vec<Notifier>,
811 first_create_info: Option<CreateSnapshotBackfillJobCommandInfo>,
812 ) -> MetaResult<()> {
813 let (table_ids_to_sync, nodes_to_sync_table) = if !is_finishing {
814 (Some(state_table_ids), Some(node_actors.keys().copied()))
815 } else {
816 (None, None)
817 };
818 partial_graph_manager.inject_barrier(
819 partial_graph_id,
820 mutation,
821 node_actors,
822 table_ids_to_sync.into_iter().flatten().copied(),
823 nodes_to_sync_table.into_iter().flatten(),
824 new_actors,
825 PartialGraphBarrierInfo::new(
826 first_create_info.map_or_else(
827 PostCollectCommand::barrier,
828 CreateSnapshotBackfillJobCommandInfo::into_post_collect,
829 ),
830 barrier_info,
831 notifiers,
832 state_table_ids.clone(),
833 ),
834 )?;
835 Ok(())
836 }
837
838 pub(crate) fn start_consume_upstream(
839 &mut self,
840 partial_graph_manager: &mut PartialGraphManager,
841 barrier_info: &BarrierInfo,
842 ) -> MetaResult<CreatingJobInfo> {
843 info!(
844 job_id = %self.job_id,
845 prev_epoch = barrier_info.prev_epoch(),
846 "start consuming upstream"
847 );
848 let info = self.status.start_consume_upstream(barrier_info);
849 Self::inject_barrier(
850 self.partial_graph_id,
851 partial_graph_manager,
852 &self.node_actors,
853 &self.state_table_ids,
854 true,
855 barrier_info.clone(),
856 None,
857 Some(Mutation::Stop(StopMutation {
858 actors: info
860 .fragment_infos
861 .values()
862 .flat_map(|info| info.actors.keys().copied())
863 .collect(),
864 dropped_sink_fragments: vec![], })),
866 vec![], None,
868 )?;
869 Ok(info)
870 }
871
872 pub(crate) fn on_new_upstream_barrier(
873 &mut self,
874 partial_graph_manager: &mut PartialGraphManager,
875 barrier_info: &BarrierInfo,
876 mutation: Option<(Mutation, Vec<Notifier>)>,
877 ) -> MetaResult<()> {
878 let progress_epoch = if let Some(max_committed_epoch) = self.max_committed_epoch {
879 max(max_committed_epoch, self.snapshot_epoch)
880 } else {
881 self.snapshot_epoch
882 };
883 self.upstream_lag.set(
884 barrier_info
885 .prev_epoch
886 .value()
887 .0
888 .saturating_sub(progress_epoch) as _,
889 );
890 let (mut mutation, mut notifiers) = match mutation {
891 Some((mutation, notifiers)) => (Some(mutation), notifiers),
892 None => (None, vec![]),
893 };
894 {
895 for (barrier_to_inject, mutation) in self.status.on_new_upstream_epoch(
896 partial_graph_manager,
897 self.partial_graph_id,
898 self.max_pending_barrier_num,
899 barrier_info,
900 mutation.take(),
901 ) {
902 Self::inject_barrier(
903 self.partial_graph_id,
904 partial_graph_manager,
905 &self.node_actors,
906 &self.state_table_ids,
907 false,
908 barrier_to_inject,
909 None,
910 mutation,
911 take(&mut notifiers),
912 None,
913 )?;
914 }
915 assert!(mutation.is_none(), "must have consumed mutation");
916 assert!(notifiers.is_empty(), "must consumed notifiers");
917 }
918 Ok(())
919 }
920
921 pub(crate) fn pre_apply_throttle<'a>(
922 &mut self,
923 fragment_nodes: impl IntoIterator<Item = (FragmentId, &'a PbStreamNode)>,
924 ) {
925 self.status.pre_apply_throttle(fragment_nodes);
926 }
927
928 pub(crate) fn collect(&mut self, collected_barrier: CollectedBarrier<'_>) -> bool {
930 let pending_barrier_num = collected_barrier.pending_barrier_num;
931 self.status.update_progress(
932 collected_barrier
933 .resps
934 .values()
935 .flat_map(|resp| &resp.create_mview_progress),
936 );
937 self.is_ready_to_merge() && pending_barrier_num <= self.max_lagged_barrier_num
938 }
939
940 fn is_ready_to_merge(&self) -> bool {
941 if let CreatingStreamingJobStatus::ConsumingLogStore {
942 log_store_progress_tracker,
943 pending_barriers,
944 ..
945 } = &self.status
946 && pending_barriers.is_empty()
947 && log_store_progress_tracker.is_finished()
948 {
949 true
950 } else {
951 false
952 }
953 }
954
955 pub(crate) fn should_merge_to_upstream(
956 &self,
957 partial_graph_manager: &PartialGraphManager,
958 ) -> bool {
959 if !self.is_ready_to_merge() {
960 return false;
961 }
962
963 partial_graph_manager.pending_barrier_num(self.partial_graph_id)
966 <= self.max_lagged_barrier_num
967 }
968}
969
970impl CreatingStreamingJobControl {
971 pub(crate) fn start_completing(
972 &mut self,
973 partial_graph_manager: &mut PartialGraphManager,
974 min_upstream_inflight_epoch: Option<u64>,
975 upstream_committed_epoch: u64,
976 ) -> Option<(
977 u64,
978 HashMap<WorkerId, BarrierCompleteResponse>,
979 PartialGraphBarrierInfo,
980 bool,
981 )> {
982 if upstream_committed_epoch < self.snapshot_epoch {
984 return None;
985 }
986 let (finished_at_epoch, epoch_end_bound) = match &self.status {
987 CreatingStreamingJobStatus::Finishing(finish_at_epoch, _) => {
988 let epoch_end_bound = min_upstream_inflight_epoch
989 .map(|upstream_epoch| {
990 if upstream_epoch < *finish_at_epoch {
991 Excluded(upstream_epoch)
992 } else {
993 Unbounded
994 }
995 })
996 .unwrap_or(Unbounded);
997 (Some(*finish_at_epoch), epoch_end_bound)
998 }
999 CreatingStreamingJobStatus::ConsumingSnapshot { .. }
1000 | CreatingStreamingJobStatus::ConsumingLogStore { .. } => (
1001 None,
1002 min_upstream_inflight_epoch
1003 .map(Excluded)
1004 .unwrap_or(Unbounded),
1005 ),
1006 CreatingStreamingJobStatus::Resetting(..) => {
1007 return None;
1008 }
1009 CreatingStreamingJobStatus::PlaceHolder => {
1010 unreachable!()
1011 }
1012 };
1013 partial_graph_manager
1014 .start_completing(
1015 self.partial_graph_id,
1016 epoch_end_bound,
1017 |non_checkpoint_epoch, _, _| {
1018 if let Some(finish_at_epoch) = finished_at_epoch {
1019 assert!(non_checkpoint_epoch.prev < finish_at_epoch);
1020 }
1021 },
1022 )
1023 .map(|(epoch, resps, info)| {
1024 let is_finish_epoch = if let Some(finish_at_epoch) = finished_at_epoch {
1025 assert!(!info.post_collect_command.should_checkpoint());
1026 if epoch == finish_at_epoch {
1027 self.ack_completed(partial_graph_manager, epoch);
1029 true
1030 } else {
1031 false
1032 }
1033 } else {
1034 false
1035 };
1036 (epoch, resps, info, is_finish_epoch)
1037 })
1038 }
1039
1040 pub(super) fn ack_completed(
1041 &mut self,
1042 partial_graph_manager: &mut PartialGraphManager,
1043 completed_epoch: u64,
1044 ) {
1045 match &self.status {
1046 CreatingStreamingJobStatus::ConsumingSnapshot { .. }
1047 | CreatingStreamingJobStatus::ConsumingLogStore { .. }
1048 | CreatingStreamingJobStatus::Finishing(_, _) => {
1049 partial_graph_manager.ack_completed(self.partial_graph_id, completed_epoch);
1050 if let Some(prev_max_committed_epoch) =
1051 self.max_committed_epoch.replace(completed_epoch)
1052 {
1053 assert!(completed_epoch > prev_max_committed_epoch);
1054 }
1055 }
1056 CreatingStreamingJobStatus::Resetting(_) => {
1057 }
1060 CreatingStreamingJobStatus::PlaceHolder => {
1061 unreachable!()
1062 }
1063 }
1064 }
1065
1066 pub(crate) fn fragment_infos(&self) -> Option<&HashMap<FragmentId, InflightFragmentInfo>> {
1067 self.status.fragment_infos()
1068 }
1069
1070 pub fn into_tracking_job(self) -> TrackingJob {
1071 match self.status {
1072 CreatingStreamingJobStatus::ConsumingSnapshot { .. }
1073 | CreatingStreamingJobStatus::ConsumingLogStore { .. }
1074 | CreatingStreamingJobStatus::Resetting(..)
1075 | CreatingStreamingJobStatus::PlaceHolder => {
1076 unreachable!("expect finish")
1077 }
1078 CreatingStreamingJobStatus::Finishing(_, tracking_job) => tracking_job,
1079 }
1080 }
1081
1082 pub(super) fn on_partial_graph_reset(mut self) {
1083 match &mut self.status {
1084 CreatingStreamingJobStatus::Resetting(notifiers) => {
1085 for notifier in notifiers.drain(..) {
1086 notifier.notify_collected();
1087 }
1088 }
1089 CreatingStreamingJobStatus::ConsumingSnapshot { .. }
1090 | CreatingStreamingJobStatus::ConsumingLogStore { .. }
1091 | CreatingStreamingJobStatus::Finishing(_, _) => {
1092 panic!(
1093 "should be resetting when receiving reset partial graph resp, but at {:?}",
1094 self.status
1095 )
1096 }
1097 CreatingStreamingJobStatus::PlaceHolder => {
1098 unreachable!()
1099 }
1100 }
1101 }
1102
1103 pub(super) fn drop(
1107 &mut self,
1108 notifiers: &mut Vec<Notifier>,
1109 partial_graph_manager: &mut PartialGraphManager,
1110 ) -> bool {
1111 match &mut self.status {
1112 CreatingStreamingJobStatus::Resetting(existing_notifiers) => {
1113 for notifier in &mut *notifiers {
1114 notifier.notify_started();
1115 }
1116 existing_notifiers.append(notifiers);
1117 true
1118 }
1119 CreatingStreamingJobStatus::ConsumingSnapshot { .. }
1120 | CreatingStreamingJobStatus::ConsumingLogStore { .. } => {
1121 for notifier in &mut *notifiers {
1122 notifier.notify_started();
1123 }
1124 partial_graph_manager.reset_partial_graphs([self.partial_graph_id]);
1125 self.status = CreatingStreamingJobStatus::Resetting(take(notifiers));
1126 true
1127 }
1128 CreatingStreamingJobStatus::Finishing(_, _) => false,
1129 CreatingStreamingJobStatus::PlaceHolder => {
1130 unreachable!()
1131 }
1132 }
1133 }
1134
1135 pub(crate) fn reset(self) -> bool {
1136 match self.status {
1137 CreatingStreamingJobStatus::ConsumingSnapshot { .. }
1138 | CreatingStreamingJobStatus::ConsumingLogStore { .. }
1139 | CreatingStreamingJobStatus::Finishing(_, _) => false,
1140 CreatingStreamingJobStatus::Resetting(notifiers) => {
1141 for notifier in notifiers {
1142 notifier.notify_collected();
1143 }
1144 true
1145 }
1146 CreatingStreamingJobStatus::PlaceHolder => {
1147 unreachable!()
1148 }
1149 }
1150 }
1151}
1152
1153#[cfg(test)]
1154mod tests {
1155 use super::*;
1156
1157 #[test]
1158 fn test_snapshot_backfill_max_pending_barrier_num() {
1159 let mut opts = MetaOpts::test(false);
1160 opts.in_flight_barrier_nums = 10;
1161
1162 opts.snapshot_backfill_barrier_amplification_factor = 0;
1163 assert_eq!(snapshot_backfill_max_pending_barrier_num(&opts), 10);
1164
1165 opts.snapshot_backfill_barrier_amplification_factor = 1;
1166 assert_eq!(snapshot_backfill_max_pending_barrier_num(&opts), 10);
1167
1168 opts.snapshot_backfill_barrier_amplification_factor = 10;
1169 assert_eq!(snapshot_backfill_max_pending_barrier_num(&opts), 100);
1170
1171 opts.in_flight_barrier_nums = usize::MAX;
1172 assert_eq!(snapshot_backfill_max_pending_barrier_num(&opts), usize::MAX);
1173 }
1174
1175 #[test]
1176 fn test_resolve_since_timestamp_upstream_log_epochs() {
1177 let upstream_log_epochs = vec![(vec![45, 50], 55)];
1178
1179 let (initial_barrier, barriers) =
1180 CreatingStreamingJobControl::resolve_since_timestamp_upstream_log_epochs(
1181 &upstream_log_epochs,
1182 [].iter(),
1183 40,
1184 60,
1185 )
1186 .unwrap();
1187
1188 assert_eq!(
1189 (initial_barrier.prev_epoch(), initial_barrier.curr_epoch()),
1190 (40, 45)
1191 );
1192 assert!(initial_barrier.kind.is_checkpoint());
1193 assert_eq!(
1194 barriers
1195 .iter()
1196 .map(|barrier| (barrier.prev_epoch(), barrier.curr_epoch()))
1197 .collect::<Vec<_>>(),
1198 vec![(45, 50), (50, 55), (55, 60)]
1199 );
1200 assert_eq!(
1201 barriers
1202 .iter()
1203 .map(|barrier| match &barrier.kind {
1204 BarrierKind::Checkpoint(epochs) => Some(epochs.clone()),
1205 _ => None,
1206 })
1207 .collect::<Vec<_>>(),
1208 vec![None, None, Some(vec![45, 50, 55])]
1209 );
1210 }
1211
1212 #[test]
1213 fn test_resolve_since_timestamp_upstream_log_epochs_with_pending_barriers() {
1214 let upstream_log_epochs = vec![(vec![45, 50], 55)];
1215 let pending_upstream_barriers = [
1216 BarrierInfo {
1217 prev_epoch: TracedEpoch::new(Epoch(60)),
1218 curr_epoch: TracedEpoch::new(Epoch(65)),
1219 kind: BarrierKind::Barrier,
1220 },
1221 BarrierInfo {
1222 prev_epoch: TracedEpoch::new(Epoch(65)),
1223 curr_epoch: TracedEpoch::new(Epoch(70)),
1224 kind: BarrierKind::Checkpoint(vec![60, 65]),
1225 },
1226 ];
1227
1228 let (initial_barrier, barriers) =
1229 CreatingStreamingJobControl::resolve_since_timestamp_upstream_log_epochs(
1230 &upstream_log_epochs,
1231 pending_upstream_barriers.iter(),
1232 40,
1233 70,
1234 )
1235 .unwrap();
1236
1237 assert_eq!(
1238 (initial_barrier.prev_epoch(), initial_barrier.curr_epoch()),
1239 (40, 45)
1240 );
1241 assert!(initial_barrier.kind.is_checkpoint());
1242 assert_eq!(
1243 barriers
1244 .iter()
1245 .map(|barrier| (barrier.prev_epoch(), barrier.curr_epoch()))
1246 .collect::<Vec<_>>(),
1247 vec![(45, 50), (50, 55), (55, 60), (60, 65), (65, 70)]
1248 );
1249 assert_eq!(
1250 barriers
1251 .iter()
1252 .map(|barrier| match &barrier.kind {
1253 BarrierKind::Checkpoint(epochs) => Some(epochs.clone()),
1254 _ => None,
1255 })
1256 .collect::<Vec<_>>(),
1257 vec![None, None, Some(vec![45, 50, 55]), None, Some(vec![60, 65])]
1258 );
1259 }
1260
1261 #[test]
1262 fn test_resolve_since_timestamp_upstream_log_epochs_with_gap_before_pending_barriers() {
1263 let upstream_log_epochs = vec![(vec![61, 62, 63, 64], 65)];
1264 let pending_upstream_barriers = [
1265 BarrierInfo {
1266 prev_epoch: TracedEpoch::new(Epoch(66)),
1267 curr_epoch: TracedEpoch::new(Epoch(67)),
1268 kind: BarrierKind::Barrier,
1269 },
1270 BarrierInfo {
1271 prev_epoch: TracedEpoch::new(Epoch(67)),
1272 curr_epoch: TracedEpoch::new(Epoch(68)),
1273 kind: BarrierKind::Barrier,
1274 },
1275 BarrierInfo {
1276 prev_epoch: TracedEpoch::new(Epoch(68)),
1277 curr_epoch: TracedEpoch::new(Epoch(69)),
1278 kind: BarrierKind::Barrier,
1279 },
1280 BarrierInfo {
1281 prev_epoch: TracedEpoch::new(Epoch(69)),
1282 curr_epoch: TracedEpoch::new(Epoch(70)),
1283 kind: BarrierKind::Barrier,
1284 },
1285 ];
1286
1287 let (initial_barrier, barriers) =
1288 CreatingStreamingJobControl::resolve_since_timestamp_upstream_log_epochs(
1289 &upstream_log_epochs,
1290 pending_upstream_barriers.iter(),
1291 60,
1292 70,
1293 )
1294 .unwrap();
1295
1296 assert_eq!(
1297 (initial_barrier.prev_epoch(), initial_barrier.curr_epoch()),
1298 (60, 61)
1299 );
1300 assert!(initial_barrier.kind.is_checkpoint());
1301 assert_eq!(
1302 barriers
1303 .iter()
1304 .map(|barrier| (barrier.prev_epoch(), barrier.curr_epoch()))
1305 .collect::<Vec<_>>(),
1306 vec![
1307 (61, 62),
1308 (62, 63),
1309 (63, 64),
1310 (64, 65),
1311 (65, 66),
1312 (66, 67),
1313 (67, 68),
1314 (68, 69),
1315 (69, 70)
1316 ]
1317 );
1318 assert_eq!(
1319 barriers
1320 .iter()
1321 .map(|barrier| match &barrier.kind {
1322 BarrierKind::Checkpoint(epochs) => Some(epochs.clone()),
1323 _ => None,
1324 })
1325 .collect::<Vec<_>>(),
1326 vec![
1327 None,
1328 None,
1329 None,
1330 None,
1331 Some(vec![61, 62, 63, 64, 65]),
1332 None,
1333 None,
1334 None,
1335 None
1336 ]
1337 );
1338 }
1339
1340 #[test]
1341 fn test_resolve_since_timestamp_upstream_log_epochs_without_pending_barriers() {
1342 let upstream_log_epochs = vec![(vec![61, 62, 63, 64], 65)];
1343
1344 let (initial_barrier, barriers) =
1345 CreatingStreamingJobControl::resolve_since_timestamp_upstream_log_epochs(
1346 &upstream_log_epochs,
1347 [].iter(),
1348 60,
1349 66,
1350 )
1351 .unwrap();
1352
1353 assert_eq!(
1354 (initial_barrier.prev_epoch(), initial_barrier.curr_epoch()),
1355 (60, 61)
1356 );
1357 assert!(initial_barrier.kind.is_checkpoint());
1358 assert_eq!(
1359 barriers
1360 .iter()
1361 .map(|barrier| (barrier.prev_epoch(), barrier.curr_epoch()))
1362 .collect::<Vec<_>>(),
1363 vec![(61, 62), (62, 63), (63, 64), (64, 65), (65, 66)]
1364 );
1365 assert_eq!(
1366 barriers
1367 .iter()
1368 .map(|barrier| match &barrier.kind {
1369 BarrierKind::Checkpoint(epochs) => Some(epochs.clone()),
1370 _ => None,
1371 })
1372 .collect::<Vec<_>>(),
1373 vec![None, None, None, None, Some(vec![61, 62, 63, 64, 65])]
1374 );
1375 }
1376}