Skip to main content

risingwave_meta/barrier/
info.rs

1// Copyright 2022 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::collections::hash_map::Entry;
16use std::collections::{HashMap, HashSet};
17use std::mem::replace;
18use std::sync::Arc;
19
20use itertools::Itertools;
21use parking_lot::RawRwLock;
22use parking_lot::lock_api::RwLockReadGuard;
23use risingwave_common::bitmap::Bitmap;
24use risingwave_common::catalog::{DatabaseId, FragmentTypeFlag, FragmentTypeMask, TableId};
25use risingwave_common::id::JobId;
26use risingwave_common::util::epoch::EpochPair;
27use risingwave_common::util::stream_graph_visitor::visit_stream_node_mut;
28use risingwave_connector::source::{SplitImpl, SplitMetaData};
29use risingwave_meta_model::WorkerId;
30use risingwave_meta_model::fragment::DistributionType;
31use risingwave_pb::ddl_service::PbBackfillType;
32use risingwave_pb::hummock::HummockVersionStats;
33use risingwave_pb::id::SubscriberId;
34use risingwave_pb::meta::PbFragmentWorkerSlotMapping;
35use risingwave_pb::meta::subscribe_response::Operation;
36use risingwave_pb::source::PbCdcTableSnapshotSplits;
37use risingwave_pb::stream_plan::stream_node::NodeBody;
38use risingwave_pb::stream_plan::{PbStreamNode, PbUpstreamSinkInfo};
39use risingwave_pb::stream_service::BarrierCompleteResponse;
40use tracing::{info, warn};
41
42use crate::barrier::cdc_progress::{CdcProgress, CdcTableBackfillTracker};
43use crate::barrier::command::{
44    CreateStreamingJobCommandInfo, PostCollectCommand, ReplaceStreamJobPlan,
45};
46use crate::barrier::edge_builder::{
47    EdgeBuilderFragmentInfo, FragmentEdgeBuildResult, FragmentEdgeBuilder,
48};
49use crate::barrier::progress::{CreateMviewProgressTracker, StagingCommitInfo};
50use crate::barrier::rpc::{ControlStreamManager, to_partial_graph_id};
51use crate::barrier::{
52    BackfillProgress, BarrierKind, CreateStreamingJobType, FragmentBackfillProgress, TracedEpoch,
53};
54use crate::controller::fragment::{InflightActorInfo, InflightFragmentInfo};
55use crate::controller::utils::rebuild_fragment_mapping;
56use crate::manager::NotificationManagerRef;
57use crate::model::{ActorId, BackfillUpstreamType, FragmentId, StreamActor, StreamJobFragments};
58use crate::stream::UpstreamSinkInfo;
59use crate::{MetaError, MetaResult};
60
61#[derive(Debug, Clone)]
62pub struct SharedActorInfo {
63    pub worker_id: WorkerId,
64    pub vnode_bitmap: Option<Bitmap>,
65    pub splits: Vec<SplitImpl>,
66}
67
68impl From<&InflightActorInfo> for SharedActorInfo {
69    fn from(value: &InflightActorInfo) -> Self {
70        Self {
71            worker_id: value.worker_id,
72            vnode_bitmap: value.vnode_bitmap.clone(),
73            splits: value.splits.clone(),
74        }
75    }
76}
77
78#[derive(Debug, Clone)]
79pub struct SharedFragmentInfo {
80    pub fragment_id: FragmentId,
81    pub job_id: JobId,
82    pub distribution_type: DistributionType,
83    pub actors: HashMap<ActorId, SharedActorInfo>,
84    pub vnode_count: usize,
85    pub fragment_type_mask: FragmentTypeMask,
86    pub state_table_ids: HashSet<TableId>,
87}
88
89impl From<(&InflightFragmentInfo, JobId)> for SharedFragmentInfo {
90    fn from(pair: (&InflightFragmentInfo, JobId)) -> Self {
91        let (info, job_id) = pair;
92
93        let InflightFragmentInfo {
94            fragment_id,
95            distribution_type,
96            fragment_type_mask,
97            actors,
98            vnode_count,
99            state_table_ids,
100            ..
101        } = info;
102
103        Self {
104            fragment_id: *fragment_id,
105            job_id,
106            distribution_type: *distribution_type,
107            fragment_type_mask: *fragment_type_mask,
108            actors: actors
109                .iter()
110                .map(|(actor_id, actor)| (*actor_id, actor.into()))
111                .collect(),
112            vnode_count: *vnode_count,
113            state_table_ids: state_table_ids.clone(),
114        }
115    }
116}
117
118#[derive(Default, Debug)]
119pub struct SharedActorInfosInner {
120    info: HashMap<DatabaseId, HashMap<FragmentId, SharedFragmentInfo>>,
121}
122
123impl SharedActorInfosInner {
124    pub fn get_fragment(&self, fragment_id: FragmentId) -> Option<&SharedFragmentInfo> {
125        self.info
126            .values()
127            .find_map(|database| database.get(&fragment_id))
128    }
129
130    pub fn iter_over_fragments(&self) -> impl Iterator<Item = (&FragmentId, &SharedFragmentInfo)> {
131        self.info.values().flatten()
132    }
133}
134
135#[derive(Clone, educe::Educe)]
136#[educe(Debug)]
137pub struct SharedActorInfos {
138    inner: Arc<parking_lot::RwLock<SharedActorInfosInner>>,
139    #[educe(Debug(ignore))]
140    notification_manager: NotificationManagerRef,
141}
142
143impl SharedActorInfos {
144    pub fn read_guard(&self) -> RwLockReadGuard<'_, RawRwLock, SharedActorInfosInner> {
145        self.inner.read()
146    }
147
148    pub fn list_assignments(&self) -> HashMap<ActorId, Vec<SplitImpl>> {
149        let core = self.inner.read();
150        core.iter_over_fragments()
151            .flat_map(|(_, fragment)| {
152                fragment
153                    .actors
154                    .iter()
155                    .map(|(actor_id, info)| (*actor_id, info.splits.clone()))
156            })
157            .collect()
158    }
159
160    /// Migrates splits from previous actors to the new actors for a rescheduled fragment.
161    ///
162    /// Very occasionally split removal may happen during scaling, in which case we need to
163    /// use the old splits for reallocation instead of the latest splits (which may be missing),
164    /// so that we can resolve the split removal in the next command.
165    pub fn migrate_splits_for_source_actors(
166        &self,
167        fragment_id: FragmentId,
168        prev_actor_ids: &[ActorId],
169        curr_actor_ids: &[ActorId],
170    ) -> MetaResult<HashMap<ActorId, Vec<SplitImpl>>> {
171        let guard = self.read_guard();
172
173        let prev_splits = prev_actor_ids
174            .iter()
175            .flat_map(|actor_id| {
176                // Note: File Source / Iceberg Source doesn't have splits assigned by meta.
177                guard
178                    .get_fragment(fragment_id)
179                    .and_then(|info| info.actors.get(actor_id))
180                    .map(|actor| actor.splits.clone())
181                    .unwrap_or_default()
182            })
183            .map(|split| (split.id(), split))
184            .collect();
185
186        let empty_actor_splits = curr_actor_ids
187            .iter()
188            .map(|actor_id| (*actor_id, vec![]))
189            .collect();
190
191        let diff = crate::stream::source_manager::reassign_splits(
192            fragment_id,
193            empty_actor_splits,
194            &prev_splits,
195            // pre-allocate splits is the first time getting splits, and it does not have scale-in scene
196            std::default::Default::default(),
197        )
198        .unwrap_or_default();
199
200        Ok(diff)
201    }
202}
203
204impl SharedActorInfos {
205    pub(crate) fn new(notification_manager: NotificationManagerRef) -> Self {
206        Self {
207            inner: Arc::new(Default::default()),
208            notification_manager,
209        }
210    }
211
212    pub(super) fn remove_database(&self, database_id: DatabaseId) {
213        if let Some(database) = self.inner.write().info.remove(&database_id) {
214            let mapping = database
215                .into_values()
216                .map(|fragment| rebuild_fragment_mapping(&fragment))
217                .collect_vec();
218            if !mapping.is_empty() {
219                self.notification_manager
220                    .notify_streaming_fragment_mapping(Operation::Delete, mapping);
221            }
222        }
223    }
224
225    pub(super) fn retain_databases(&self, database_ids: impl IntoIterator<Item = DatabaseId>) {
226        let database_ids: HashSet<_> = database_ids.into_iter().collect();
227
228        let mut mapping = Vec::new();
229        for fragment in self
230            .inner
231            .write()
232            .info
233            .extract_if(|database_id, _| !database_ids.contains(database_id))
234            .flat_map(|(_, fragments)| fragments.into_values())
235        {
236            mapping.push(rebuild_fragment_mapping(&fragment));
237        }
238        if !mapping.is_empty() {
239            self.notification_manager
240                .notify_streaming_fragment_mapping(Operation::Delete, mapping);
241        }
242    }
243
244    pub(super) fn recover_database(
245        &self,
246        database_id: DatabaseId,
247        fragments: impl Iterator<Item = (&InflightFragmentInfo, JobId)>,
248    ) {
249        let mut remaining_fragments: HashMap<_, _> = fragments
250            .map(|info @ (fragment, _)| (fragment.fragment_id, info))
251            .collect();
252        // delete the fragments that exist previously, but not included in the recovered fragments
253        let mut writer = self.start_writer(database_id);
254        let database = writer.write_guard.info.entry(database_id).or_default();
255        for (_, fragment) in database.extract_if(|fragment_id, fragment_infos| {
256            if let Some(info) = remaining_fragments.remove(fragment_id) {
257                let info = info.into();
258                writer
259                    .updated_fragment_mapping
260                    .get_or_insert_default()
261                    .push(rebuild_fragment_mapping(&info));
262                *fragment_infos = info;
263                false
264            } else {
265                true
266            }
267        }) {
268            writer
269                .deleted_fragment_mapping
270                .get_or_insert_default()
271                .push(rebuild_fragment_mapping(&fragment));
272        }
273        for (fragment_id, info) in remaining_fragments {
274            let info = info.into();
275            writer
276                .added_fragment_mapping
277                .get_or_insert_default()
278                .push(rebuild_fragment_mapping(&info));
279            database.insert(fragment_id, info);
280        }
281        writer.finish();
282    }
283
284    pub(super) fn upsert(
285        &self,
286        database_id: DatabaseId,
287        infos: impl IntoIterator<Item = (&InflightFragmentInfo, JobId)>,
288    ) {
289        let mut writer = self.start_writer(database_id);
290        writer.upsert(infos);
291        writer.finish();
292    }
293
294    pub(super) fn start_writer(&self, database_id: DatabaseId) -> SharedActorInfoWriter<'_> {
295        SharedActorInfoWriter {
296            database_id,
297            write_guard: self.inner.write(),
298            notification_manager: &self.notification_manager,
299            added_fragment_mapping: None,
300            updated_fragment_mapping: None,
301            deleted_fragment_mapping: None,
302        }
303    }
304}
305
306pub(super) struct SharedActorInfoWriter<'a> {
307    database_id: DatabaseId,
308    write_guard: parking_lot::RwLockWriteGuard<'a, SharedActorInfosInner>,
309    notification_manager: &'a NotificationManagerRef,
310    added_fragment_mapping: Option<Vec<PbFragmentWorkerSlotMapping>>,
311    updated_fragment_mapping: Option<Vec<PbFragmentWorkerSlotMapping>>,
312    deleted_fragment_mapping: Option<Vec<PbFragmentWorkerSlotMapping>>,
313}
314
315impl SharedActorInfoWriter<'_> {
316    pub(super) fn upsert(
317        &mut self,
318        infos: impl IntoIterator<Item = (&InflightFragmentInfo, JobId)>,
319    ) {
320        let database = self.write_guard.info.entry(self.database_id).or_default();
321        for info @ (fragment, _) in infos {
322            match database.entry(fragment.fragment_id) {
323                Entry::Occupied(mut entry) => {
324                    let info = info.into();
325                    self.updated_fragment_mapping
326                        .get_or_insert_default()
327                        .push(rebuild_fragment_mapping(&info));
328                    entry.insert(info);
329                }
330                Entry::Vacant(entry) => {
331                    let info = info.into();
332                    self.added_fragment_mapping
333                        .get_or_insert_default()
334                        .push(rebuild_fragment_mapping(&info));
335                    entry.insert(info);
336                }
337            }
338        }
339    }
340
341    pub(super) fn remove(&mut self, info: &InflightFragmentInfo) {
342        if let Some(database) = self.write_guard.info.get_mut(&self.database_id)
343            && let Some(fragment) = database.remove(&info.fragment_id)
344        {
345            self.deleted_fragment_mapping
346                .get_or_insert_default()
347                .push(rebuild_fragment_mapping(&fragment));
348        }
349    }
350
351    pub(super) fn finish(self) {
352        if let Some(mapping) = self.added_fragment_mapping {
353            self.notification_manager
354                .notify_streaming_fragment_mapping(Operation::Add, mapping);
355        }
356        if let Some(mapping) = self.updated_fragment_mapping {
357            self.notification_manager
358                .notify_streaming_fragment_mapping(Operation::Update, mapping);
359        }
360        if let Some(mapping) = self.deleted_fragment_mapping {
361            self.notification_manager
362                .notify_streaming_fragment_mapping(Operation::Delete, mapping);
363        }
364    }
365}
366
367#[derive(Debug, Clone)]
368pub(super) struct BarrierInfo {
369    pub prev_epoch: TracedEpoch,
370    pub curr_epoch: TracedEpoch,
371    pub kind: BarrierKind,
372}
373
374impl BarrierInfo {
375    pub(super) fn prev_epoch(&self) -> u64 {
376        self.prev_epoch.value().0
377    }
378
379    pub(super) fn curr_epoch(&self) -> u64 {
380        self.curr_epoch.value().0
381    }
382
383    pub(super) fn epoch(&self) -> EpochPair {
384        EpochPair {
385            curr: self.curr_epoch(),
386            prev: self.prev_epoch(),
387        }
388    }
389}
390
391#[derive(Clone, Debug)]
392pub enum SubscriberType {
393    Subscription(u64),
394    SnapshotBackfill,
395}
396
397#[derive(Debug)]
398pub(super) enum CreateStreamingJobStatus {
399    Init,
400    Creating { tracker: CreateMviewProgressTracker },
401    Created,
402}
403
404#[derive(Debug)]
405pub(super) struct InflightStreamingJobInfo {
406    pub job_id: JobId,
407    pub fragment_infos: HashMap<FragmentId, InflightFragmentInfo>,
408    pub subscribers: HashMap<SubscriberId, SubscriberType>,
409    pub status: CreateStreamingJobStatus,
410    pub cdc_table_backfill_tracker: Option<CdcTableBackfillTracker>,
411}
412
413impl InflightStreamingJobInfo {
414    pub fn fragment_infos(&self) -> impl Iterator<Item = &InflightFragmentInfo> + '_ {
415        self.fragment_infos.values()
416    }
417
418    pub fn snapshot_backfill_actor_ids(
419        fragment_infos: &HashMap<FragmentId, InflightFragmentInfo>,
420    ) -> impl Iterator<Item = ActorId> + '_ {
421        fragment_infos
422            .values()
423            .filter(|fragment| {
424                fragment
425                    .fragment_type_mask
426                    .contains(FragmentTypeFlag::SnapshotBackfillStreamScan)
427            })
428            .flat_map(|fragment| fragment.actors.keys().copied())
429    }
430
431    pub fn tracking_progress_actor_ids(
432        fragment_infos: &HashMap<FragmentId, InflightFragmentInfo>,
433    ) -> Vec<(ActorId, BackfillUpstreamType)> {
434        StreamJobFragments::tracking_progress_actor_ids_impl(
435            fragment_infos
436                .values()
437                .map(|fragment| (fragment.fragment_type_mask, fragment.actors.keys().copied())),
438        )
439    }
440}
441
442impl<'a> IntoIterator for &'a InflightStreamingJobInfo {
443    type Item = &'a InflightFragmentInfo;
444
445    type IntoIter = impl Iterator<Item = &'a InflightFragmentInfo> + 'a;
446
447    fn into_iter(self) -> Self::IntoIter {
448        self.fragment_infos()
449    }
450}
451
452#[derive(Debug)]
453pub struct InflightDatabaseInfo {
454    pub(super) database_id: DatabaseId,
455    jobs: HashMap<JobId, InflightStreamingJobInfo>,
456    fragment_location: HashMap<FragmentId, JobId>,
457    pub(super) shared_actor_infos: SharedActorInfos,
458}
459
460impl InflightDatabaseInfo {
461    pub fn fragment_infos(&self) -> impl Iterator<Item = &InflightFragmentInfo> + '_ {
462        self.jobs.values().flat_map(|job| job.fragment_infos())
463    }
464
465    pub fn contains_job(&self, job_id: JobId) -> bool {
466        self.jobs.contains_key(&job_id)
467    }
468
469    pub(super) fn job_id_by_fragment(&self, fragment_id: FragmentId) -> Option<JobId> {
470        self.fragment_location.get(&fragment_id).copied()
471    }
472
473    pub fn fragment(&self, fragment_id: FragmentId) -> &InflightFragmentInfo {
474        let job_id = self.fragment_location[&fragment_id];
475        self.jobs
476            .get(&job_id)
477            .expect("should exist")
478            .fragment_infos
479            .get(&fragment_id)
480            .expect("should exist")
481    }
482
483    pub(super) fn backfill_fragment_ids_for_job(
484        &self,
485        job_id: JobId,
486    ) -> MetaResult<HashSet<FragmentId>> {
487        let job = self
488            .jobs
489            .get(&job_id)
490            .ok_or_else(|| MetaError::invalid_parameter(format!("job {} not found", job_id)))?;
491        Ok(job
492            .fragment_infos
493            .iter()
494            .filter_map(|(fragment_id, fragment)| {
495                fragment
496                    .fragment_type_mask
497                    .contains_any([
498                        FragmentTypeFlag::StreamScan,
499                        FragmentTypeFlag::SourceScan,
500                        FragmentTypeFlag::LocalityProvider,
501                    ])
502                    .then_some(*fragment_id)
503            })
504            .collect())
505    }
506
507    pub(super) fn is_backfill_fragment(&self, fragment_id: FragmentId) -> MetaResult<bool> {
508        let job_id = self.fragment_location.get(&fragment_id).ok_or_else(|| {
509            MetaError::invalid_parameter(format!("fragment {} not found", fragment_id))
510        })?;
511        let fragment = self
512            .jobs
513            .get(job_id)
514            .expect("should exist")
515            .fragment_infos
516            .get(&fragment_id)
517            .expect("should exist");
518        Ok(fragment.fragment_type_mask.contains_any([
519            FragmentTypeFlag::StreamScan,
520            FragmentTypeFlag::SourceScan,
521            FragmentTypeFlag::LocalityProvider,
522        ]))
523    }
524
525    pub fn gen_backfill_progress(&self) -> impl Iterator<Item = (JobId, BackfillProgress)> + '_ {
526        self.jobs
527            .iter()
528            .filter_map(|(job_id, job)| match &job.status {
529                CreateStreamingJobStatus::Init => None,
530                CreateStreamingJobStatus::Creating { tracker } => {
531                    let progress = tracker.gen_backfill_progress();
532                    Some((
533                        *job_id,
534                        BackfillProgress {
535                            progress,
536                            backfill_type: PbBackfillType::NormalBackfill,
537                        },
538                    ))
539                }
540                CreateStreamingJobStatus::Created => None,
541            })
542    }
543
544    pub fn gen_cdc_progress(&self) -> impl Iterator<Item = (JobId, CdcProgress)> + '_ {
545        self.jobs.iter().filter_map(|(job_id, job)| {
546            job.cdc_table_backfill_tracker
547                .as_ref()
548                .map(|tracker| (*job_id, tracker.gen_cdc_progress()))
549        })
550    }
551
552    pub fn gen_fragment_backfill_progress(&self) -> Vec<FragmentBackfillProgress> {
553        let mut result = Vec::new();
554        for job in self.jobs.values() {
555            let CreateStreamingJobStatus::Creating { tracker } = &job.status else {
556                continue;
557            };
558            let fragment_progress = tracker.collect_fragment_progress(&job.fragment_infos, true);
559            result.extend(fragment_progress);
560        }
561        result
562    }
563
564    pub(super) fn may_assign_fragment_cdc_backfill_splits(
565        &mut self,
566        fragment_id: FragmentId,
567    ) -> MetaResult<Option<HashMap<ActorId, PbCdcTableSnapshotSplits>>> {
568        let job_id = self.fragment_location[&fragment_id];
569        let job = self.jobs.get_mut(&job_id).expect("should exist");
570        if let Some(tracker) = &mut job.cdc_table_backfill_tracker {
571            let cdc_scan_fragment_id = tracker.cdc_scan_fragment_id();
572            if cdc_scan_fragment_id != fragment_id {
573                return Ok(None);
574            }
575            let actors = job.fragment_infos[&cdc_scan_fragment_id]
576                .actors
577                .keys()
578                .copied()
579                .collect();
580            tracker.reassign_splits(actors).map(Some)
581        } else {
582            Ok(None)
583        }
584    }
585
586    pub(super) fn assign_cdc_backfill_splits(
587        &mut self,
588        job_id: JobId,
589    ) -> MetaResult<Option<HashMap<ActorId, PbCdcTableSnapshotSplits>>> {
590        let job = self.jobs.get_mut(&job_id).expect("should exist");
591        if let Some(tracker) = &mut job.cdc_table_backfill_tracker {
592            let cdc_scan_fragment_id = tracker.cdc_scan_fragment_id();
593            let actors = job.fragment_infos[&cdc_scan_fragment_id]
594                .actors
595                .keys()
596                .copied()
597                .collect();
598            tracker.reassign_splits(actors).map(Some)
599        } else {
600            Ok(None)
601        }
602    }
603
604    pub(super) fn apply_collected_command(
605        &mut self,
606        command: &PostCollectCommand,
607        resps: &HashMap<WorkerId, BarrierCompleteResponse>,
608        version_stats: &HummockVersionStats,
609    ) {
610        if let PostCollectCommand::CreateStreamingJob { info, job_type, .. } = command {
611            match job_type {
612                CreateStreamingJobType::Normal | CreateStreamingJobType::SinkIntoTable(_) => {
613                    let job_id = info.streaming_job.id();
614                    if let Some(job_info) = self.jobs.get_mut(&job_id) {
615                        let CreateStreamingJobStatus::Init = replace(
616                            &mut job_info.status,
617                            CreateStreamingJobStatus::Creating {
618                                tracker: CreateMviewProgressTracker::new(
619                                    info,
620                                    version_stats,
621                                    &job_info.fragment_infos,
622                                ),
623                            },
624                        ) else {
625                            unreachable!("should be init before collect the first barrier")
626                        };
627                    } else {
628                        info!(%job_id, "newly create job get cancelled before first barrier is collected")
629                    }
630                }
631                CreateStreamingJobType::SnapshotBackfill { .. }
632                | CreateStreamingJobType::BatchRefresh(_) => {
633                    // The progress of SnapshotBackfill/BatchRefresh won't be tracked here
634                }
635            }
636        }
637        if let PostCollectCommand::Reschedule { reschedules, .. } = command {
638            // During reschedule we expect fragments to be rebuilt with new actors and no vnode bitmap update.
639            debug_assert!(
640                reschedules
641                    .values()
642                    .all(|reschedule| reschedule.vnode_bitmap_updates.is_empty()),
643                "Reschedule should not carry vnode bitmap updates when actors are rebuilt"
644            );
645
646            // Collect jobs that own the rescheduled fragments; de-duplicate via HashSet.
647            let related_job_ids = reschedules
648                .keys()
649                .filter_map(|fragment_id| self.fragment_location.get(fragment_id))
650                .cloned()
651                .collect::<HashSet<_>>();
652            for job_id in related_job_ids {
653                if let Some(job) = self.jobs.get_mut(&job_id)
654                    && let CreateStreamingJobStatus::Creating { tracker, .. } = &mut job.status
655                {
656                    tracker.refresh_after_reschedule(&job.fragment_infos, version_stats);
657                }
658            }
659        }
660        for progress in resps.values().flat_map(|resp| &resp.create_mview_progress) {
661            let Some(job_id) = self.fragment_location.get(&progress.fragment_id) else {
662                warn!(
663                    "update the progress of an non-existent creating streaming job: {progress:?}, which could be cancelled"
664                );
665                continue;
666            };
667            let tracker = match &mut self.jobs.get_mut(job_id).expect("should exist").status {
668                CreateStreamingJobStatus::Init => {
669                    continue;
670                }
671                CreateStreamingJobStatus::Creating { tracker, .. } => tracker,
672                CreateStreamingJobStatus::Created => {
673                    if !progress.done {
674                        warn!("update the progress of an created streaming job: {progress:?}");
675                    }
676                    continue;
677                }
678            };
679            tracker.apply_progress(progress, version_stats);
680        }
681        for progress in resps
682            .values()
683            .flat_map(|resp| &resp.cdc_table_backfill_progress)
684        {
685            let Some(job_id) = self.fragment_location.get(&progress.fragment_id) else {
686                warn!(
687                    "update the cdc progress of an non-existent creating streaming job: {progress:?}, which could be cancelled"
688                );
689                continue;
690            };
691            let Some(tracker) = &mut self
692                .jobs
693                .get_mut(job_id)
694                .expect("should exist")
695                .cdc_table_backfill_tracker
696            else {
697                warn!("update the cdc progress of an created streaming job: {progress:?}");
698                continue;
699            };
700            tracker.update_split_progress(progress);
701        }
702        // Handle CDC source offset updated events
703        for cdc_offset_updated in resps
704            .values()
705            .flat_map(|resp| &resp.cdc_source_offset_updated)
706        {
707            let source_id = cdc_offset_updated.source_id;
708            let job_id = source_id.as_share_source_job_id();
709            if let Some(job) = self.jobs.get_mut(&job_id) {
710                if let CreateStreamingJobStatus::Creating { tracker, .. } = &mut job.status {
711                    tracker.mark_cdc_source_finished();
712                }
713            } else {
714                warn!(
715                    "update cdc source offset for non-existent creating streaming job: source_id={}, job_id={}",
716                    cdc_offset_updated.source_id, job_id
717                );
718            }
719        }
720    }
721
722    fn iter_creating_job_tracker(&self) -> impl Iterator<Item = &CreateMviewProgressTracker> {
723        self.jobs.values().filter_map(|job| match &job.status {
724            CreateStreamingJobStatus::Init => None,
725            CreateStreamingJobStatus::Creating { tracker, .. } => Some(tracker),
726            CreateStreamingJobStatus::Created => None,
727        })
728    }
729
730    fn iter_mut_creating_job_tracker(
731        &mut self,
732    ) -> impl Iterator<Item = &mut CreateMviewProgressTracker> {
733        self.jobs
734            .values_mut()
735            .filter_map(|job| match &mut job.status {
736                CreateStreamingJobStatus::Init => None,
737                CreateStreamingJobStatus::Creating { tracker, .. } => Some(tracker),
738                CreateStreamingJobStatus::Created => None,
739            })
740    }
741
742    pub(super) fn has_pending_finished_jobs(&self) -> bool {
743        self.iter_creating_job_tracker()
744            .any(|tracker| tracker.is_finished())
745    }
746
747    pub(super) fn take_pending_backfill_nodes(&mut self) -> Vec<FragmentId> {
748        self.iter_mut_creating_job_tracker()
749            .flat_map(|tracker| tracker.take_pending_backfill_nodes())
750            .collect()
751    }
752
753    pub(super) fn take_staging_commit_info(&mut self) -> StagingCommitInfo {
754        let mut finished_jobs = vec![];
755        let mut table_ids_to_truncate = vec![];
756        let mut finished_cdc_table_backfill = vec![];
757        for (job_id, job) in &mut self.jobs {
758            if let CreateStreamingJobStatus::Creating { tracker, .. } = &mut job.status {
759                let (is_finished, truncate_table_ids) = tracker.collect_staging_commit_info();
760                table_ids_to_truncate.extend(truncate_table_ids);
761                if is_finished {
762                    let CreateStreamingJobStatus::Creating { tracker, .. } =
763                        replace(&mut job.status, CreateStreamingJobStatus::Created)
764                    else {
765                        unreachable!()
766                    };
767                    finished_jobs.push(tracker.into_tracking_job());
768                }
769            }
770            if let Some(tracker) = &mut job.cdc_table_backfill_tracker
771                && tracker.take_pre_completed()
772            {
773                finished_cdc_table_backfill.push(*job_id);
774            }
775        }
776        StagingCommitInfo {
777            finished_jobs,
778            table_ids_to_truncate,
779            finished_cdc_table_backfill,
780        }
781    }
782
783    pub fn fragment_subscribers(
784        &self,
785        fragment_id: FragmentId,
786    ) -> impl Iterator<Item = SubscriberId> + '_ {
787        let job_id = self.fragment_location[&fragment_id];
788        self.jobs[&job_id].subscribers.keys().copied()
789    }
790
791    pub fn job_subscribers(&self, job_id: JobId) -> impl Iterator<Item = SubscriberId> + '_ {
792        self.jobs[&job_id].subscribers.keys().copied()
793    }
794
795    pub fn max_subscription_retention(&self) -> impl Iterator<Item = (TableId, u64)> + '_ {
796        self.jobs.iter().filter_map(|(job_id, info)| {
797            info.subscribers
798                .values()
799                .filter_map(|subscriber| match subscriber {
800                    SubscriberType::Subscription(retention) => Some(*retention),
801                    SubscriberType::SnapshotBackfill => None,
802                })
803                .max()
804                .map(|max_subscription| (job_id.as_mv_table_id(), max_subscription))
805        })
806    }
807
808    pub fn register_subscriber(
809        &mut self,
810        job_id: JobId,
811        subscriber_id: SubscriberId,
812        subscriber: SubscriberType,
813    ) {
814        self.jobs
815            .get_mut(&job_id)
816            .expect("should exist")
817            .subscribers
818            .try_insert(subscriber_id, subscriber)
819            .expect("non duplicate");
820    }
821
822    pub fn unregister_subscriber(
823        &mut self,
824        job_id: JobId,
825        subscriber_id: SubscriberId,
826    ) -> Option<SubscriberType> {
827        self.jobs
828            .get_mut(&job_id)
829            .expect("should exist")
830            .subscribers
831            .remove(&subscriber_id)
832    }
833
834    pub fn update_subscription_retention(
835        &mut self,
836        job_id: JobId,
837        subscriber_id: SubscriberId,
838        retention_second: u64,
839    ) {
840        let job = self.jobs.get_mut(&job_id).expect("should exist");
841        match job.subscribers.get_mut(&subscriber_id) {
842            Some(SubscriberType::Subscription(current_retention)) => {
843                *current_retention = retention_second;
844            }
845            Some(SubscriberType::SnapshotBackfill) => {
846                warn!(
847                    %job_id,
848                    %subscriber_id,
849                    "cannot update retention for snapshot backfill subscriber"
850                );
851            }
852            None => {
853                warn!(%job_id, %subscriber_id, "subscription subscriber not found");
854            }
855        }
856    }
857
858    fn fragment_mut(&mut self, fragment_id: FragmentId) -> (&mut InflightFragmentInfo, JobId) {
859        let job_id = self.fragment_location[&fragment_id];
860        let fragment = self
861            .jobs
862            .get_mut(&job_id)
863            .expect("should exist")
864            .fragment_infos
865            .get_mut(&fragment_id)
866            .expect("should exist");
867        (fragment, job_id)
868    }
869
870    fn empty_inner(database_id: DatabaseId, shared_actor_infos: SharedActorInfos) -> Self {
871        Self {
872            database_id,
873            jobs: Default::default(),
874            fragment_location: Default::default(),
875            shared_actor_infos,
876        }
877    }
878
879    pub fn empty(database_id: DatabaseId, shared_actor_infos: SharedActorInfos) -> Self {
880        // remove the database because it's empty.
881        shared_actor_infos.remove_database(database_id);
882        Self::empty_inner(database_id, shared_actor_infos)
883    }
884
885    pub fn recover(
886        database_id: DatabaseId,
887        jobs: impl Iterator<Item = InflightStreamingJobInfo>,
888        shared_actor_infos: SharedActorInfos,
889    ) -> Self {
890        let mut info = Self::empty_inner(database_id, shared_actor_infos);
891        for job in jobs {
892            info.add_existing(job);
893        }
894        info
895    }
896
897    pub fn is_empty(&self) -> bool {
898        self.jobs.is_empty()
899    }
900
901    pub fn add_existing(&mut self, job: InflightStreamingJobInfo) {
902        let InflightStreamingJobInfo {
903            job_id,
904            fragment_infos,
905            subscribers,
906            status,
907            cdc_table_backfill_tracker,
908        } = job;
909        self.jobs
910            .try_insert(
911                job_id,
912                InflightStreamingJobInfo {
913                    job_id,
914                    subscribers,
915                    fragment_infos: Default::default(), // fill in later in pre_apply_new_fragments
916                    status,
917                    cdc_table_backfill_tracker,
918                },
919            )
920            .expect("non-duplicate");
921        self.pre_apply_new_fragments(
922            fragment_infos
923                .into_iter()
924                .map(|(fragment_id, info)| (fragment_id, job_id, info)),
925        );
926    }
927
928    /// Register a new streaming job entry (with empty `fragment_infos`).
929    pub(crate) fn pre_apply_new_job(
930        &mut self,
931        job_id: JobId,
932        cdc_table_backfill_tracker: Option<CdcTableBackfillTracker>,
933    ) {
934        {
935            self.jobs
936                .try_insert(
937                    job_id,
938                    InflightStreamingJobInfo {
939                        job_id,
940                        fragment_infos: Default::default(),
941                        subscribers: Default::default(), // no subscriber for newly create job
942                        status: CreateStreamingJobStatus::Init,
943                        cdc_table_backfill_tracker,
944                    },
945                )
946                .expect("non-duplicate");
947        }
948    }
949
950    /// Add new fragment infos and update shared actor infos.
951    pub(crate) fn pre_apply_new_fragments(
952        &mut self,
953        fragments: impl IntoIterator<Item = (FragmentId, JobId, InflightFragmentInfo)>,
954    ) {
955        {
956            let shared_infos = self.shared_actor_infos.clone();
957            let mut shared_actor_writer = shared_infos.start_writer(self.database_id);
958            for (fragment_id, job_id, info) in fragments {
959                {
960                    {
961                        let fragment_infos = self.jobs.get_mut(&job_id).expect("should exist");
962                        shared_actor_writer.upsert([(&info, job_id)]);
963                        fragment_infos
964                            .fragment_infos
965                            .try_insert(fragment_id, info)
966                            .expect("non duplicate");
967                        self.fragment_location
968                            .try_insert(fragment_id, job_id)
969                            .expect("non duplicate");
970                    }
971                }
972            }
973            shared_actor_writer.finish();
974        }
975    }
976
977    /// Pre-apply reschedule: update actors, vnode bitmaps, and splits.
978    /// The actual removal of old actors happens in `post_apply_reschedules`.
979    pub(crate) fn pre_apply_reschedule(
980        &mut self,
981        fragment_id: FragmentId,
982        new_actors: HashMap<ActorId, InflightActorInfo>,
983        actor_update_vnode_bitmap: HashMap<ActorId, Bitmap>,
984        actor_splits: HashMap<ActorId, Vec<SplitImpl>>,
985    ) {
986        {
987            {
988                {
989                    {
990                        let (info, _) = self.fragment_mut(fragment_id);
991                        let actors = &mut info.actors;
992                        for (actor_id, new_vnodes) in actor_update_vnode_bitmap {
993                            actors
994                                .get_mut(&actor_id)
995                                .expect("should exist")
996                                .vnode_bitmap = Some(new_vnodes);
997                        }
998                        for (actor_id, actor) in new_actors {
999                            actors
1000                                .try_insert(actor_id as _, actor)
1001                                .expect("non-duplicate");
1002                        }
1003                        for (actor_id, splits) in actor_splits {
1004                            actors.get_mut(&actor_id).expect("should exist").splits = splits;
1005                        }
1006                        // info will be upserted into shared_actor_infos in post_apply stage
1007                    }
1008                }
1009            }
1010        }
1011    }
1012
1013    /// Replace upstream fragment IDs in merge nodes of a fragment's stream graph.
1014    pub(crate) fn pre_apply_replace_node_upstream(
1015        &mut self,
1016        fragment_id: FragmentId,
1017        replace_map: &HashMap<FragmentId, FragmentId>,
1018    ) {
1019        {
1020            {
1021                {
1022                    {
1023                        let mut remaining_fragment_ids: HashSet<_> =
1024                            replace_map.keys().cloned().collect();
1025                        let (info, _) = self.fragment_mut(fragment_id);
1026                        visit_stream_node_mut(&mut info.nodes, |node| {
1027                            if let NodeBody::Merge(m) = node
1028                                && let Some(new_upstream_fragment_id) =
1029                                    replace_map.get(&m.upstream_fragment_id)
1030                            {
1031                                if !remaining_fragment_ids.remove(&m.upstream_fragment_id) {
1032                                    if cfg!(debug_assertions) {
1033                                        panic!(
1034                                            "duplicate upstream fragment: {:?} {:?}",
1035                                            m, replace_map
1036                                        );
1037                                    } else {
1038                                        warn!(?m, ?replace_map, "duplicate upstream fragment");
1039                                    }
1040                                }
1041                                m.upstream_fragment_id = *new_upstream_fragment_id;
1042                            }
1043                        });
1044                        if cfg!(debug_assertions) {
1045                            assert!(
1046                                remaining_fragment_ids.is_empty(),
1047                                "non-existing fragment to replace: {:?} {:?} {:?}",
1048                                remaining_fragment_ids,
1049                                info.nodes,
1050                                replace_map
1051                            );
1052                        } else {
1053                            warn!(?remaining_fragment_ids, node = ?info.nodes, ?replace_map, "non-existing fragment to replace");
1054                        }
1055                    }
1056                }
1057            }
1058        }
1059    }
1060
1061    /// Add a new upstream sink node to a fragment's `UpstreamSinkUnion`.
1062    pub(crate) fn pre_apply_add_node_upstream(
1063        &mut self,
1064        fragment_id: FragmentId,
1065        new_upstream_info: &PbUpstreamSinkInfo,
1066    ) {
1067        {
1068            {
1069                {
1070                    {
1071                        let (info, _) = self.fragment_mut(fragment_id);
1072                        let mut injected = false;
1073                        visit_stream_node_mut(&mut info.nodes, |node| {
1074                            if let NodeBody::UpstreamSinkUnion(u) = node {
1075                                if cfg!(debug_assertions) {
1076                                    let current_upstream_fragment_ids = u
1077                                        .init_upstreams
1078                                        .iter()
1079                                        .map(|upstream| upstream.upstream_fragment_id)
1080                                        .collect::<HashSet<_>>();
1081                                    if current_upstream_fragment_ids
1082                                        .contains(&new_upstream_info.upstream_fragment_id)
1083                                    {
1084                                        panic!(
1085                                            "duplicate upstream fragment: {:?} {:?}",
1086                                            u, new_upstream_info
1087                                        );
1088                                    }
1089                                }
1090                                u.init_upstreams.push(new_upstream_info.clone());
1091                                injected = true;
1092                            }
1093                        });
1094                        assert!(injected, "should inject upstream into UpstreamSinkUnion");
1095                    }
1096                }
1097            }
1098        }
1099    }
1100
1101    /// Remove upstream sink nodes from a fragment's `UpstreamSinkUnion`.
1102    pub(crate) fn pre_apply_drop_node_upstream(
1103        &mut self,
1104        fragment_id: FragmentId,
1105        drop_upstream_fragment_ids: &[FragmentId],
1106    ) {
1107        if !self.fragment_location.contains_key(&fragment_id) {
1108            warn!(
1109                target_fragment_id = %fragment_id,
1110                drop_upstream_fragment_ids = ?drop_upstream_fragment_ids,
1111                "skip dropping upstream sink fragments for non-existing target fragment"
1112            );
1113            return;
1114        }
1115        {
1116            {
1117                {
1118                    {
1119                        let (info, _) = self.fragment_mut(fragment_id);
1120                        let mut removed = false;
1121                        visit_stream_node_mut(&mut info.nodes, |node| {
1122                            if let NodeBody::UpstreamSinkUnion(u) = node {
1123                                if cfg!(debug_assertions) {
1124                                    let current_upstream_fragment_ids = u
1125                                        .init_upstreams
1126                                        .iter()
1127                                        .map(|upstream| upstream.upstream_fragment_id)
1128                                        .collect::<HashSet<FragmentId>>();
1129                                    for drop_fragment_id in drop_upstream_fragment_ids {
1130                                        if !current_upstream_fragment_ids.contains(drop_fragment_id)
1131                                        {
1132                                            panic!(
1133                                                "non-existing upstream fragment to drop: {:?} {:?} {:?}",
1134                                                u, drop_upstream_fragment_ids, drop_fragment_id
1135                                            );
1136                                        }
1137                                    }
1138                                }
1139                                u.init_upstreams.retain(|upstream| {
1140                                    !drop_upstream_fragment_ids
1141                                        .contains(&upstream.upstream_fragment_id)
1142                                });
1143                                removed = true;
1144                            }
1145                        });
1146                        assert!(removed, "should remove upstream from UpstreamSinkUnion");
1147                    }
1148                }
1149            }
1150        }
1151    }
1152
1153    /// Sync inflight `nodes` so a later reschedule won't materialize new actors from stale data.
1154    pub(crate) fn pre_apply_throttle(
1155        &mut self,
1156        fragment_id: FragmentId,
1157        stream_node: &PbStreamNode,
1158    ) {
1159        // Snapshot-backfill creating jobs live outside main inflight; their throttles
1160        // flow through `on_new_upstream_barrier`.
1161        if !self.fragment_location.contains_key(&fragment_id) {
1162            return;
1163        }
1164        self.fragment_mut(fragment_id).0.nodes = stream_node.clone();
1165    }
1166
1167    /// Update split assignments for actors in fragments.
1168    pub(crate) fn pre_apply_split_assignments(
1169        &mut self,
1170        assignments: impl IntoIterator<Item = (FragmentId, HashMap<ActorId, Vec<SplitImpl>>)>,
1171    ) {
1172        {
1173            let shared_infos = self.shared_actor_infos.clone();
1174            let mut shared_actor_writer = shared_infos.start_writer(self.database_id);
1175            {
1176                {
1177                    for (fragment_id, actor_splits) in assignments {
1178                        let (info, job_id) = self.fragment_mut(fragment_id);
1179                        let actors = &mut info.actors;
1180                        for (actor_id, splits) in actor_splits {
1181                            actors.get_mut(&actor_id).expect("should exist").splits = splits;
1182                        }
1183                        shared_actor_writer.upsert([(&*info, job_id)]);
1184                    }
1185                }
1186            }
1187            shared_actor_writer.finish();
1188        }
1189    }
1190
1191    pub(super) fn build_edge(
1192        &self,
1193        info: Option<(&CreateStreamingJobCommandInfo, bool)>,
1194        replace_job: Option<&ReplaceStreamJobPlan>,
1195        new_upstream_sink: Option<&UpstreamSinkInfo>,
1196        control_stream_manager: &ControlStreamManager,
1197        stream_actors: &HashMap<FragmentId, Vec<StreamActor>>,
1198        actor_location: &HashMap<ActorId, WorkerId>,
1199    ) -> FragmentEdgeBuildResult {
1200        // `existing_fragment_ids` consists of
1201        //  - keys of `info.upstream_fragment_downstreams`, which are the `fragment_id` the upstream fragment of the newly created job
1202        //  - keys of `replace_job.upstream_fragment_downstreams`, which are the `fragment_id` of upstream fragment of replace_job,
1203        // if the upstream fragment previously exists
1204        //  - keys of `replace_upstream`, which are the `fragment_id` of downstream fragments that will update their upstream fragments,
1205        // if creating a new sink-into-table
1206        //  - should contain the `fragment_id` of the downstream table.
1207        let existing_fragment_ids = info
1208            .into_iter()
1209            .flat_map(|(info, _)| info.upstream_fragment_downstreams.keys())
1210            .chain(replace_job.into_iter().flat_map(|replace_job| {
1211                replace_job
1212                    .upstream_fragment_downstreams
1213                    .keys()
1214                    .filter(|fragment_id| {
1215                        info.map(|(info, _)| {
1216                            !info
1217                                .stream_job_fragments
1218                                .fragments
1219                                .contains_key(*fragment_id)
1220                        })
1221                        .unwrap_or(true)
1222                    })
1223                    .chain(replace_job.replace_upstream.keys())
1224            }))
1225            .chain(
1226                new_upstream_sink
1227                    .into_iter()
1228                    .map(|ctx| &ctx.new_sink_downstream.downstream_fragment_id),
1229            )
1230            .cloned();
1231        // Collect new fragments with their partial graph IDs
1232        let new_fragments = info
1233            .into_iter()
1234            .flat_map(|(info, is_snapshot_backfill)| {
1235                let partial_graph_id = to_partial_graph_id(
1236                    self.database_id,
1237                    is_snapshot_backfill.then_some(info.streaming_job.id()),
1238                );
1239                info.stream_job_fragments
1240                    .fragments
1241                    .values()
1242                    .map(move |fragment| (partial_graph_id, fragment))
1243            })
1244            .chain(replace_job.into_iter().flat_map(|replace_job| {
1245                replace_job
1246                    .new_fragments
1247                    .fragments
1248                    .values()
1249                    .chain(
1250                        replace_job
1251                            .auto_refresh_schema_sinks
1252                            .as_ref()
1253                            .into_iter()
1254                            .flat_map(move |sinks| sinks.iter().map(|sink| &sink.new_fragment)),
1255                    )
1256                    .map(|fragment| {
1257                        (
1258                            // we assume that replace job only happens in database partial graph
1259                            to_partial_graph_id(self.database_id, None),
1260                            fragment,
1261                        )
1262                    })
1263            }));
1264
1265        let mut builder = FragmentEdgeBuilder::new(
1266            // Existing fragments
1267            existing_fragment_ids
1268                .map(|fragment_id| {
1269                    (
1270                        fragment_id,
1271                        EdgeBuilderFragmentInfo::from_inflight(
1272                            self.fragment(fragment_id),
1273                            to_partial_graph_id(self.database_id, None),
1274                            control_stream_manager,
1275                        ),
1276                    )
1277                })
1278                // New fragments from create/replace jobs
1279                .chain(new_fragments.map(|(partial_graph_id, fragment)| {
1280                    (
1281                        fragment.fragment_id,
1282                        EdgeBuilderFragmentInfo::from_fragment(
1283                            fragment,
1284                            stream_actors,
1285                            actor_location,
1286                            partial_graph_id,
1287                            control_stream_manager,
1288                        ),
1289                    )
1290                })),
1291        );
1292        if let Some((info, _)) = info {
1293            builder.add_relations(&info.upstream_fragment_downstreams);
1294            builder.add_relations(&info.stream_job_fragments.downstreams);
1295        }
1296        if let Some(replace_job) = replace_job {
1297            builder.add_relations(&replace_job.upstream_fragment_downstreams);
1298            builder.add_relations(&replace_job.new_fragments.downstreams);
1299        }
1300        if let Some(new_upstream_sink) = new_upstream_sink {
1301            let sink_fragment_id = new_upstream_sink.sink_fragment_id;
1302            let new_sink_downstream = &new_upstream_sink.new_sink_downstream;
1303            builder.add_edge(sink_fragment_id, new_sink_downstream);
1304        }
1305        if let Some(replace_job) = replace_job {
1306            for (fragment_id, fragment_replacement) in &replace_job.replace_upstream {
1307                for (original_upstream_fragment_id, new_upstream_fragment_id) in
1308                    fragment_replacement
1309                {
1310                    builder.replace_upstream(
1311                        *fragment_id,
1312                        *original_upstream_fragment_id,
1313                        *new_upstream_fragment_id,
1314                    );
1315                }
1316            }
1317        }
1318        builder.build()
1319    }
1320
1321    /// Post-apply reschedule: remove actors that were marked for removal.
1322    pub(crate) fn post_apply_reschedules(
1323        &mut self,
1324        reschedules: impl IntoIterator<Item = (FragmentId, HashSet<ActorId>)>,
1325    ) {
1326        let inner = self.shared_actor_infos.clone();
1327        let mut shared_actor_writer = inner.start_writer(self.database_id);
1328        {
1329            {
1330                {
1331                    for (fragment_id, to_remove) in reschedules {
1332                        let job_id = self.fragment_location[&fragment_id];
1333                        let info = self
1334                            .jobs
1335                            .get_mut(&job_id)
1336                            .expect("should exist")
1337                            .fragment_infos
1338                            .get_mut(&fragment_id)
1339                            .expect("should exist");
1340                        for actor_id in to_remove {
1341                            assert!(info.actors.remove(&actor_id).is_some());
1342                        }
1343                        shared_actor_writer.upsert([(&*info, job_id)]);
1344                    }
1345                }
1346            }
1347        }
1348        shared_actor_writer.finish();
1349    }
1350
1351    /// Post-apply fragment removal: remove fragments and their jobs if empty.
1352    pub(crate) fn post_apply_remove_fragments(
1353        &mut self,
1354        fragment_ids: impl IntoIterator<Item = FragmentId>,
1355    ) {
1356        let inner = self.shared_actor_infos.clone();
1357        let mut shared_actor_writer = inner.start_writer(self.database_id);
1358        {
1359            {
1360                {
1361                    for fragment_id in fragment_ids {
1362                        let job_id = self
1363                            .fragment_location
1364                            .remove(&fragment_id)
1365                            .expect("should exist");
1366                        let job = self.jobs.get_mut(&job_id).expect("should exist");
1367                        let fragment = job
1368                            .fragment_infos
1369                            .remove(&fragment_id)
1370                            .expect("should exist");
1371                        shared_actor_writer.remove(&fragment);
1372                        if job.fragment_infos.is_empty() {
1373                            self.jobs.remove(&job_id).expect("should exist");
1374                        }
1375                    }
1376                }
1377            }
1378        }
1379        shared_actor_writer.finish();
1380    }
1381
1382    pub(crate) fn post_apply_remove_job(
1383        &mut self,
1384        job_id: JobId,
1385    ) -> Option<InflightStreamingJobInfo> {
1386        let job = self.jobs.remove(&job_id)?;
1387        let inner = self.shared_actor_infos.clone();
1388        let mut shared_actor_writer = inner.start_writer(self.database_id);
1389        for (fragment_id, fragment) in &job.fragment_infos {
1390            self.fragment_location
1391                .remove(fragment_id)
1392                .expect("should exist");
1393            shared_actor_writer.remove(fragment);
1394        }
1395        shared_actor_writer.finish();
1396        Some(job)
1397    }
1398}
1399
1400impl InflightFragmentInfo {
1401    /// Returns actor list to collect in the target worker node.
1402    pub(crate) fn actor_ids_to_collect(
1403        infos: impl IntoIterator<Item = &Self>,
1404    ) -> HashMap<WorkerId, HashSet<ActorId>> {
1405        let mut ret: HashMap<_, HashSet<_>> = HashMap::new();
1406        for (actor_id, actor) in infos.into_iter().flat_map(|info| info.actors.iter()) {
1407            assert!(
1408                ret.entry(actor.worker_id)
1409                    .or_default()
1410                    .insert(*actor_id as _)
1411            )
1412        }
1413        ret
1414    }
1415
1416    pub fn existing_table_ids<'a>(
1417        infos: impl IntoIterator<Item = &'a Self> + 'a,
1418    ) -> impl Iterator<Item = TableId> + 'a {
1419        infos
1420            .into_iter()
1421            .flat_map(|info| info.state_table_ids.iter().cloned())
1422    }
1423
1424    pub fn workers<'a>(
1425        infos: impl IntoIterator<Item = &'a Self> + 'a,
1426    ) -> impl Iterator<Item = WorkerId> + 'a {
1427        infos
1428            .into_iter()
1429            .flat_map(|fragment| fragment.actors.values().map(|actor| actor.worker_id))
1430    }
1431
1432    pub fn contains_worker<'a>(
1433        infos: impl IntoIterator<Item = &'a Self> + 'a,
1434        worker_id: WorkerId,
1435    ) -> bool {
1436        Self::workers(infos).any(|existing_worker_id| existing_worker_id == worker_id)
1437    }
1438}
1439
1440impl InflightDatabaseInfo {
1441    pub fn contains_worker(&self, worker_id: WorkerId) -> bool {
1442        InflightFragmentInfo::contains_worker(self.fragment_infos(), worker_id)
1443    }
1444
1445    pub fn existing_table_ids(&self) -> impl Iterator<Item = TableId> + '_ {
1446        InflightFragmentInfo::existing_table_ids(self.fragment_infos())
1447    }
1448}