Skip to main content

risingwave_meta/controller/
fragment.rs

1// Copyright 2023 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::{BTreeMap, BTreeSet, HashMap, HashSet};
17use std::sync::Arc;
18
19use anyhow::{Context, anyhow};
20use futures::TryStreamExt;
21use itertools::{Either, Itertools};
22use risingwave_common::bail;
23use risingwave_common::bitmap::Bitmap;
24use risingwave_common::catalog::{FragmentTypeFlag, FragmentTypeMask};
25use risingwave_common::hash::{VnodeCount, VnodeCountCompat};
26use risingwave_common::id::JobId;
27use risingwave_common::system_param::AdaptiveParallelismStrategy;
28use risingwave_common::system_param::adaptive_parallelism_strategy::parse_strategy;
29use risingwave_common::util::stream_graph_visitor::visit_stream_node_body;
30use risingwave_connector::source::SplitImpl;
31use risingwave_connector::source::cdc::CdcScanOptions;
32use risingwave_meta_model::fragment::DistributionType;
33use risingwave_meta_model::object::ObjectType;
34use risingwave_meta_model::prelude::{
35    Fragment as FragmentModel, FragmentRelation, FragmentSplits, Sink, StreamingJob,
36};
37use risingwave_meta_model::{
38    ActorId, ConnectorSplits, DatabaseId, DispatcherType, ExprContext, FragmentId, I32Array,
39    JobStatus, ObjectId, SchemaId, SinkId, SourceId, StreamNode, StreamingParallelism, TableId,
40    TableIdArray, VnodeBitmap, WorkerId, database, fragment, fragment_relation, fragment_splits,
41    object, sink, source, streaming_job, table,
42};
43use risingwave_meta_model_migration::{ExprTrait, OnConflict, SimpleExpr};
44use risingwave_pb::catalog::PbTable;
45use risingwave_pb::common::PbActorLocation;
46use risingwave_pb::meta::subscribe_response::{
47    Info as NotificationInfo, Operation as NotificationOperation,
48};
49use risingwave_pb::meta::table_fragments::fragment::{
50    FragmentDistributionType, PbFragmentDistributionType,
51};
52use risingwave_pb::meta::table_fragments::{PbActorStatus, PbState};
53use risingwave_pb::meta::{FragmentDistribution, PbFragmentWorkerSlotMapping};
54use risingwave_pb::source::{ConnectorSplit, PbConnectorSplits};
55use risingwave_pb::stream_plan;
56use risingwave_pb::stream_plan::stream_node::NodeBody;
57use risingwave_pb::stream_plan::{
58    PbDispatchOutputMapping, PbDispatcherType, PbStreamNode, PbStreamScanType, SinkLogStoreType,
59    StreamScanType,
60};
61use sea_orm::ActiveValue::Set;
62use sea_orm::sea_query::Expr;
63use sea_orm::{
64    ColumnTrait, ConnectionTrait, DatabaseTransaction, EntityTrait, FromQueryResult, JoinType,
65    PaginatorTrait, QueryFilter, QuerySelect, RelationTrait, StreamTrait, TransactionTrait,
66};
67use serde::{Deserialize, Serialize};
68
69use crate::barrier::{SharedActorInfos, SharedFragmentInfo, SnapshotBackfillInfo};
70use crate::controller::catalog::{CatalogController, CatalogControllerInner};
71use crate::controller::scale::{
72    FragmentRenderMap, LoadedFragmentContext, NoShuffleEnsemble, RenderedGraph,
73    find_fragment_no_shuffle_dags_detailed, load_fragment_context_for_jobs,
74    render_actor_assignments, resolve_streaming_job_definition,
75};
76use crate::controller::utils::{
77    FragmentDesc, PartialActorLocation, PartialFragmentStateTables, compose_dispatchers,
78    get_sink_fragment_by_ids, has_table_been_migrated, rebuild_fragment_mapping,
79    resolve_no_shuffle_actor_mapping,
80};
81use crate::error::MetaError;
82use crate::manager::{ActiveStreamingWorkerNodes, LocalNotification, NotificationManager};
83use crate::model::{
84    DownstreamFragmentRelation, Fragment, FragmentActorDispatchers, FragmentDownstreamRelation,
85    StreamActor, StreamContext, StreamJobFragments, StreamingJobModelContextExt as _,
86};
87use crate::rpc::ddl_controller::build_upstream_sink_info;
88use crate::stream::UpstreamSinkInfo;
89use crate::{MetaResult, model};
90
91/// Some information of running (inflight) actors.
92#[derive(Debug)]
93pub struct InflightActorInfo {
94    pub worker_id: WorkerId,
95    pub vnode_bitmap: Option<Bitmap>,
96    pub splits: Vec<SplitImpl>,
97}
98
99#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
100struct ActorInfo {
101    pub actor_id: ActorId,
102    pub fragment_id: FragmentId,
103    pub splits: ConnectorSplits,
104    pub worker_id: WorkerId,
105    pub vnode_bitmap: Option<VnodeBitmap>,
106    pub expr_context: ExprContext,
107    pub config_override: Arc<str>,
108}
109
110#[derive(Debug)]
111struct FragmentDescRow {
112    fragment_id: FragmentId,
113    job_id: JobId,
114    fragment_type_mask: i32,
115    distribution_type: DistributionType,
116    state_table_ids: TableIdArray,
117    vnode_count: i32,
118    parallelism_override: Option<StreamingParallelism>,
119    stream_node: Option<StreamNode>,
120}
121
122#[derive(Debug)]
123pub struct InflightFragmentInfo {
124    pub fragment_id: FragmentId,
125    pub distribution_type: DistributionType,
126    pub fragment_type_mask: FragmentTypeMask,
127    pub vnode_count: usize,
128    pub nodes: PbStreamNode,
129    pub actors: HashMap<ActorId, InflightActorInfo>,
130    pub state_table_ids: HashSet<TableId>,
131}
132
133#[derive(Clone, Debug)]
134pub struct FragmentServingInfo {
135    /// The query-visible result table for this job, if it has one.
136    /// Sink/source jobs have no serving target.
137    pub result_table_id: Option<TableId>,
138    pub distribution_type: FragmentDistributionType,
139    pub vnode_count: usize,
140}
141
142#[easy_ext::ext(FragmentTypeMaskExt)]
143pub impl FragmentTypeMask {
144    /// Matches fragments whose creation progress cannot be recovered after a failure.
145    fn contains_non_recoverable_fragment() -> SimpleExpr {
146        Self::intersects(FragmentTypeFlag::Values)
147    }
148
149    fn intersects(flag: FragmentTypeFlag) -> SimpleExpr {
150        Expr::col(fragment::Column::FragmentTypeMask)
151            .bit_and(Expr::value(flag as i32))
152            .ne(0)
153    }
154
155    fn intersects_any(flags: impl IntoIterator<Item = FragmentTypeFlag>) -> SimpleExpr {
156        Expr::col(fragment::Column::FragmentTypeMask)
157            .bit_and(Expr::value(FragmentTypeFlag::raw_flag(flags) as i32))
158            .ne(0)
159    }
160
161    fn disjoint(flag: FragmentTypeFlag) -> SimpleExpr {
162        Expr::col(fragment::Column::FragmentTypeMask)
163            .bit_and(Expr::value(flag as i32))
164            .eq(0)
165    }
166}
167
168#[derive(Clone, Debug, FromQueryResult, Serialize, Deserialize)]
169#[serde(rename_all = "camelCase")] // for dashboard
170pub struct StreamingJobInfo {
171    pub job_id: JobId,
172    pub obj_type: ObjectType,
173    pub name: String,
174    pub job_status: JobStatus,
175    pub parallelism: StreamingParallelism,
176    pub adaptive_parallelism_strategy: Option<String>,
177    // These backfill fields are only used to derive the effective parallelism shown for
178    // in-progress jobs. They are skipped in JSON responses so existing dashboard payloads keep
179    // the same schema.
180    #[serde(skip)]
181    pub backfill_parallelism: Option<StreamingParallelism>,
182    #[serde(skip)]
183    pub backfill_adaptive_parallelism_strategy: Option<String>,
184    pub max_parallelism: i32,
185    pub resource_group: String,
186    pub config_override: String,
187    pub database_id: DatabaseId,
188    pub schema_id: SchemaId,
189}
190
191impl NotificationManager {
192    /// Notify frontend about streaming worker slot mapping changes.
193    ///
194    /// This only sends streaming mapping updates to frontends. Serving mapping
195    /// notifications are decoupled and driven by fragment model changes (insert/delete)
196    /// instead of barrier-driven actor set changes.
197    pub(crate) fn notify_streaming_fragment_mapping(
198        &self,
199        operation: NotificationOperation,
200        fragment_mappings: Vec<PbFragmentWorkerSlotMapping>,
201    ) {
202        if fragment_mappings.is_empty() {
203            return;
204        }
205        // notify all fragment mappings to frontend.
206        for fragment_mapping in fragment_mappings {
207            self.notify_frontend_without_version(
208                operation,
209                NotificationInfo::StreamingWorkerSlotMapping(fragment_mapping),
210            );
211        }
212    }
213
214    /// Notify the serving module about fragment mapping changes.
215    ///
216    /// This should be called when fragments are inserted into or deleted from the meta store,
217    /// so that serving vnode mappings are kept in sync with the fragment model.
218    pub(crate) fn notify_serving_fragment_mapping_update(
219        &self,
220        fragment_ids: Vec<crate::model::FragmentId>,
221    ) {
222        if fragment_ids.is_empty() {
223            return;
224        }
225        self.notify_local_subscribers(LocalNotification::ServingFragmentMappingsUpsert(
226            fragment_ids,
227        ));
228    }
229
230    /// Notify the serving module about fragment mapping deletions.
231    ///
232    /// This should be called when fragments are deleted from the meta store,
233    /// so that serving vnode mappings are cleaned up.
234    pub(crate) fn notify_serving_fragment_mapping_delete(
235        &self,
236        fragment_ids: Vec<crate::model::FragmentId>,
237    ) {
238        if fragment_ids.is_empty() {
239            return;
240        }
241        self.notify_local_subscribers(LocalNotification::ServingFragmentMappingsDelete(
242            fragment_ids,
243        ));
244    }
245}
246
247impl CatalogControllerInner {
248    /// Returns distribution type, vnode count and query-visible result table for all fragments.
249    ///
250    /// Reads directly from the persistent catalog rather than from the in-memory
251    /// `shared_actor_infos`. This is critical because the serving vnode mapping
252    /// must be available even before recovery has populated `shared_actor_infos`.
253    pub async fn fragment_serving_infos(
254        &self,
255    ) -> MetaResult<HashMap<FragmentId, FragmentServingInfo>> {
256        let query = FragmentModel::find().select_only().columns([
257            fragment::Column::FragmentId,
258            fragment::Column::JobId,
259            fragment::Column::DistributionType,
260            fragment::Column::VnodeCount,
261            fragment::Column::StateTableIds,
262        ]);
263        let fragments: Vec<(FragmentId, JobId, DistributionType, i32, TableIdArray)> =
264            query.into_tuple().all(&self.db).await?;
265
266        Ok(fragments
267            .into_iter()
268            .map(
269                |(fragment_id, job_id, distribution_type, vnode_count, state_table_ids)| {
270                    // Table/MV/index result tables reuse the job id and appear only in their
271                    // owning fragment; sink/source jobs therefore have no matching state table.
272                    let result_table_id = job_id.as_mv_table_id();
273                    (
274                        fragment_id,
275                        FragmentServingInfo {
276                            result_table_id: state_table_ids
277                                .0
278                                .contains(&result_table_id)
279                                .then_some(result_table_id),
280                            distribution_type: PbFragmentDistributionType::from(distribution_type),
281                            vnode_count: vnode_count as usize,
282                        },
283                    )
284                },
285            )
286            .collect())
287    }
288}
289
290impl CatalogController {
291    pub fn prepare_fragment_models_from_fragments(
292        job_id: JobId,
293        fragments: impl Iterator<Item = &Fragment>,
294    ) -> MetaResult<Vec<fragment::Model>> {
295        fragments
296            .map(|fragment| Self::prepare_fragment_model_for_new_job(job_id, fragment))
297            .try_collect()
298    }
299
300    pub fn prepare_fragment_model_for_new_job(
301        job_id: JobId,
302        fragment: &Fragment,
303    ) -> MetaResult<fragment::Model> {
304        let vnode_count = fragment.vnode_count();
305        let Fragment {
306            fragment_id: pb_fragment_id,
307            fragment_type_mask: pb_fragment_type_mask,
308            distribution_type: pb_distribution_type,
309            state_table_ids: pb_state_table_ids,
310            nodes,
311            ..
312        } = fragment;
313
314        let state_table_ids = pb_state_table_ids.clone().into();
315
316        let fragment_parallelism = nodes
317            .node_body
318            .as_ref()
319            .and_then(|body| match body {
320                NodeBody::StreamCdcScan(node) => Some(node),
321                _ => None,
322            })
323            .and_then(|node| node.options.as_ref())
324            .map(CdcScanOptions::from_proto)
325            .filter(|opts| opts.is_parallelized_backfill())
326            .map(|opts| StreamingParallelism::Fixed(opts.backfill_parallelism as usize));
327
328        let stream_node = StreamNode::from(nodes);
329
330        let distribution_type = PbFragmentDistributionType::try_from(*pb_distribution_type)
331            .unwrap()
332            .into();
333
334        #[expect(deprecated)]
335        let fragment = fragment::Model {
336            fragment_id: *pb_fragment_id as _,
337            job_id,
338            fragment_type_mask: (*pb_fragment_type_mask).into(),
339            distribution_type,
340            stream_node,
341            state_table_ids,
342            upstream_fragment_id: Default::default(),
343            vnode_count: vnode_count as _,
344            parallelism: fragment_parallelism,
345        };
346
347        Ok(fragment)
348    }
349
350    #[expect(clippy::type_complexity)]
351    fn compose_table_fragments(
352        job_id: JobId,
353        state: PbState,
354        ctx: StreamContext,
355        fragments: Vec<(fragment::Model, Vec<ActorInfo>)>,
356        max_parallelism: usize,
357        job_definition: Option<String>,
358    ) -> MetaResult<(
359        StreamJobFragments,
360        HashMap<FragmentId, Vec<StreamActor>>,
361        HashMap<crate::model::ActorId, PbActorStatus>,
362    )> {
363        let mut pb_fragments = BTreeMap::new();
364        let mut fragment_actors = HashMap::new();
365        let mut all_actor_status = HashMap::new();
366
367        for (fragment, actors) in fragments {
368            let (fragment, actors, actor_status, _) =
369                Self::compose_fragment(fragment, actors, job_definition.clone())?;
370            let fragment_id = fragment.fragment_id;
371            fragment_actors.insert(fragment_id, actors);
372            all_actor_status.extend(actor_status);
373
374            pb_fragments.insert(fragment_id, fragment);
375        }
376
377        let table_fragments = StreamJobFragments {
378            stream_job_id: job_id,
379            state: state as _,
380            fragments: pb_fragments,
381            ctx,
382            max_parallelism,
383        };
384
385        Ok((table_fragments, fragment_actors, all_actor_status))
386    }
387
388    #[expect(clippy::type_complexity)]
389    fn compose_fragment(
390        fragment: fragment::Model,
391        actors: Vec<ActorInfo>,
392        job_definition: Option<String>,
393    ) -> MetaResult<(
394        Fragment,
395        Vec<StreamActor>,
396        HashMap<crate::model::ActorId, PbActorStatus>,
397        HashMap<crate::model::ActorId, PbConnectorSplits>,
398    )> {
399        let fragment::Model {
400            fragment_id,
401            fragment_type_mask,
402            distribution_type,
403            stream_node,
404            state_table_ids,
405            vnode_count,
406            ..
407        } = fragment;
408
409        let stream_node = stream_node.to_protobuf();
410        let mut upstream_fragments = HashSet::new();
411        visit_stream_node_body(&stream_node, |body| {
412            if let NodeBody::Merge(m) = body {
413                assert!(
414                    upstream_fragments.insert(m.upstream_fragment_id),
415                    "non-duplicate upstream fragment"
416                );
417            }
418        });
419
420        let mut pb_actors = vec![];
421
422        let mut pb_actor_status = HashMap::new();
423        let mut pb_actor_splits = HashMap::new();
424
425        for actor in actors {
426            if actor.fragment_id != fragment_id {
427                bail!(
428                    "fragment id {} from actor {} is different from fragment {}",
429                    actor.fragment_id,
430                    actor.actor_id,
431                    fragment_id
432                )
433            }
434
435            let ActorInfo {
436                actor_id,
437                fragment_id,
438                worker_id,
439                splits,
440                vnode_bitmap,
441                expr_context,
442                config_override,
443                ..
444            } = actor;
445
446            let vnode_bitmap =
447                vnode_bitmap.map(|vnode_bitmap| Bitmap::from(vnode_bitmap.to_protobuf()));
448            let pb_expr_context = Some(expr_context.to_protobuf());
449
450            pb_actor_status.insert(
451                actor_id as _,
452                PbActorStatus {
453                    location: PbActorLocation::from_worker(worker_id),
454                },
455            );
456
457            pb_actor_splits.insert(actor_id as _, splits.to_protobuf());
458
459            pb_actors.push(StreamActor {
460                actor_id: actor_id as _,
461                fragment_id: fragment_id as _,
462                vnode_bitmap,
463                mview_definition: job_definition.clone().unwrap_or("".to_owned()),
464                expr_context: pb_expr_context,
465                config_override,
466            })
467        }
468
469        let pb_state_table_ids = state_table_ids.0;
470        let pb_distribution_type = PbFragmentDistributionType::from(distribution_type) as _;
471        let pb_fragment = Fragment {
472            fragment_id: fragment_id as _,
473            fragment_type_mask: fragment_type_mask.into(),
474            distribution_type: pb_distribution_type,
475            state_table_ids: pb_state_table_ids,
476            maybe_vnode_count: VnodeCount::set(vnode_count).to_protobuf(),
477            nodes: stream_node,
478        };
479
480        Ok((pb_fragment, pb_actors, pb_actor_status, pb_actor_splits))
481    }
482
483    pub async fn fragment_serving_infos(
484        &self,
485    ) -> MetaResult<HashMap<FragmentId, FragmentServingInfo>> {
486        let inner = self.inner.read().await;
487        inner.fragment_serving_infos().await
488    }
489
490    pub async fn fragment_job_mapping(&self) -> MetaResult<HashMap<FragmentId, JobId>> {
491        let inner = self.inner.read().await;
492        let fragment_jobs: Vec<(FragmentId, JobId)> = FragmentModel::find()
493            .select_only()
494            .columns([fragment::Column::FragmentId, fragment::Column::JobId])
495            .into_tuple()
496            .all(&inner.db)
497            .await?;
498        Ok(fragment_jobs.into_iter().collect())
499    }
500
501    pub async fn get_fragment_job_id(
502        &self,
503        fragment_ids: Vec<FragmentId>,
504    ) -> MetaResult<Vec<ObjectId>> {
505        let inner = self.inner.read().await;
506        self.get_fragment_job_id_in_txn(&inner.db, fragment_ids)
507            .await
508    }
509
510    pub async fn get_fragment_job_id_in_txn<C>(
511        &self,
512        txn: &C,
513        fragment_ids: Vec<FragmentId>,
514    ) -> MetaResult<Vec<ObjectId>>
515    where
516        C: ConnectionTrait + Send,
517    {
518        let object_ids: Vec<ObjectId> = FragmentModel::find()
519            .select_only()
520            .column(fragment::Column::JobId)
521            .filter(fragment::Column::FragmentId.is_in(fragment_ids))
522            .into_tuple()
523            .all(txn)
524            .await?;
525
526        Ok(object_ids)
527    }
528
529    pub async fn get_fragment_desc_by_id(
530        &self,
531        fragment_id: FragmentId,
532    ) -> MetaResult<Option<(FragmentDesc, Vec<FragmentId>)>> {
533        let inner = self.inner.read().await;
534
535        let fragment_model = match FragmentModel::find_by_id(fragment_id)
536            .one(&inner.db)
537            .await?
538        {
539            Some(fragment) => fragment,
540            None => return Ok(None),
541        };
542
543        let job_parallelism: Option<(StreamingParallelism, Option<String>)> =
544            StreamingJob::find_by_id(fragment_model.job_id)
545                .select_only()
546                .columns([
547                    streaming_job::Column::Parallelism,
548                    streaming_job::Column::AdaptiveParallelismStrategy,
549                ])
550                .into_tuple::<(StreamingParallelism, Option<String>)>()
551                .one(&inner.db)
552                .await?;
553
554        let upstream_entries: Vec<(FragmentId, DispatcherType)> = FragmentRelation::find()
555            .select_only()
556            .columns([
557                fragment_relation::Column::SourceFragmentId,
558                fragment_relation::Column::DispatcherType,
559            ])
560            .filter(fragment_relation::Column::TargetFragmentId.eq(fragment_id))
561            .into_tuple()
562            .all(&inner.db)
563            .await?;
564
565        let upstreams: Vec<_> = upstream_entries
566            .into_iter()
567            .map(|(source_id, _)| source_id)
568            .collect();
569
570        let root_fragment_map = find_fragment_no_shuffle_dags_detailed(&inner.db, &[fragment_id])
571            .await
572            .map(Self::collect_root_fragment_mapping)?;
573        let root_fragments = root_fragment_map
574            .get(&fragment_id)
575            .cloned()
576            .unwrap_or_default();
577
578        let info = self.env.shared_actor_infos().read_guard();
579        let SharedFragmentInfo { actors, .. } = info
580            .get_fragment(fragment_model.fragment_id as _)
581            .unwrap_or_else(|| {
582                panic!(
583                    "Failed to retrieve fragment description: fragment {} (job_id {}) not found in shared actor info",
584                    fragment_model.fragment_id,
585                    fragment_model.job_id
586                )
587            });
588
589        let parallelism_policy = Self::format_fragment_parallelism_policy(
590            fragment_model.distribution_type,
591            fragment_model.parallelism.as_ref(),
592            job_parallelism.as_ref().map(|(parallelism, _)| parallelism),
593            job_parallelism
594                .as_ref()
595                .and_then(|(_, strategy)| strategy.as_deref()),
596            &root_fragments,
597        );
598
599        let fragment = FragmentDesc {
600            fragment_id: fragment_model.fragment_id,
601            job_id: fragment_model.job_id,
602            fragment_type_mask: fragment_model.fragment_type_mask,
603            distribution_type: fragment_model.distribution_type,
604            state_table_ids: fragment_model.state_table_ids.clone(),
605            parallelism: actors.len() as _,
606            vnode_count: fragment_model.vnode_count,
607            stream_node: fragment_model.stream_node.clone(),
608            parallelism_policy,
609        };
610
611        Ok(Some((fragment, upstreams)))
612    }
613
614    pub async fn list_fragment_database_ids(
615        &self,
616        select_fragment_ids: Option<Vec<FragmentId>>,
617    ) -> MetaResult<Vec<(FragmentId, DatabaseId)>> {
618        let inner = self.inner.read().await;
619        let select = FragmentModel::find()
620            .select_only()
621            .column(fragment::Column::FragmentId)
622            .column(object::Column::DatabaseId)
623            .join(JoinType::InnerJoin, fragment::Relation::Object.def());
624        let select = if let Some(select_fragment_ids) = select_fragment_ids {
625            select.filter(fragment::Column::FragmentId.is_in(select_fragment_ids))
626        } else {
627            select
628        };
629        Ok(select.into_tuple().all(&inner.db).await?)
630    }
631
632    pub async fn get_job_fragments_by_id(
633        &self,
634        job_id: JobId,
635    ) -> MetaResult<(
636        StreamJobFragments,
637        HashMap<FragmentId, Vec<StreamActor>>,
638        HashMap<ActorId, PbActorStatus>,
639    )> {
640        let inner = self.inner.read().await;
641
642        // Load fragments matching the job from the database
643        let fragments: Vec<_> = FragmentModel::find()
644            .filter(fragment::Column::JobId.eq(job_id))
645            .all(&inner.db)
646            .await?;
647
648        let job_info = StreamingJob::find_by_id(job_id)
649            .one(&inner.db)
650            .await?
651            .ok_or_else(|| anyhow::anyhow!("job {} not found in database", job_id))?;
652
653        let fragment_actors =
654            self.collect_fragment_actor_pairs(fragments, job_info.stream_context())?;
655
656        let job_definition = resolve_streaming_job_definition(&inner.db, &HashSet::from([job_id]))
657            .await?
658            .remove(&job_id);
659
660        Self::compose_table_fragments(
661            job_id,
662            job_info.job_status.into(),
663            job_info.stream_context(),
664            fragment_actors,
665            job_info.max_parallelism as _,
666            job_definition,
667        )
668    }
669
670    pub async fn get_fragment_actor_dispatchers(
671        &self,
672        fragment_ids: Vec<FragmentId>,
673    ) -> MetaResult<FragmentActorDispatchers> {
674        let inner = self.inner.read().await;
675
676        self.get_fragment_actor_dispatchers_txn(&inner.db, fragment_ids)
677            .await
678    }
679
680    pub async fn get_fragment_actor_dispatchers_txn(
681        &self,
682        c: &impl ConnectionTrait,
683        fragment_ids: Vec<FragmentId>,
684    ) -> MetaResult<FragmentActorDispatchers> {
685        let fragment_relations = FragmentRelation::find()
686            .filter(fragment_relation::Column::SourceFragmentId.is_in(fragment_ids))
687            .all(c)
688            .await?;
689
690        type FragmentActorInfo = (
691            DistributionType,
692            Arc<HashMap<crate::model::ActorId, Option<Bitmap>>>,
693        );
694
695        let shared_info = self.env.shared_actor_infos();
696        let mut fragment_actor_cache: HashMap<FragmentId, FragmentActorInfo> = HashMap::new();
697        let get_fragment_actors = |fragment_id: FragmentId| async move {
698            let result: MetaResult<FragmentActorInfo> = try {
699                let read_guard = shared_info.read_guard();
700
701                let fragment = read_guard.get_fragment(fragment_id as _).unwrap();
702
703                (
704                    fragment.distribution_type,
705                    Arc::new(
706                        fragment
707                            .actors
708                            .iter()
709                            .map(|(actor_id, actor_info)| {
710                                (
711                                    *actor_id,
712                                    actor_info
713                                        .vnode_bitmap
714                                        .as_ref()
715                                        .map(|bitmap| Bitmap::from(bitmap.to_protobuf())),
716                                )
717                            })
718                            .collect(),
719                    ),
720                )
721            };
722            result
723        };
724
725        let mut actor_dispatchers_map: HashMap<_, HashMap<_, Vec<_>>> = HashMap::new();
726        for fragment_relation::Model {
727            source_fragment_id,
728            target_fragment_id,
729            dispatcher_type,
730            dist_key_indices,
731            output_indices,
732            output_type_mapping,
733        } in fragment_relations
734        {
735            let (source_fragment_distribution, source_fragment_actors) = {
736                let (distribution, actors) = {
737                    match fragment_actor_cache.entry(source_fragment_id) {
738                        Entry::Occupied(entry) => entry.into_mut(),
739                        Entry::Vacant(entry) => {
740                            entry.insert(get_fragment_actors(source_fragment_id).await?)
741                        }
742                    }
743                };
744                (*distribution, actors.clone())
745            };
746            let (target_fragment_distribution, target_fragment_actors) = {
747                let (distribution, actors) = {
748                    match fragment_actor_cache.entry(target_fragment_id) {
749                        Entry::Occupied(entry) => entry.into_mut(),
750                        Entry::Vacant(entry) => {
751                            entry.insert(get_fragment_actors(target_fragment_id).await?)
752                        }
753                    }
754                };
755                (*distribution, actors.clone())
756            };
757            let output_mapping = PbDispatchOutputMapping {
758                indices: output_indices.into_u32_array(),
759                types: output_type_mapping.unwrap_or_default().to_protobuf(),
760            };
761            let (dispatchers, _) = compose_dispatchers(
762                source_fragment_distribution,
763                &source_fragment_actors,
764                target_fragment_id as _,
765                target_fragment_distribution,
766                &target_fragment_actors,
767                dispatcher_type,
768                dist_key_indices.into_u32_array(),
769                output_mapping,
770            );
771            let actor_dispatchers_map = actor_dispatchers_map
772                .entry(source_fragment_id as _)
773                .or_default();
774            for (actor_id, dispatchers) in dispatchers {
775                actor_dispatchers_map
776                    .entry(actor_id as _)
777                    .or_default()
778                    .push(dispatchers);
779            }
780        }
781        Ok(actor_dispatchers_map)
782    }
783
784    pub async fn get_fragment_downstream_relations(
785        &self,
786        fragment_ids: Vec<FragmentId>,
787    ) -> MetaResult<FragmentDownstreamRelation> {
788        let inner = self.inner.read().await;
789        self.get_fragment_downstream_relations_in_txn(&inner.db, fragment_ids)
790            .await
791    }
792
793    pub async fn get_fragment_downstream_relations_in_txn<C>(
794        &self,
795        txn: &C,
796        fragment_ids: Vec<FragmentId>,
797    ) -> MetaResult<FragmentDownstreamRelation>
798    where
799        C: ConnectionTrait + StreamTrait + Send,
800    {
801        let mut stream = FragmentRelation::find()
802            .filter(fragment_relation::Column::SourceFragmentId.is_in(fragment_ids))
803            .stream(txn)
804            .await?;
805        let mut relations = FragmentDownstreamRelation::new();
806        while let Some(relation) = stream.try_next().await? {
807            relations
808                .entry(relation.source_fragment_id as _)
809                .or_default()
810                .push(DownstreamFragmentRelation {
811                    downstream_fragment_id: relation.target_fragment_id as _,
812                    dispatcher_type: relation.dispatcher_type,
813                    dist_key_indices: relation.dist_key_indices.into_u32_array(),
814                    output_mapping: PbDispatchOutputMapping {
815                        indices: relation.output_indices.into_u32_array(),
816                        types: relation
817                            .output_type_mapping
818                            .unwrap_or_default()
819                            .to_protobuf(),
820                    },
821                });
822        }
823        Ok(relations)
824    }
825
826    pub async fn get_job_fragment_backfill_scan_type(
827        &self,
828        job_id: JobId,
829    ) -> MetaResult<HashMap<crate::model::FragmentId, PbStreamScanType>> {
830        let inner = self.inner.read().await;
831        self.get_job_fragment_backfill_scan_type_in_txn(&inner.db, job_id)
832            .await
833    }
834
835    pub async fn get_job_fragment_backfill_scan_type_in_txn<C>(
836        &self,
837        txn: &C,
838        job_id: JobId,
839    ) -> MetaResult<HashMap<crate::model::FragmentId, PbStreamScanType>>
840    where
841        C: ConnectionTrait + Send,
842    {
843        let fragments: Vec<_> = FragmentModel::find()
844            .filter(fragment::Column::JobId.eq(job_id))
845            .all(txn)
846            .await?;
847
848        let mut result = HashMap::new();
849
850        for fragment::Model {
851            fragment_id,
852            stream_node,
853            ..
854        } in fragments
855        {
856            let stream_node = stream_node.to_protobuf();
857            visit_stream_node_body(&stream_node, |body| {
858                if let NodeBody::StreamScan(node) = body {
859                    match node.stream_scan_type() {
860                        StreamScanType::Unspecified => {}
861                        scan_type => {
862                            result.insert(fragment_id as crate::model::FragmentId, scan_type);
863                        }
864                    }
865                }
866            });
867        }
868
869        Ok(result)
870    }
871
872    pub async fn count_streaming_jobs(&self) -> MetaResult<usize> {
873        let inner = self.inner.read().await;
874        let count = StreamingJob::find().count(&inner.db).await?;
875        Ok(usize::try_from(count).context("streaming job count overflow")?)
876    }
877
878    pub async fn list_streaming_job_infos(&self) -> MetaResult<Vec<StreamingJobInfo>> {
879        let inner = self.inner.read().await;
880        let job_states = StreamingJob::find()
881            .select_only()
882            .column(streaming_job::Column::JobId)
883            .join(JoinType::InnerJoin, streaming_job::Relation::Object.def())
884            .join(JoinType::InnerJoin, object::Relation::Database2.def())
885            .column(object::Column::ObjType)
886            .join(JoinType::LeftJoin, table::Relation::Object1.def().rev())
887            .join(JoinType::LeftJoin, source::Relation::Object.def().rev())
888            .join(JoinType::LeftJoin, sink::Relation::Object.def().rev())
889            .column_as(
890                Expr::if_null(
891                    Expr::col((table::Entity, table::Column::Name)),
892                    Expr::if_null(
893                        Expr::col((source::Entity, source::Column::Name)),
894                        Expr::if_null(
895                            Expr::col((sink::Entity, sink::Column::Name)),
896                            Expr::val("<unknown>"),
897                        ),
898                    ),
899                ),
900                "name",
901            )
902            .columns([
903                streaming_job::Column::JobStatus,
904                streaming_job::Column::Parallelism,
905                streaming_job::Column::AdaptiveParallelismStrategy,
906                streaming_job::Column::BackfillParallelism,
907                streaming_job::Column::BackfillAdaptiveParallelismStrategy,
908                streaming_job::Column::MaxParallelism,
909            ])
910            .column_as(
911                Expr::if_null(
912                    Expr::col((
913                        streaming_job::Entity,
914                        streaming_job::Column::SpecificResourceGroup,
915                    )),
916                    Expr::col((database::Entity, database::Column::ResourceGroup)),
917                ),
918                "resource_group",
919            )
920            .column_as(
921                Expr::if_null(
922                    Expr::col((streaming_job::Entity, streaming_job::Column::ConfigOverride)),
923                    Expr::val(""),
924                ),
925                "config_override",
926            )
927            .column(object::Column::DatabaseId)
928            .column(object::Column::SchemaId)
929            .into_model()
930            .all(&inner.db)
931            .await?;
932        Ok(job_states)
933    }
934
935    pub async fn get_max_parallelism_by_id(&self, job_id: JobId) -> MetaResult<usize> {
936        let inner = self.inner.read().await;
937        let max_parallelism: i32 = StreamingJob::find_by_id(job_id)
938            .select_only()
939            .column(streaming_job::Column::MaxParallelism)
940            .into_tuple()
941            .one(&inner.db)
942            .await?
943            .ok_or_else(|| anyhow::anyhow!("job {} not found in database", job_id))?;
944        Ok(max_parallelism as usize)
945    }
946
947    /// Try to get internal table ids of each streaming job, used by metrics collection.
948    pub async fn get_job_internal_table_ids(&self) -> Option<Vec<(JobId, Vec<TableId>)>> {
949        if let Ok(inner) = self.inner.try_read()
950            && let Ok(job_state_tables) = FragmentModel::find()
951                .select_only()
952                .columns([fragment::Column::JobId, fragment::Column::StateTableIds])
953                .into_tuple::<(JobId, I32Array)>()
954                .all(&inner.db)
955                .await
956        {
957            let mut job_internal_table_ids = HashMap::new();
958            for (job_id, state_table_ids) in job_state_tables {
959                job_internal_table_ids
960                    .entry(job_id)
961                    .or_insert_with(Vec::new)
962                    .extend(
963                        state_table_ids
964                            .into_inner()
965                            .into_iter()
966                            .map(|table_id| TableId::new(table_id as _)),
967                    );
968            }
969            return Some(job_internal_table_ids.into_iter().collect());
970        }
971        None
972    }
973
974    pub async fn has_any_running_jobs(&self) -> MetaResult<bool> {
975        let inner = self.inner.read().await;
976        let count = FragmentModel::find().count(&inner.db).await?;
977        Ok(count > 0)
978    }
979
980    pub fn worker_actor_count(&self) -> MetaResult<HashMap<WorkerId, usize>> {
981        let read_guard = self.env.shared_actor_infos().read_guard();
982        let actor_cnt: HashMap<WorkerId, _> = read_guard
983            .iter_over_fragments()
984            .flat_map(|(_, fragment)| {
985                fragment
986                    .actors
987                    .iter()
988                    .map(|(actor_id, actor)| (actor.worker_id, *actor_id))
989            })
990            .into_group_map()
991            .into_iter()
992            .map(|(k, v)| (k, v.len()))
993            .collect();
994
995        Ok(actor_cnt)
996    }
997
998    fn collect_fragment_actor_map(
999        &self,
1000        fragment_ids: &[FragmentId],
1001        stream_context: StreamContext,
1002    ) -> MetaResult<HashMap<FragmentId, Vec<ActorInfo>>> {
1003        let guard = self.env.shared_actor_infos().read_guard();
1004        let pb_expr_context = stream_context.to_expr_context();
1005        let expr_context: ExprContext = (&pb_expr_context).into();
1006
1007        let mut actor_map = HashMap::with_capacity(fragment_ids.len());
1008        for fragment_id in fragment_ids {
1009            let fragment_info = guard.get_fragment(*fragment_id as _).ok_or_else(|| {
1010                anyhow!("fragment {} not found in shared actor info", fragment_id)
1011            })?;
1012
1013            let actors = fragment_info
1014                .actors
1015                .iter()
1016                .map(|(actor_id, actor_info)| ActorInfo {
1017                    actor_id: *actor_id as _,
1018                    fragment_id: *fragment_id,
1019                    splits: ConnectorSplits::from(&PbConnectorSplits {
1020                        splits: actor_info.splits.iter().map(ConnectorSplit::from).collect(),
1021                    }),
1022                    worker_id: actor_info.worker_id as _,
1023                    vnode_bitmap: actor_info
1024                        .vnode_bitmap
1025                        .as_ref()
1026                        .map(|bitmap| VnodeBitmap::from(&bitmap.to_protobuf())),
1027                    expr_context: expr_context.clone(),
1028                    config_override: stream_context.config_override.clone(),
1029                })
1030                .collect();
1031
1032            actor_map.insert(*fragment_id, actors);
1033        }
1034
1035        Ok(actor_map)
1036    }
1037
1038    fn collect_fragment_actor_pairs(
1039        &self,
1040        fragments: Vec<fragment::Model>,
1041        stream_context: StreamContext,
1042    ) -> MetaResult<Vec<(fragment::Model, Vec<ActorInfo>)>> {
1043        let fragment_ids: Vec<_> = fragments.iter().map(|f| f.fragment_id).collect();
1044        let mut actor_map = self.collect_fragment_actor_map(&fragment_ids, stream_context)?;
1045        fragments
1046            .into_iter()
1047            .map(|fragment| {
1048                let actors = actor_map.remove(&fragment.fragment_id).ok_or_else(|| {
1049                    anyhow!(
1050                        "fragment {} missing in shared actor info map",
1051                        fragment.fragment_id
1052                    )
1053                })?;
1054                Ok((fragment, actors))
1055            })
1056            .collect()
1057    }
1058
1059    // TODO: This function is too heavy, we should avoid using it and implement others on demand.
1060    pub async fn table_fragments(
1061        &self,
1062    ) -> MetaResult<
1063        BTreeMap<
1064            JobId,
1065            (
1066                StreamJobFragments,
1067                HashMap<FragmentId, Vec<StreamActor>>,
1068                HashMap<crate::model::ActorId, PbActorStatus>,
1069            ),
1070        >,
1071    > {
1072        let inner = self.inner.read().await;
1073        let jobs = StreamingJob::find().all(&inner.db).await?;
1074
1075        let mut job_definition = resolve_streaming_job_definition(
1076            &inner.db,
1077            &HashSet::from_iter(jobs.iter().map(|job| job.job_id)),
1078        )
1079        .await?;
1080
1081        let mut table_fragments = BTreeMap::new();
1082        for job in jobs {
1083            let fragments = FragmentModel::find()
1084                .filter(fragment::Column::JobId.eq(job.job_id))
1085                .all(&inner.db)
1086                .await?;
1087
1088            let fragment_actors =
1089                self.collect_fragment_actor_pairs(fragments, job.stream_context())?;
1090
1091            table_fragments.insert(
1092                job.job_id,
1093                Self::compose_table_fragments(
1094                    job.job_id,
1095                    job.job_status.into(),
1096                    job.stream_context(),
1097                    fragment_actors,
1098                    job.max_parallelism as _,
1099                    job_definition.remove(&job.job_id),
1100                )?,
1101            );
1102        }
1103
1104        Ok(table_fragments)
1105    }
1106
1107    pub async fn upstream_fragments(
1108        &self,
1109        fragment_ids: impl Iterator<Item = crate::model::FragmentId>,
1110    ) -> MetaResult<HashMap<crate::model::FragmentId, HashSet<crate::model::FragmentId>>> {
1111        let inner = self.inner.read().await;
1112        self.upstream_fragments_in_txn(&inner.db, fragment_ids)
1113            .await
1114    }
1115
1116    pub async fn upstream_fragments_in_txn<C>(
1117        &self,
1118        txn: &C,
1119        fragment_ids: impl Iterator<Item = crate::model::FragmentId>,
1120    ) -> MetaResult<HashMap<crate::model::FragmentId, HashSet<crate::model::FragmentId>>>
1121    where
1122        C: ConnectionTrait + StreamTrait + Send,
1123    {
1124        let mut stream = FragmentRelation::find()
1125            .select_only()
1126            .columns([
1127                fragment_relation::Column::SourceFragmentId,
1128                fragment_relation::Column::TargetFragmentId,
1129            ])
1130            .filter(
1131                fragment_relation::Column::TargetFragmentId
1132                    .is_in(fragment_ids.map(|id| id as FragmentId)),
1133            )
1134            .into_tuple::<(FragmentId, FragmentId)>()
1135            .stream(txn)
1136            .await?;
1137        let mut upstream_fragments: HashMap<_, HashSet<_>> = HashMap::new();
1138        while let Some((upstream_fragment_id, downstream_fragment_id)) = stream.try_next().await? {
1139            upstream_fragments
1140                .entry(downstream_fragment_id as crate::model::FragmentId)
1141                .or_default()
1142                .insert(upstream_fragment_id as crate::model::FragmentId);
1143        }
1144        Ok(upstream_fragments)
1145    }
1146
1147    pub fn list_actor_locations(&self) -> MetaResult<Vec<PartialActorLocation>> {
1148        let info = self.env.shared_actor_infos().read_guard();
1149
1150        let actor_locations = info
1151            .iter_over_fragments()
1152            .flat_map(|(fragment_id, fragment)| {
1153                fragment
1154                    .actors
1155                    .iter()
1156                    .map(|(actor_id, actor)| PartialActorLocation {
1157                        actor_id: *actor_id as _,
1158                        fragment_id: *fragment_id as _,
1159                        worker_id: actor.worker_id,
1160                    })
1161            })
1162            .collect_vec();
1163
1164        Ok(actor_locations)
1165    }
1166
1167    pub async fn list_actor_info(
1168        &self,
1169        database_id: Option<DatabaseId>,
1170    ) -> MetaResult<Vec<(ActorId, FragmentId, ObjectId, SchemaId, ObjectType)>> {
1171        let inner = self.inner.read().await;
1172
1173        let fragment_objects: Vec<(FragmentId, ObjectId, SchemaId, ObjectType)> =
1174            FragmentModel::find()
1175                .select_only()
1176                .join(JoinType::LeftJoin, fragment::Relation::Object.def())
1177                .column(fragment::Column::FragmentId)
1178                .column_as(object::Column::Oid, "job_id")
1179                .column_as(object::Column::SchemaId, "schema_id")
1180                .column_as(object::Column::ObjType, "type")
1181                .filter(
1182                    database_id
1183                        // Unknown IDs outside the catalog's i32 range must not overflow.
1184                        .map(|id| object::Column::DatabaseId.eq(i64::from(id.as_raw_id())))
1185                        .unwrap_or_else(|| SimpleExpr::from(true)),
1186                )
1187                .into_tuple()
1188                .all(&inner.db)
1189                .await?;
1190
1191        let actor_infos = {
1192            let info = self.env.shared_actor_infos().read_guard();
1193
1194            let mut result = Vec::new();
1195
1196            for (fragment_id, object_id, schema_id, object_type) in fragment_objects {
1197                let Some(fragment) = info.get_fragment(fragment_id as _) else {
1198                    return Err(MetaError::unavailable(format!(
1199                        "shared actor info missing for fragment {fragment_id} while listing actors"
1200                    )));
1201                };
1202
1203                for actor_id in fragment.actors.keys() {
1204                    result.push((
1205                        *actor_id as _,
1206                        fragment.fragment_id as _,
1207                        object_id,
1208                        schema_id,
1209                        object_type,
1210                    ));
1211                }
1212            }
1213
1214            result
1215        };
1216
1217        Ok(actor_infos)
1218    }
1219
1220    pub fn get_worker_slot_mappings(&self) -> Vec<PbFragmentWorkerSlotMapping> {
1221        let guard = self.env.shared_actor_info.read_guard();
1222        guard
1223            .iter_over_fragments()
1224            .map(|(_, fragment)| rebuild_fragment_mapping(fragment))
1225            .collect_vec()
1226    }
1227
1228    pub async fn list_fragment_descs_with_node(
1229        &self,
1230        is_creating: bool,
1231    ) -> MetaResult<Vec<(FragmentDistribution, Vec<FragmentId>)>> {
1232        let inner = self.inner.read().await;
1233        let txn = inner.db.begin().await?;
1234
1235        let fragments_query = Self::build_fragment_query(is_creating);
1236        let fragments: Vec<fragment::Model> = fragments_query.all(&txn).await?;
1237
1238        let rows = fragments
1239            .into_iter()
1240            .map(|fragment| FragmentDescRow {
1241                fragment_id: fragment.fragment_id,
1242                job_id: fragment.job_id,
1243                fragment_type_mask: fragment.fragment_type_mask,
1244                distribution_type: fragment.distribution_type,
1245                state_table_ids: fragment.state_table_ids,
1246                vnode_count: fragment.vnode_count,
1247                parallelism_override: fragment.parallelism,
1248                stream_node: Some(fragment.stream_node),
1249            })
1250            .collect_vec();
1251
1252        self.build_fragment_distributions(&txn, rows).await
1253    }
1254
1255    pub async fn list_fragment_descs_without_node(
1256        &self,
1257        is_creating: bool,
1258    ) -> MetaResult<Vec<(FragmentDistribution, Vec<FragmentId>)>> {
1259        let inner = self.inner.read().await;
1260        let txn = inner.db.begin().await?;
1261
1262        let fragments_query = Self::build_fragment_query(is_creating);
1263        #[expect(clippy::type_complexity)]
1264        let fragments: Vec<(
1265            FragmentId,
1266            JobId,
1267            i32,
1268            DistributionType,
1269            TableIdArray,
1270            i32,
1271            Option<StreamingParallelism>,
1272        )> = fragments_query
1273            .select_only()
1274            .columns([
1275                fragment::Column::FragmentId,
1276                fragment::Column::JobId,
1277                fragment::Column::FragmentTypeMask,
1278                fragment::Column::DistributionType,
1279                fragment::Column::StateTableIds,
1280                fragment::Column::VnodeCount,
1281                fragment::Column::Parallelism,
1282            ])
1283            .into_tuple()
1284            .all(&txn)
1285            .await?;
1286
1287        let rows = fragments
1288            .into_iter()
1289            .map(
1290                |(
1291                    fragment_id,
1292                    job_id,
1293                    fragment_type_mask,
1294                    distribution_type,
1295                    state_table_ids,
1296                    vnode_count,
1297                    parallelism_override,
1298                )| FragmentDescRow {
1299                    fragment_id,
1300                    job_id,
1301                    fragment_type_mask,
1302                    distribution_type,
1303                    state_table_ids,
1304                    vnode_count,
1305                    parallelism_override,
1306                    stream_node: None,
1307                },
1308            )
1309            .collect_vec();
1310
1311        self.build_fragment_distributions(&txn, rows).await
1312    }
1313
1314    fn build_fragment_query(is_creating: bool) -> sea_orm::Select<fragment::Entity> {
1315        if is_creating {
1316            FragmentModel::find()
1317                .join(JoinType::LeftJoin, fragment::Relation::Object.def())
1318                .join(JoinType::LeftJoin, object::Relation::StreamingJob.def())
1319                .filter(
1320                    streaming_job::Column::JobStatus
1321                        .eq(JobStatus::Initial)
1322                        .or(streaming_job::Column::JobStatus.eq(JobStatus::Creating)),
1323                )
1324        } else {
1325            FragmentModel::find()
1326        }
1327    }
1328
1329    async fn build_fragment_distributions(
1330        &self,
1331        txn: &DatabaseTransaction,
1332        rows: Vec<FragmentDescRow>,
1333    ) -> MetaResult<Vec<(FragmentDistribution, Vec<FragmentId>)>> {
1334        let fragment_ids = rows.iter().map(|row| row.fragment_id).collect_vec();
1335        let job_ids = rows.iter().map(|row| row.job_id).unique().collect_vec();
1336
1337        let job_parallelisms: HashMap<JobId, (StreamingParallelism, Option<String>)> =
1338            if fragment_ids.is_empty() {
1339                HashMap::new()
1340            } else {
1341                StreamingJob::find()
1342                    .select_only()
1343                    .columns([
1344                        streaming_job::Column::JobId,
1345                        streaming_job::Column::Parallelism,
1346                        streaming_job::Column::AdaptiveParallelismStrategy,
1347                    ])
1348                    .filter(streaming_job::Column::JobId.is_in(job_ids))
1349                    .into_tuple::<(JobId, StreamingParallelism, Option<String>)>()
1350                    .all(txn)
1351                    .await?
1352                    .into_iter()
1353                    .map(|(job_id, parallelism, strategy)| (job_id, (parallelism, strategy)))
1354                    .collect()
1355            };
1356
1357        let upstream_entries: Vec<(FragmentId, FragmentId, DispatcherType)> =
1358            if fragment_ids.is_empty() {
1359                Vec::new()
1360            } else {
1361                FragmentRelation::find()
1362                    .select_only()
1363                    .columns([
1364                        fragment_relation::Column::TargetFragmentId,
1365                        fragment_relation::Column::SourceFragmentId,
1366                        fragment_relation::Column::DispatcherType,
1367                    ])
1368                    .filter(fragment_relation::Column::TargetFragmentId.is_in(fragment_ids.clone()))
1369                    .into_tuple()
1370                    .all(txn)
1371                    .await?
1372            };
1373
1374        let mut all_upstreams: HashMap<FragmentId, Vec<FragmentId>> = HashMap::new();
1375        for (target_id, source_id, _) in upstream_entries {
1376            all_upstreams.entry(target_id).or_default().push(source_id);
1377        }
1378
1379        let root_fragment_map = if fragment_ids.is_empty() {
1380            HashMap::new()
1381        } else {
1382            let ensembles = find_fragment_no_shuffle_dags_detailed(txn, &fragment_ids).await?;
1383            Self::collect_root_fragment_mapping(ensembles)
1384        };
1385
1386        let guard = self.env.shared_actor_info.read_guard();
1387        let mut result = Vec::with_capacity(rows.len());
1388
1389        for row in rows {
1390            let parallelism = guard
1391                .get_fragment(row.fragment_id as _)
1392                .map(|fragment| fragment.actors.len())
1393                .unwrap_or_default();
1394
1395            let root_fragments = root_fragment_map
1396                .get(&row.fragment_id)
1397                .cloned()
1398                .unwrap_or_default();
1399
1400            let upstreams = all_upstreams.remove(&row.fragment_id).unwrap_or_default();
1401
1402            let parallelism_policy = Self::format_fragment_parallelism_policy(
1403                row.distribution_type,
1404                row.parallelism_override.as_ref(),
1405                job_parallelisms
1406                    .get(&row.job_id)
1407                    .map(|(parallelism, _)| parallelism),
1408                job_parallelisms
1409                    .get(&row.job_id)
1410                    .and_then(|(_, strategy)| strategy.as_deref()),
1411                &root_fragments,
1412            );
1413
1414            let fragment = FragmentDistribution {
1415                fragment_id: row.fragment_id,
1416                table_id: row.job_id,
1417                distribution_type: PbFragmentDistributionType::from(row.distribution_type) as _,
1418                state_table_ids: row.state_table_ids.0,
1419                upstream_fragment_ids: upstreams.clone(),
1420                fragment_type_mask: row.fragment_type_mask as _,
1421                parallelism: parallelism as _,
1422                vnode_count: row.vnode_count as _,
1423                node: row.stream_node.map(|node| node.to_protobuf()),
1424                parallelism_policy,
1425            };
1426
1427            result.push((fragment, upstreams));
1428        }
1429
1430        Ok(result)
1431    }
1432
1433    /// The staging table of a refreshable table, read from the persisted stream nodes.
1434    pub async fn get_refresh_staging_table_id(
1435        &self,
1436        job_id: JobId,
1437    ) -> MetaResult<Option<risingwave_common::catalog::TableId>> {
1438        let inner = self.inner.read().await;
1439        let stream_nodes: Vec<(StreamNode,)> = FragmentModel::find()
1440            .select_only()
1441            .column(fragment::Column::StreamNode)
1442            .filter(fragment::Column::JobId.eq(job_id))
1443            .filter(FragmentTypeMask::intersects(FragmentTypeFlag::Mview))
1444            .into_tuple()
1445            .all(&inner.db)
1446            .await?;
1447        let mut staging_table_id = None;
1448        for (stream_node,) in stream_nodes {
1449            visit_stream_node_body(&stream_node.to_protobuf(), |body| {
1450                if let NodeBody::Materialize(node) = body
1451                    && let Some(table) = &node.staging_table
1452                {
1453                    staging_table_id = Some(table.id);
1454                }
1455            });
1456        }
1457        Ok(staging_table_id)
1458    }
1459
1460    pub async fn list_sink_log_store_tables(&self) -> MetaResult<Vec<(SinkId, TableId)>> {
1461        let inner = self.inner.read().await;
1462        let txn = inner.db.begin().await?;
1463
1464        let fragments: Vec<(JobId, StreamNode)> = FragmentModel::find()
1465            .select_only()
1466            .columns([fragment::Column::JobId, fragment::Column::StreamNode])
1467            .filter(FragmentTypeMask::intersects(FragmentTypeFlag::Sink))
1468            .into_tuple()
1469            .all(&txn)
1470            .await?;
1471
1472        let mut mapping: HashMap<SinkId, TableId> = HashMap::new();
1473
1474        for (job_id, stream_node) in fragments {
1475            let sink_id = SinkId::new(job_id.as_raw_id());
1476            let stream_node = stream_node.to_protobuf();
1477            let mut internal_table_id: Option<TableId> = None;
1478
1479            visit_stream_node_body(&stream_node, |body| {
1480                if let NodeBody::Sink(node) = body
1481                    && node.log_store_type == SinkLogStoreType::KvLogStore as i32
1482                    && let Some(table) = &node.table
1483                {
1484                    internal_table_id = Some(TableId::new(table.id.as_raw_id()));
1485                }
1486            });
1487
1488            if let Some(table_id) = internal_table_id
1489                && let Some(existing) = mapping.insert(sink_id, table_id)
1490                && existing != table_id
1491            {
1492                tracing::warn!(
1493                    "sink {sink_id:?} has multiple log store tables: {existing:?} vs {table_id:?}"
1494                );
1495            }
1496        }
1497
1498        Ok(mapping.into_iter().collect())
1499    }
1500
1501    /// Build a fragment-to-root lookup for all reported root fragment ensembles.
1502    fn collect_root_fragment_mapping(
1503        ensembles: Vec<NoShuffleEnsemble>,
1504    ) -> HashMap<FragmentId, Vec<FragmentId>> {
1505        let mut mapping = HashMap::new();
1506
1507        for ensemble in ensembles {
1508            let mut roots: Vec<_> = ensemble.entry_fragments().collect();
1509            roots.sort_unstable();
1510            roots.dedup();
1511
1512            if roots.is_empty() {
1513                continue;
1514            }
1515
1516            let root_set: HashSet<_> = roots.iter().copied().collect();
1517
1518            for fragment_id in ensemble.component_fragments() {
1519                if root_set.contains(&fragment_id) {
1520                    mapping.insert(fragment_id, Vec::new());
1521                } else {
1522                    mapping.insert(fragment_id, roots.clone());
1523                }
1524            }
1525        }
1526
1527        mapping
1528    }
1529
1530    fn format_fragment_parallelism_policy(
1531        distribution_type: DistributionType,
1532        fragment_parallelism: Option<&StreamingParallelism>,
1533        job_parallelism: Option<&StreamingParallelism>,
1534        job_adaptive_parallelism_strategy: Option<&str>,
1535        root_fragments: &[FragmentId],
1536    ) -> String {
1537        if distribution_type == DistributionType::Single {
1538            return "single".to_owned();
1539        }
1540
1541        if let Some(parallelism) = fragment_parallelism {
1542            return format!(
1543                "override({})",
1544                Self::format_streaming_parallelism(parallelism, job_adaptive_parallelism_strategy)
1545            );
1546        }
1547
1548        if !root_fragments.is_empty() {
1549            let mut upstreams = root_fragments.to_vec();
1550            upstreams.sort_unstable();
1551            upstreams.dedup();
1552
1553            return format!("upstream_fragment({upstreams:?})");
1554        }
1555
1556        let inherited = job_parallelism
1557            .map(|parallelism| {
1558                Self::format_streaming_parallelism(parallelism, job_adaptive_parallelism_strategy)
1559            })
1560            .unwrap_or_else(|| "unknown".to_owned());
1561        format!("inherit({inherited})")
1562    }
1563
1564    fn format_streaming_parallelism(
1565        parallelism: &StreamingParallelism,
1566        adaptive_parallelism_strategy: Option<&str>,
1567    ) -> String {
1568        match parallelism {
1569            StreamingParallelism::Adaptive => adaptive_parallelism_strategy
1570                .and_then(Self::format_adaptive_parallelism_strategy)
1571                .unwrap_or_else(|| "adaptive".to_owned()),
1572            StreamingParallelism::Fixed(n) => n.to_string(),
1573            StreamingParallelism::Custom => adaptive_parallelism_strategy
1574                .and_then(Self::format_adaptive_parallelism_strategy)
1575                .unwrap_or_else(|| "custom".to_owned()),
1576        }
1577    }
1578
1579    fn format_adaptive_parallelism_strategy(strategy: &str) -> Option<String> {
1580        parse_strategy(strategy)
1581            .ok()
1582            .map(|strategy| match strategy {
1583                AdaptiveParallelismStrategy::Auto | AdaptiveParallelismStrategy::Full => {
1584                    "adaptive".to_owned()
1585                }
1586                AdaptiveParallelismStrategy::Bounded(n) => format!("bounded({n})"),
1587                AdaptiveParallelismStrategy::Ratio(r) => format!("ratio({r})"),
1588            })
1589    }
1590
1591    pub async fn list_sink_actor_mapping(
1592        &self,
1593    ) -> MetaResult<HashMap<SinkId, (String, Vec<ActorId>)>> {
1594        let inner = self.inner.read().await;
1595        let sink_id_names: Vec<(SinkId, String)> = Sink::find()
1596            .select_only()
1597            .columns([sink::Column::SinkId, sink::Column::Name])
1598            .into_tuple()
1599            .all(&inner.db)
1600            .await?;
1601        let (sink_ids, _): (Vec<_>, Vec<_>) = sink_id_names.iter().cloned().unzip();
1602
1603        let sink_name_mapping: HashMap<SinkId, String> = sink_id_names.into_iter().collect();
1604
1605        let actor_with_type: Vec<(ActorId, SinkId)> = {
1606            let info = self.env.shared_actor_infos().read_guard();
1607
1608            info.iter_over_fragments()
1609                .filter(|(_, fragment)| {
1610                    sink_ids.contains(&fragment.job_id.as_sink_id())
1611                        && fragment.fragment_type_mask.contains(FragmentTypeFlag::Sink)
1612                })
1613                .flat_map(|(_, fragment)| {
1614                    fragment
1615                        .actors
1616                        .keys()
1617                        .map(move |actor_id| (*actor_id as _, fragment.job_id.as_sink_id()))
1618                })
1619                .collect()
1620        };
1621
1622        let mut sink_actor_mapping = HashMap::new();
1623        for (actor_id, sink_id) in actor_with_type {
1624            sink_actor_mapping
1625                .entry(sink_id)
1626                .or_insert_with(|| (sink_name_mapping.get(&sink_id).unwrap().clone(), vec![]))
1627                .1
1628                .push(actor_id);
1629        }
1630
1631        Ok(sink_actor_mapping)
1632    }
1633
1634    pub async fn list_fragment_state_tables(&self) -> MetaResult<Vec<PartialFragmentStateTables>> {
1635        let inner = self.inner.read().await;
1636        let fragment_state_tables: Vec<PartialFragmentStateTables> = FragmentModel::find()
1637            .select_only()
1638            .columns([
1639                fragment::Column::FragmentId,
1640                fragment::Column::JobId,
1641                fragment::Column::StateTableIds,
1642            ])
1643            .into_partial_model()
1644            .all(&inner.db)
1645            .await?;
1646        Ok(fragment_state_tables)
1647    }
1648
1649    /// Used in [`crate::barrier::GlobalBarrierManager`], load all running actor that need to be sent or
1650    /// collected
1651    pub async fn load_all_actors_dynamic(
1652        &self,
1653        database_id: Option<DatabaseId>,
1654        worker_nodes: &ActiveStreamingWorkerNodes,
1655    ) -> MetaResult<FragmentRenderMap> {
1656        let loaded = self.load_fragment_context(database_id).await?;
1657
1658        if loaded.is_empty() {
1659            return Ok(HashMap::new());
1660        }
1661
1662        let RenderedGraph { fragments, .. } = render_actor_assignments(
1663            self.env.actor_id_generator(),
1664            worker_nodes.current(),
1665            &loaded,
1666        )?;
1667
1668        tracing::trace!(?fragments, "reload all actors");
1669
1670        Ok(fragments)
1671    }
1672
1673    /// Async load stage: collects all metadata required for rendering actor assignments.
1674    pub async fn load_fragment_context(
1675        &self,
1676        database_id: Option<DatabaseId>,
1677    ) -> MetaResult<LoadedFragmentContext> {
1678        let inner = self.inner.read().await;
1679        let txn = inner.db.begin().await?;
1680
1681        self.load_fragment_context_in_txn(&txn, database_id).await
1682    }
1683
1684    pub async fn load_fragment_context_in_txn<C>(
1685        &self,
1686        txn: &C,
1687        database_id: Option<DatabaseId>,
1688    ) -> MetaResult<LoadedFragmentContext>
1689    where
1690        C: ConnectionTrait,
1691    {
1692        let mut query = StreamingJob::find()
1693            .select_only()
1694            .column(streaming_job::Column::JobId);
1695
1696        if let Some(database_id) = database_id {
1697            query = query
1698                .join(JoinType::InnerJoin, streaming_job::Relation::Object.def())
1699                .filter(object::Column::DatabaseId.eq(database_id));
1700        }
1701
1702        let jobs: Vec<JobId> = query.into_tuple().all(txn).await?;
1703
1704        let jobs: HashSet<JobId> = jobs.into_iter().collect();
1705
1706        if jobs.is_empty() {
1707            return Ok(LoadedFragmentContext::default());
1708        }
1709
1710        load_fragment_context_for_jobs(txn, jobs).await
1711    }
1712
1713    #[await_tree::instrument]
1714    pub async fn fill_snapshot_backfill_epoch(
1715        &self,
1716        fragment_ids: impl Iterator<Item = FragmentId>,
1717        snapshot_backfill_info: Option<&SnapshotBackfillInfo>,
1718        cross_db_snapshot_backfill_info: &SnapshotBackfillInfo,
1719    ) -> MetaResult<()> {
1720        let inner = self.inner.write().await;
1721        let txn = inner.db.begin().await?;
1722        for fragment_id in fragment_ids {
1723            let fragment = FragmentModel::find_by_id(fragment_id)
1724                .one(&txn)
1725                .await?
1726                .context(format!("fragment {} not found", fragment_id))?;
1727            let mut node = fragment.stream_node.to_protobuf();
1728            if crate::stream::fill_snapshot_backfill_epoch(
1729                &mut node,
1730                snapshot_backfill_info,
1731                cross_db_snapshot_backfill_info,
1732            )? {
1733                let node = StreamNode::from(&node);
1734                FragmentModel::update(fragment::ActiveModel {
1735                    fragment_id: Set(fragment_id),
1736                    stream_node: Set(node),
1737                    ..Default::default()
1738                })
1739                .exec(&txn)
1740                .await?;
1741            }
1742        }
1743        txn.commit().await?;
1744        Ok(())
1745    }
1746
1747    /// Get the actor ids of the fragment with `fragment_id` with `Running` status.
1748    pub fn get_running_actors_of_fragment(
1749        &self,
1750        fragment_id: FragmentId,
1751    ) -> MetaResult<HashSet<model::ActorId>> {
1752        let info = self.env.shared_actor_infos().read_guard();
1753
1754        let actors = info
1755            .get_fragment(fragment_id as _)
1756            .map(|SharedFragmentInfo { actors, .. }| actors.keys().copied().collect())
1757            .unwrap_or_default();
1758
1759        Ok(actors)
1760    }
1761
1762    /// Get the actor ids, and each actor's upstream source actor ids of the fragment with `fragment_id` with `Running` status.
1763    /// (`backfill_actor_id`, `upstream_source_actor_id`)
1764    pub async fn get_running_actors_for_source_backfill(
1765        &self,
1766        source_backfill_fragment_id: FragmentId,
1767        source_fragment_id: FragmentId,
1768    ) -> MetaResult<Vec<(ActorId, ActorId)>> {
1769        let inner = self.inner.read().await;
1770        let txn = inner.db.begin().await?;
1771
1772        let fragment_relation: DispatcherType = FragmentRelation::find()
1773            .select_only()
1774            .column(fragment_relation::Column::DispatcherType)
1775            .filter(fragment_relation::Column::SourceFragmentId.eq(source_fragment_id))
1776            .filter(fragment_relation::Column::TargetFragmentId.eq(source_backfill_fragment_id))
1777            .into_tuple()
1778            .one(&txn)
1779            .await?
1780            .ok_or_else(|| {
1781                anyhow!(
1782                    "no fragment connection from source fragment {} to source backfill fragment {}",
1783                    source_fragment_id,
1784                    source_backfill_fragment_id
1785                )
1786            })?;
1787
1788        if fragment_relation != DispatcherType::NoShuffle {
1789            return Err(anyhow!("expected NoShuffle but got {:?}", fragment_relation).into());
1790        }
1791
1792        let load_fragment_distribution_type = |txn, fragment_id: FragmentId| async move {
1793            let result: MetaResult<DistributionType> = try {
1794                FragmentModel::find_by_id(fragment_id)
1795                    .select_only()
1796                    .column(fragment::Column::DistributionType)
1797                    .into_tuple()
1798                    .one(txn)
1799                    .await
1800                    .map_err(MetaError::from)?
1801                    .ok_or_else(|| {
1802                        MetaError::from(anyhow!("failed to find fragment: {}", fragment_id))
1803                    })?
1804            };
1805            result
1806        };
1807
1808        let source_backfill_distribution_type =
1809            load_fragment_distribution_type(&txn, source_backfill_fragment_id).await?;
1810        let source_distribution_type =
1811            load_fragment_distribution_type(&txn, source_fragment_id).await?;
1812
1813        let load_fragment_actor_distribution =
1814            |actor_info: &SharedActorInfos,
1815             fragment_id: FragmentId|
1816             -> HashMap<crate::model::ActorId, Option<Bitmap>> {
1817                let guard = actor_info.read_guard();
1818
1819                guard
1820                    .get_fragment(fragment_id as _)
1821                    .map(|fragment| {
1822                        fragment
1823                            .actors
1824                            .iter()
1825                            .map(|(actor_id, actor)| {
1826                                (
1827                                    *actor_id as _,
1828                                    actor
1829                                        .vnode_bitmap
1830                                        .as_ref()
1831                                        .map(|bitmap| Bitmap::from(bitmap.to_protobuf())),
1832                                )
1833                            })
1834                            .collect()
1835                    })
1836                    .unwrap_or_default()
1837            };
1838
1839        let source_backfill_actors: HashMap<crate::model::ActorId, Option<Bitmap>> =
1840            load_fragment_actor_distribution(
1841                self.env.shared_actor_infos(),
1842                source_backfill_fragment_id,
1843            );
1844
1845        let source_actors =
1846            load_fragment_actor_distribution(self.env.shared_actor_infos(), source_fragment_id);
1847
1848        Ok(resolve_no_shuffle_actor_mapping(
1849            source_distribution_type,
1850            source_actors.iter().map(|(&id, bitmap)| (id, bitmap)),
1851            source_backfill_distribution_type,
1852            source_backfill_actors
1853                .iter()
1854                .map(|(&id, bitmap)| (id, bitmap)),
1855        )
1856        .into_iter()
1857        .map(|(source_actor, source_backfill_actor)| {
1858            (source_backfill_actor as _, source_actor as _)
1859        })
1860        .collect())
1861    }
1862
1863    /// Get and filter the "**root**" fragments of the specified jobs.
1864    /// The root fragment is the bottom-most fragment of its fragment graph, and can be a `MView` or a `Source`.
1865    ///
1866    /// Root fragment connects to downstream jobs.
1867    ///
1868    /// ## What can be the root fragment
1869    /// - For sink, it should have one `Sink` fragment.
1870    /// - For MV, it should have one `MView` fragment.
1871    /// - For table, it should have one `MView` fragment and one or two `Source` fragments. `MView` should be the root.
1872    /// - For source, it should have one `Source` fragment.
1873    ///
1874    /// In other words, it's the `MView` or `Sink` fragment if it exists, otherwise it's the `Source` fragment.
1875    pub async fn get_root_fragments(
1876        &self,
1877        job_ids: Vec<JobId>,
1878    ) -> MetaResult<HashMap<JobId, Fragment>> {
1879        let inner = self.inner.read().await;
1880
1881        let all_fragments = FragmentModel::find()
1882            .filter(fragment::Column::JobId.is_in(job_ids))
1883            .all(&inner.db)
1884            .await?;
1885        // job_id -> fragment
1886        let mut root_fragments = HashMap::<JobId, Fragment>::new();
1887        for fragment in all_fragments {
1888            let mask = FragmentTypeMask::from(fragment.fragment_type_mask);
1889            if mask.contains_any([FragmentTypeFlag::Mview, FragmentTypeFlag::Sink]) {
1890                _ = root_fragments.insert(fragment.job_id, fragment.into());
1891            } else if mask.contains(FragmentTypeFlag::Source) {
1892                // look for Source fragment only if there's no MView fragment
1893                // (notice try_insert here vs insert above)
1894                _ = root_fragments.try_insert(fragment.job_id, fragment.into());
1895            }
1896        }
1897
1898        Ok(root_fragments)
1899    }
1900
1901    pub async fn get_root_fragment(&self, job_id: JobId) -> MetaResult<Fragment> {
1902        let mut root_fragments = self.get_root_fragments(vec![job_id]).await?;
1903        let root_fragment = root_fragments
1904            .remove(&job_id)
1905            .context(format!("root fragment for job {} not found", job_id))?;
1906
1907        Ok(root_fragment)
1908    }
1909
1910    /// Get the downstream fragments connected to the specified job.
1911    pub async fn get_downstream_fragments(
1912        &self,
1913        job_id: JobId,
1914    ) -> MetaResult<Vec<(stream_plan::DispatcherType, Fragment)>> {
1915        let root_fragment = self.get_root_fragment(job_id).await?;
1916
1917        let inner = self.inner.read().await;
1918        let txn = inner.db.begin().await?;
1919        let downstream_fragment_relations: Vec<fragment_relation::Model> = FragmentRelation::find()
1920            .filter(
1921                fragment_relation::Column::SourceFragmentId
1922                    .eq(root_fragment.fragment_id as FragmentId),
1923            )
1924            .all(&txn)
1925            .await?;
1926
1927        let downstream_fragment_ids = downstream_fragment_relations
1928            .iter()
1929            .map(|model| model.target_fragment_id as FragmentId)
1930            .collect::<HashSet<_>>();
1931
1932        let downstream_fragments: Vec<fragment::Model> = FragmentModel::find()
1933            .filter(fragment::Column::FragmentId.is_in(downstream_fragment_ids))
1934            .all(&txn)
1935            .await?;
1936
1937        let mut downstream_fragments_map: HashMap<_, _> = downstream_fragments
1938            .into_iter()
1939            .map(|fragment| (fragment.fragment_id, fragment))
1940            .collect();
1941
1942        let mut downstream_fragments = vec![];
1943
1944        let fragment_map: HashMap<_, _> = downstream_fragment_relations
1945            .iter()
1946            .map(|model| (model.target_fragment_id, model.dispatcher_type))
1947            .collect();
1948
1949        for (fragment_id, dispatcher_type) in fragment_map {
1950            let dispatch_type = PbDispatcherType::from(dispatcher_type);
1951
1952            let fragment = downstream_fragments_map
1953                .remove(&fragment_id)
1954                .context(format!(
1955                    "downstream fragment node for id {} not found",
1956                    fragment_id
1957                ))?
1958                .into();
1959
1960            downstream_fragments.push((dispatch_type, fragment));
1961        }
1962        Ok(downstream_fragments)
1963    }
1964
1965    pub async fn load_source_fragment_ids(
1966        &self,
1967    ) -> MetaResult<HashMap<SourceId, BTreeSet<FragmentId>>> {
1968        let inner = self.inner.read().await;
1969        let fragments: Vec<(FragmentId, StreamNode)> = FragmentModel::find()
1970            .select_only()
1971            .columns([fragment::Column::FragmentId, fragment::Column::StreamNode])
1972            .filter(FragmentTypeMask::intersects(FragmentTypeFlag::Source))
1973            .into_tuple()
1974            .all(&inner.db)
1975            .await?;
1976
1977        let mut source_fragment_ids = HashMap::new();
1978        for (fragment_id, stream_node) in fragments {
1979            if let Some(source_id) = stream_node.to_protobuf().find_stream_source() {
1980                source_fragment_ids
1981                    .entry(source_id)
1982                    .or_insert_with(BTreeSet::new)
1983                    .insert(fragment_id);
1984            }
1985        }
1986        Ok(source_fragment_ids)
1987    }
1988
1989    pub async fn load_backfill_fragment_ids(
1990        &self,
1991    ) -> MetaResult<HashMap<SourceId, BTreeSet<(FragmentId, FragmentId)>>> {
1992        let inner = self.inner.read().await;
1993        let fragments: Vec<(FragmentId, StreamNode)> = FragmentModel::find()
1994            .select_only()
1995            .columns([fragment::Column::FragmentId, fragment::Column::StreamNode])
1996            .filter(FragmentTypeMask::intersects(FragmentTypeFlag::SourceScan))
1997            .into_tuple()
1998            .all(&inner.db)
1999            .await?;
2000
2001        let mut source_fragment_ids = HashMap::new();
2002        for (fragment_id, stream_node) in fragments {
2003            if let Some((source_id, upstream_source_fragment_id)) =
2004                stream_node.to_protobuf().find_source_backfill()
2005            {
2006                source_fragment_ids
2007                    .entry(source_id)
2008                    .or_insert_with(BTreeSet::new)
2009                    .insert((fragment_id, upstream_source_fragment_id));
2010            }
2011        }
2012        Ok(source_fragment_ids)
2013    }
2014
2015    pub async fn get_all_upstream_sink_infos(
2016        &self,
2017        target_table: &PbTable,
2018        target_fragment_id: FragmentId,
2019    ) -> MetaResult<Vec<UpstreamSinkInfo>> {
2020        let inner = self.inner.read().await;
2021        let txn = inner.db.begin().await?;
2022
2023        self.get_all_upstream_sink_infos_in_txn(&txn, target_table, target_fragment_id)
2024            .await
2025    }
2026
2027    pub async fn get_all_upstream_sink_infos_in_txn<C>(
2028        &self,
2029        txn: &C,
2030        target_table: &PbTable,
2031        target_fragment_id: FragmentId,
2032    ) -> MetaResult<Vec<UpstreamSinkInfo>>
2033    where
2034        C: ConnectionTrait,
2035    {
2036        let incoming_sinks = Sink::find()
2037            .filter(sink::Column::TargetTable.eq(target_table.id))
2038            .all(txn)
2039            .await?;
2040
2041        let sink_ids = incoming_sinks.iter().map(|s| s.sink_id).collect_vec();
2042        let sink_fragment_ids = get_sink_fragment_by_ids(txn, sink_ids).await?;
2043
2044        let mut upstream_sink_infos = Vec::with_capacity(incoming_sinks.len());
2045        for sink in &incoming_sinks {
2046            let sink_fragment_id =
2047                sink_fragment_ids
2048                    .get(&sink.sink_id)
2049                    .cloned()
2050                    .ok_or(anyhow::anyhow!(
2051                        "sink fragment not found for sink id {}",
2052                        sink.sink_id
2053                    ))?;
2054            let upstream_info = build_upstream_sink_info(
2055                sink.sink_id,
2056                sink.original_target_columns
2057                    .as_ref()
2058                    .map(|cols| cols.to_protobuf())
2059                    .unwrap_or_default(),
2060                sink_fragment_id,
2061                target_table,
2062                target_fragment_id,
2063            )?;
2064            upstream_sink_infos.push(upstream_info);
2065        }
2066
2067        Ok(upstream_sink_infos)
2068    }
2069
2070    pub async fn get_mview_fragment_by_id(&self, job_id: JobId) -> MetaResult<FragmentId> {
2071        let inner = self.inner.read().await;
2072        let txn = inner.db.begin().await?;
2073
2074        let mview_fragment: Vec<FragmentId> = FragmentModel::find()
2075            .select_only()
2076            .column(fragment::Column::FragmentId)
2077            .filter(
2078                fragment::Column::JobId
2079                    .eq(job_id)
2080                    .and(FragmentTypeMask::intersects(FragmentTypeFlag::Mview)),
2081            )
2082            .into_tuple()
2083            .all(&txn)
2084            .await?;
2085
2086        if mview_fragment.len() != 1 {
2087            return Err(anyhow::anyhow!(
2088                "expected exactly one mview fragment for job {}, found {}",
2089                job_id,
2090                mview_fragment.len()
2091            )
2092            .into());
2093        }
2094
2095        Ok(mview_fragment.into_iter().next().unwrap())
2096    }
2097
2098    pub async fn has_table_been_migrated(&self, table_id: TableId) -> MetaResult<bool> {
2099        let inner = self.inner.read().await;
2100        let txn = inner.db.begin().await?;
2101        has_table_been_migrated(&txn, table_id).await
2102    }
2103
2104    pub async fn update_fragment_splits<C>(
2105        &self,
2106        txn: &C,
2107        fragment_splits: &HashMap<FragmentId, Vec<SplitImpl>>,
2108    ) -> MetaResult<()>
2109    where
2110        C: ConnectionTrait,
2111    {
2112        if fragment_splits.is_empty() {
2113            return Ok(());
2114        }
2115
2116        let existing_fragment_ids: HashSet<FragmentId> = FragmentModel::find()
2117            .select_only()
2118            .column(fragment::Column::FragmentId)
2119            .filter(fragment::Column::FragmentId.is_in(fragment_splits.keys().copied()))
2120            .into_tuple()
2121            .all(txn)
2122            .await?
2123            .into_iter()
2124            .collect();
2125
2126        // Filter out stale fragment ids to avoid FK violations when split updates race with drop.
2127        let (models, skipped_fragment_ids): (Vec<_>, Vec<_>) = fragment_splits
2128            .iter()
2129            .partition_map(|(fragment_id, splits)| {
2130                if existing_fragment_ids.contains(fragment_id) {
2131                    Either::Left(fragment_splits::ActiveModel {
2132                        fragment_id: Set(*fragment_id as _),
2133                        splits: Set(Some(ConnectorSplits::from(&PbConnectorSplits {
2134                            splits: splits.iter().map(Into::into).collect_vec(),
2135                        }))),
2136                    })
2137                } else {
2138                    Either::Right(*fragment_id)
2139                }
2140            });
2141
2142        if !skipped_fragment_ids.is_empty() {
2143            tracing::warn!(
2144                skipped_fragment_ids = ?skipped_fragment_ids,
2145                total_fragment_ids = fragment_splits.len(),
2146                "skipping stale fragment split updates for missing fragments"
2147            );
2148        }
2149
2150        if models.is_empty() {
2151            return Ok(());
2152        }
2153
2154        FragmentSplits::insert_many(models)
2155            .on_conflict(
2156                OnConflict::column(fragment_splits::Column::FragmentId)
2157                    .update_column(fragment_splits::Column::Splits)
2158                    .to_owned(),
2159            )
2160            .exec(txn)
2161            .await?;
2162
2163        Ok(())
2164    }
2165}
2166
2167#[cfg(test)]
2168mod tests {
2169    use std::collections::{BTreeMap, HashMap, HashSet};
2170
2171    use itertools::Itertools;
2172    use risingwave_common::catalog::{FragmentTypeFlag, FragmentTypeMask};
2173    use risingwave_common::hash::{ActorMapping, VirtualNode, VnodeCount};
2174    use risingwave_common::id::JobId;
2175    use risingwave_common::util::iter_util::ZipEqDebug;
2176    use risingwave_common::util::stream_graph_visitor::visit_stream_node_body;
2177    use risingwave_meta_model::fragment::DistributionType;
2178    use risingwave_meta_model::*;
2179    use risingwave_pb::meta::table_fragments::fragment::PbFragmentDistributionType;
2180    use risingwave_pb::plan_common::PbExprContext;
2181    use risingwave_pb::source::{PbConnectorSplit, PbConnectorSplits};
2182    use risingwave_pb::stream_plan::stream_node::PbNodeBody;
2183    use risingwave_pb::stream_plan::{MergeNode, PbStreamNode, PbUnionNode};
2184
2185    use super::ActorInfo;
2186    use crate::MetaResult;
2187    use crate::controller::catalog::CatalogController;
2188    use crate::model::{Fragment, StreamActor};
2189
2190    type ActorUpstreams = BTreeMap<crate::model::FragmentId, HashSet<crate::model::ActorId>>;
2191
2192    type FragmentActorUpstreams = HashMap<crate::model::ActorId, ActorUpstreams>;
2193
2194    const TEST_FRAGMENT_ID: FragmentId = FragmentId::new(1);
2195
2196    const TEST_UPSTREAM_FRAGMENT_ID: FragmentId = FragmentId::new(2);
2197
2198    const TEST_JOB_ID: JobId = JobId::new(1);
2199
2200    const TEST_STATE_TABLE_ID: TableId = TableId::new(1000);
2201
2202    fn generate_upstream_actor_ids_for_actor(actor_id: ActorId) -> ActorUpstreams {
2203        let mut upstream_actor_ids = BTreeMap::new();
2204        upstream_actor_ids.insert(
2205            TEST_UPSTREAM_FRAGMENT_ID,
2206            HashSet::from_iter([(actor_id + 100)]),
2207        );
2208        upstream_actor_ids.insert(
2209            (TEST_UPSTREAM_FRAGMENT_ID + 1) as _,
2210            HashSet::from_iter([(actor_id + 200)]),
2211        );
2212        upstream_actor_ids
2213    }
2214
2215    fn generate_merger_stream_node(actor_upstream_actor_ids: &ActorUpstreams) -> PbStreamNode {
2216        let mut input = vec![];
2217        for &upstream_fragment_id in actor_upstream_actor_ids.keys() {
2218            input.push(PbStreamNode {
2219                node_body: Some(PbNodeBody::Merge(Box::new(MergeNode {
2220                    upstream_fragment_id,
2221                    ..Default::default()
2222                }))),
2223                ..Default::default()
2224            });
2225        }
2226
2227        PbStreamNode {
2228            input,
2229            node_body: Some(PbNodeBody::Union(PbUnionNode {})),
2230            ..Default::default()
2231        }
2232    }
2233
2234    #[tokio::test]
2235    async fn test_extract_fragment() -> MetaResult<()> {
2236        let actor_count = 3u32;
2237        let upstream_actor_ids: FragmentActorUpstreams = (0..actor_count)
2238            .map(|actor_id| {
2239                (
2240                    actor_id.into(),
2241                    generate_upstream_actor_ids_for_actor(actor_id.into()),
2242                )
2243            })
2244            .collect();
2245
2246        let stream_node = generate_merger_stream_node(upstream_actor_ids.values().next().unwrap());
2247
2248        let pb_fragment = Fragment {
2249            fragment_id: TEST_FRAGMENT_ID as _,
2250            fragment_type_mask: FragmentTypeMask::from(FragmentTypeFlag::Source as u32),
2251            distribution_type: PbFragmentDistributionType::Hash as _,
2252            state_table_ids: vec![TEST_STATE_TABLE_ID as _],
2253            maybe_vnode_count: VnodeCount::for_test().to_protobuf(),
2254            nodes: stream_node,
2255        };
2256
2257        let fragment =
2258            CatalogController::prepare_fragment_model_for_new_job(TEST_JOB_ID, &pb_fragment)?;
2259
2260        check_fragment(fragment, pb_fragment);
2261
2262        Ok(())
2263    }
2264
2265    #[tokio::test]
2266    async fn test_compose_fragment() -> MetaResult<()> {
2267        let actor_count = 3u32;
2268
2269        let upstream_actor_ids: FragmentActorUpstreams = (0..actor_count)
2270            .map(|actor_id| {
2271                (
2272                    actor_id.into(),
2273                    generate_upstream_actor_ids_for_actor(actor_id.into()),
2274                )
2275            })
2276            .collect();
2277
2278        let mut actor_bitmaps = ActorMapping::new_uniform(
2279            (0..actor_count).map(|i| i.into()),
2280            VirtualNode::COUNT_FOR_TEST,
2281        )
2282        .to_bitmaps();
2283
2284        let actors = (0..actor_count)
2285            .map(|actor_id| {
2286                let actor_splits = ConnectorSplits::from(&PbConnectorSplits {
2287                    splits: vec![PbConnectorSplit {
2288                        split_type: "dummy".to_owned(),
2289                        ..Default::default()
2290                    }],
2291                });
2292
2293                ActorInfo {
2294                    actor_id: actor_id.into(),
2295                    fragment_id: TEST_FRAGMENT_ID,
2296                    splits: actor_splits,
2297                    worker_id: 0.into(),
2298                    vnode_bitmap: actor_bitmaps
2299                        .remove(&actor_id)
2300                        .map(|bitmap| bitmap.to_protobuf())
2301                        .as_ref()
2302                        .map(VnodeBitmap::from),
2303                    expr_context: ExprContext::from(&PbExprContext {
2304                        time_zone: String::from("America/New_York"),
2305                        strict_mode: false,
2306                    }),
2307                    config_override: "a.b.c = true".into(),
2308                }
2309            })
2310            .collect_vec();
2311
2312        let stream_node = {
2313            let template_actor = actors.first().cloned().unwrap();
2314
2315            let template_upstream_actor_ids =
2316                upstream_actor_ids.get(&template_actor.actor_id).unwrap();
2317
2318            generate_merger_stream_node(template_upstream_actor_ids)
2319        };
2320
2321        #[expect(deprecated)]
2322        let fragment = fragment::Model {
2323            fragment_id: TEST_FRAGMENT_ID,
2324            job_id: TEST_JOB_ID,
2325            fragment_type_mask: 0,
2326            distribution_type: DistributionType::Hash,
2327            stream_node: StreamNode::from(&stream_node),
2328            state_table_ids: TableIdArray(vec![TEST_STATE_TABLE_ID]),
2329            upstream_fragment_id: Default::default(),
2330            vnode_count: VirtualNode::COUNT_FOR_TEST as _,
2331            parallelism: None,
2332        };
2333
2334        let (pb_fragment, pb_actors, pb_actor_status, pb_actor_splits) =
2335            CatalogController::compose_fragment(fragment.clone(), actors.clone(), None).unwrap();
2336
2337        assert_eq!(pb_actor_status.len(), actor_count as usize);
2338        assert!(
2339            pb_actor_status
2340                .values()
2341                .all(|actor_status| actor_status.location.is_some())
2342        );
2343        assert_eq!(pb_actor_splits.len(), actor_count as usize);
2344
2345        check_fragment(fragment, pb_fragment);
2346        check_actors(
2347            actors,
2348            &upstream_actor_ids,
2349            pb_actors,
2350            pb_actor_splits,
2351            &stream_node,
2352        );
2353
2354        Ok(())
2355    }
2356
2357    fn check_actors(
2358        actors: Vec<ActorInfo>,
2359        actor_upstreams: &FragmentActorUpstreams,
2360        pb_actors: Vec<StreamActor>,
2361        pb_actor_splits: HashMap<ActorId, PbConnectorSplits>,
2362        stream_node: &PbStreamNode,
2363    ) {
2364        for (
2365            ActorInfo {
2366                actor_id,
2367                fragment_id,
2368                splits,
2369                worker_id: _,
2370                vnode_bitmap,
2371                expr_context,
2372                ..
2373            },
2374            StreamActor {
2375                actor_id: pb_actor_id,
2376                fragment_id: pb_fragment_id,
2377                vnode_bitmap: pb_vnode_bitmap,
2378                mview_definition,
2379                expr_context: pb_expr_context,
2380                ..
2381            },
2382        ) in actors.into_iter().zip_eq_debug(pb_actors.into_iter())
2383        {
2384            assert_eq!(actor_id, pb_actor_id as ActorId);
2385            assert_eq!(fragment_id, pb_fragment_id as FragmentId);
2386
2387            assert_eq!(
2388                vnode_bitmap.map(|bitmap| bitmap.to_protobuf().into()),
2389                pb_vnode_bitmap,
2390            );
2391
2392            assert_eq!(mview_definition, "");
2393
2394            visit_stream_node_body(stream_node, |body| {
2395                if let PbNodeBody::Merge(m) = body {
2396                    assert!(
2397                        actor_upstreams
2398                            .get(&actor_id)
2399                            .unwrap()
2400                            .contains_key(&m.upstream_fragment_id)
2401                    );
2402                }
2403            });
2404
2405            assert_eq!(
2406                splits,
2407                pb_actor_splits
2408                    .get(&pb_actor_id)
2409                    .map(ConnectorSplits::from)
2410                    .unwrap_or_default()
2411            );
2412
2413            assert_eq!(Some(expr_context.to_protobuf()), pb_expr_context);
2414        }
2415    }
2416
2417    fn check_fragment(fragment: fragment::Model, pb_fragment: Fragment) {
2418        let Fragment {
2419            fragment_id,
2420            fragment_type_mask,
2421            distribution_type: pb_distribution_type,
2422            state_table_ids: pb_state_table_ids,
2423            maybe_vnode_count: _,
2424            nodes,
2425        } = pb_fragment;
2426
2427        assert_eq!(fragment_id, TEST_FRAGMENT_ID);
2428        assert_eq!(fragment_type_mask, fragment.fragment_type_mask.into());
2429        assert_eq!(
2430            pb_distribution_type,
2431            PbFragmentDistributionType::from(fragment.distribution_type)
2432        );
2433
2434        assert_eq!(pb_state_table_ids, fragment.state_table_ids.0);
2435        assert_eq!(fragment.stream_node.to_protobuf(), nodes);
2436    }
2437
2438    #[test]
2439    fn test_parallelism_policy_with_root_fragments() {
2440        #[expect(deprecated)]
2441        let fragment = fragment::Model {
2442            fragment_id: 3.into(),
2443            job_id: TEST_JOB_ID,
2444            fragment_type_mask: 0,
2445            distribution_type: DistributionType::Hash,
2446            stream_node: StreamNode::from(&PbStreamNode::default()),
2447            state_table_ids: TableIdArray::default(),
2448            upstream_fragment_id: Default::default(),
2449            vnode_count: 0,
2450            parallelism: None,
2451        };
2452
2453        let job_parallelism = StreamingParallelism::Fixed(4);
2454
2455        let policy = super::CatalogController::format_fragment_parallelism_policy(
2456            fragment.distribution_type,
2457            fragment.parallelism.as_ref(),
2458            Some(&job_parallelism),
2459            None,
2460            &[],
2461        );
2462
2463        assert_eq!(policy, "inherit(4)");
2464    }
2465
2466    #[test]
2467    fn test_parallelism_policy_with_adaptive_strategy() {
2468        #[expect(deprecated)]
2469        let fragment = fragment::Model {
2470            fragment_id: 4.into(),
2471            job_id: TEST_JOB_ID,
2472            fragment_type_mask: 0,
2473            distribution_type: DistributionType::Hash,
2474            stream_node: StreamNode::from(&PbStreamNode::default()),
2475            state_table_ids: TableIdArray::default(),
2476            upstream_fragment_id: Default::default(),
2477            vnode_count: 0,
2478            parallelism: None,
2479        };
2480
2481        let job_parallelism = StreamingParallelism::Adaptive;
2482
2483        let policy = super::CatalogController::format_fragment_parallelism_policy(
2484            fragment.distribution_type,
2485            fragment.parallelism.as_ref(),
2486            Some(&job_parallelism),
2487            Some("RATIO(0.5)"),
2488            &[],
2489        );
2490
2491        assert_eq!(policy, "inherit(ratio(0.5))");
2492    }
2493
2494    #[test]
2495    fn test_parallelism_policy_with_custom_strategy() {
2496        #[expect(deprecated)]
2497        let fragment = fragment::Model {
2498            fragment_id: 6.into(),
2499            job_id: TEST_JOB_ID,
2500            fragment_type_mask: 0,
2501            distribution_type: DistributionType::Hash,
2502            stream_node: StreamNode::from(&PbStreamNode::default()),
2503            state_table_ids: TableIdArray::default(),
2504            upstream_fragment_id: Default::default(),
2505            vnode_count: 0,
2506            parallelism: None,
2507        };
2508
2509        let job_parallelism = StreamingParallelism::Custom;
2510
2511        let policy = super::CatalogController::format_fragment_parallelism_policy(
2512            fragment.distribution_type,
2513            fragment.parallelism.as_ref(),
2514            Some(&job_parallelism),
2515            Some("BOUNDED(8)"),
2516            &[],
2517        );
2518
2519        assert_eq!(policy, "inherit(bounded(8))");
2520    }
2521
2522    #[test]
2523    fn test_parallelism_policy_with_invalid_adaptive_strategy_falls_back() {
2524        #[expect(deprecated)]
2525        let fragment = fragment::Model {
2526            fragment_id: 7.into(),
2527            job_id: TEST_JOB_ID,
2528            fragment_type_mask: 0,
2529            distribution_type: DistributionType::Hash,
2530            stream_node: StreamNode::from(&PbStreamNode::default()),
2531            state_table_ids: TableIdArray::default(),
2532            upstream_fragment_id: Default::default(),
2533            vnode_count: 0,
2534            parallelism: None,
2535        };
2536
2537        let job_parallelism = StreamingParallelism::Adaptive;
2538
2539        let policy = super::CatalogController::format_fragment_parallelism_policy(
2540            fragment.distribution_type,
2541            fragment.parallelism.as_ref(),
2542            Some(&job_parallelism),
2543            Some("NOT_A_STRATEGY"),
2544            &[],
2545        );
2546
2547        assert_eq!(policy, "inherit(adaptive)");
2548    }
2549
2550    #[test]
2551    fn test_parallelism_policy_with_upstream_roots() {
2552        #[expect(deprecated)]
2553        let fragment = fragment::Model {
2554            fragment_id: 5.into(),
2555            job_id: TEST_JOB_ID,
2556            fragment_type_mask: 0,
2557            distribution_type: DistributionType::Hash,
2558            stream_node: StreamNode::from(&PbStreamNode::default()),
2559            state_table_ids: TableIdArray::default(),
2560            upstream_fragment_id: Default::default(),
2561            vnode_count: 0,
2562            parallelism: None,
2563        };
2564
2565        let policy = super::CatalogController::format_fragment_parallelism_policy(
2566            fragment.distribution_type,
2567            fragment.parallelism.as_ref(),
2568            None,
2569            None,
2570            &[3.into(), 1.into(), 2.into(), 1.into()],
2571        );
2572
2573        assert_eq!(policy, "upstream_fragment([1, 2, 3])");
2574    }
2575}