Skip to main content

risingwave_meta/stream/stream_graph/
actor.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::{BTreeMap, HashMap};
16
17use assert_matches::assert_matches;
18use risingwave_common::bail;
19use risingwave_common::catalog::TableDesc;
20use risingwave_common::hash::{IsSingleton, VnodeCount, VnodeCountCompat};
21use risingwave_common::util::iter_util::ZipEqFast;
22use risingwave_common::util::stream_graph_visitor::{
23    visit_fragment, visit_iceberg_completion_tables, visit_tables,
24};
25use risingwave_pb::stream_plan::stream_node::NodeBody;
26use risingwave_pb::stream_plan::{
27    DispatchStrategy, DispatcherType, MergeNode, StreamNode, StreamScanType,
28};
29
30use crate::MetaResult;
31use crate::model::{Fragment, FragmentDownstreamRelation, FragmentId, FragmentReplaceUpstream};
32use crate::stream::stream_graph::fragment::{
33    CompleteStreamFragmentGraph, DownstreamExternalEdgeId, EdgeId, EitherFragment,
34    StreamFragmentEdge,
35};
36use crate::stream::stream_graph::id::GlobalFragmentId;
37use crate::stream::stream_graph::schedule;
38use crate::stream::stream_graph::schedule::Distribution;
39
40impl FragmentActorBuilder {
41    /// Rewrite the actor body.
42    ///
43    /// During this process, the following things will be done:
44    /// 1. Replace the logical `Exchange` in node's input with `Merge`, which can be executed on the
45    ///    compute nodes.
46    fn rewrite(&self) -> MetaResult<StreamNode> {
47        self.rewrite_inner(&self.node, 0)
48    }
49
50    fn rewrite_inner(&self, stream_node: &StreamNode, depth: usize) -> MetaResult<StreamNode> {
51        match stream_node.get_node_body()? {
52            // Leaf node `Exchange`.
53            NodeBody::Exchange(exchange) => {
54                // The exchange node should always be the bottom of the plan node. If we find one
55                // when the depth is 0, it means that the plan node is not well-formed.
56                if depth == 0 {
57                    bail!(
58                        "there should be no ExchangeNode on the top of the plan node: {:#?}",
59                        stream_node
60                    )
61                }
62                assert!(!stream_node.get_fields().is_empty());
63                assert!(stream_node.input.is_empty());
64
65                // Index the upstreams by the an internal edge ID.
66                let (upstream_fragment_id, _) = &self.upstreams[&EdgeId::Internal {
67                    link_id: stream_node.get_operator_id().as_raw_id(),
68                }];
69
70                let upstream_fragment_id = upstream_fragment_id.as_global_id();
71
72                Ok(StreamNode {
73                    node_body: Some(NodeBody::Merge(Box::new({
74                        MergeNode {
75                            upstream_fragment_id,
76                            upstream_dispatcher_type: exchange.get_strategy()?.r#type,
77                            ..Default::default()
78                        }
79                    }))),
80                    identity: "MergeExecutor".to_owned(),
81                    ..stream_node.clone()
82                })
83            }
84
85            // "Leaf" node `StreamScan`.
86            NodeBody::StreamScan(stream_scan) => {
87                let input = stream_node.get_input();
88                if stream_scan.stream_scan_type() == StreamScanType::CrossDbSnapshotBackfill {
89                    // CrossDbSnapshotBackfill is a special case, which doesn't have any upstream actor
90                    // and always reads from log store.
91                    assert!(input.is_empty());
92                    return Ok(stream_node.clone());
93                }
94                assert_eq!(input.len(), 2);
95
96                let merge_node = &input[0];
97                assert_matches!(merge_node.node_body, Some(NodeBody::Merge(_)));
98                let batch_plan_node = &input[1];
99                assert_matches!(batch_plan_node.node_body, Some(NodeBody::BatchPlan(_)));
100
101                // Index the upstreams by the an external edge ID.
102                let (upstream_fragment_id, upstream_no_shuffle_actor) = &self.upstreams
103                    [&EdgeId::UpstreamExternal {
104                        upstream_job_id: stream_scan.table_id.as_job_id(),
105                        downstream_fragment_id: self.fragment_id,
106                    }];
107
108                let is_shuffled_backfill = stream_scan.stream_scan_type
109                    == StreamScanType::ArrangementBackfill as i32
110                    || stream_scan.stream_scan_type == StreamScanType::SnapshotBackfill as i32;
111                if !is_shuffled_backfill {
112                    assert!(*upstream_no_shuffle_actor);
113                }
114
115                let upstream_dispatcher_type = if is_shuffled_backfill {
116                    // FIXME(kwannoel): Should the upstream dispatcher type depends on the upstream distribution?
117                    // If singleton, use `Simple` dispatcher, otherwise use `Hash` dispatcher.
118                    DispatcherType::Hash as _
119                } else {
120                    DispatcherType::NoShuffle as _
121                };
122
123                let upstream_fragment_id = upstream_fragment_id.as_global_id();
124
125                let input = vec![
126                    // Fill the merge node body with correct upstream info.
127                    StreamNode {
128                        node_body: Some(NodeBody::Merge(Box::new({
129                            MergeNode {
130                                upstream_fragment_id,
131                                upstream_dispatcher_type,
132                                ..Default::default()
133                            }
134                        }))),
135                        ..merge_node.clone()
136                    },
137                    batch_plan_node.clone(),
138                ];
139
140                Ok(StreamNode {
141                    input,
142                    ..stream_node.clone()
143                })
144            }
145
146            // "Leaf" node `CdcFilter` and `SourceBackfill`. They both `Merge` an upstream `Source`
147            // cdc_filter -> backfill -> mview
148            // source_backfill -> mview
149            NodeBody::CdcFilter(_) | NodeBody::SourceBackfill(_) => {
150                let input = stream_node.get_input();
151                assert_eq!(input.len(), 1);
152
153                let merge_node = &input[0];
154                assert_matches!(merge_node.node_body, Some(NodeBody::Merge(_)));
155
156                let upstream_source_id = match stream_node.get_node_body()? {
157                    NodeBody::CdcFilter(node) => node.upstream_source_id,
158                    NodeBody::SourceBackfill(node) => node.upstream_source_id,
159                    _ => unreachable!(),
160                };
161
162                // Index the upstreams by the an external edge ID.
163                let (upstream_fragment_id, upstream_is_no_shuffle) = &self.upstreams
164                    [&EdgeId::UpstreamExternal {
165                        upstream_job_id: upstream_source_id.as_share_source_job_id(),
166                        downstream_fragment_id: self.fragment_id,
167                    }];
168
169                assert!(
170                    *upstream_is_no_shuffle,
171                    "Upstream Cdc Source should be singleton. \
172                    SourceBackfill is NoShuffle 1-1 correspondence. \
173                    So they both should have only one upstream actor."
174                );
175
176                let upstream_fragment_id = upstream_fragment_id.as_global_id();
177
178                // rewrite the input
179                let input = vec![
180                    // Fill the merge node body with correct upstream info.
181                    StreamNode {
182                        node_body: Some(NodeBody::Merge(Box::new({
183                            MergeNode {
184                                upstream_fragment_id,
185                                upstream_dispatcher_type: DispatcherType::NoShuffle as _,
186                                ..Default::default()
187                            }
188                        }))),
189                        ..merge_node.clone()
190                    },
191                ];
192                Ok(StreamNode {
193                    input,
194                    ..stream_node.clone()
195                })
196            }
197
198            // For other nodes, visit the children recursively.
199            _ => {
200                let mut new_stream_node = stream_node.clone();
201                for (input, new_input) in stream_node
202                    .input
203                    .iter()
204                    .zip_eq_fast(&mut new_stream_node.input)
205                {
206                    *new_input = self.rewrite_inner(input, depth + 1)?;
207                }
208                Ok(new_stream_node)
209            }
210        }
211    }
212}
213
214/// The required changes to an existing external actor to build the graph of a streaming job.
215///
216/// For example, when we're creating an mview on an existing mview, we need to add new downstreams
217/// to the upstream actors, by adding new dispatchers.
218#[derive(Default)]
219struct UpstreamFragmentChange {
220    /// The new downstreams to be added.
221    new_downstreams: HashMap<GlobalFragmentId, DispatchStrategy>,
222}
223
224#[derive(Default)]
225struct DownstreamFragmentChange {
226    /// The new upstreams to be added (replaced), indexed by the edge id to upstream fragment.
227    /// `edge_id` -> new upstream fragment id
228    new_upstreams: HashMap<DownstreamExternalEdgeId, GlobalFragmentId>,
229}
230
231impl UpstreamFragmentChange {
232    /// Add a dispatcher to the external actor.
233    fn add_dispatcher(
234        &mut self,
235        downstream_fragment_id: GlobalFragmentId,
236        dispatch: DispatchStrategy,
237    ) {
238        self.new_downstreams
239            .try_insert(downstream_fragment_id, dispatch)
240            .unwrap();
241    }
242}
243
244impl DownstreamFragmentChange {
245    /// Add an upstream to the external actor.
246    fn add_upstream(
247        &mut self,
248        edge_id: DownstreamExternalEdgeId,
249        new_upstream_fragment_id: GlobalFragmentId,
250    ) {
251        self.new_upstreams
252            .try_insert(edge_id, new_upstream_fragment_id)
253            .unwrap();
254    }
255}
256
257#[derive(Debug)]
258struct FragmentActorBuilder {
259    fragment_id: GlobalFragmentId,
260    node: StreamNode,
261    downstreams: HashMap<GlobalFragmentId, DispatchStrategy>,
262    // edge_id -> (upstream fragment_id, whether the edge is no shuffle)
263    upstreams: HashMap<EdgeId, (GlobalFragmentId, bool)>,
264}
265
266impl FragmentActorBuilder {
267    fn new(fragment_id: GlobalFragmentId, node: StreamNode) -> Self {
268        Self {
269            fragment_id,
270            node,
271            downstreams: Default::default(),
272            upstreams: Default::default(),
273        }
274    }
275}
276
277/// The actual mutable state of building an actor graph.
278///
279/// When the fragments are visited in a topological order, actor builders will be added to this
280/// state and the scheduled locations will be added. As the building process is run on the
281/// **complete graph** which also contains the info of the existing (external) fragments, the info
282/// of them will be also recorded.
283#[derive(Default)]
284struct ActorGraphBuildStateInner {
285    /// The builders of the actors to be built.
286    fragment_actor_builders: BTreeMap<GlobalFragmentId, FragmentActorBuilder>,
287
288    /// The required changes to the external downstream fragment. See [`DownstreamFragmentChange`].
289    /// Indexed by the `fragment_id` of fragments that have updates on its downstream.
290    downstream_fragment_changes: BTreeMap<GlobalFragmentId, DownstreamFragmentChange>,
291
292    /// The required changes to the external upstream fragment. See [`UpstreamFragmentChange`].
293    /// /// Indexed by the `fragment_id` of fragments that have updates on its upstream.
294    upstream_fragment_changes: BTreeMap<GlobalFragmentId, UpstreamFragmentChange>,
295}
296
297/// The information of a fragment, used for parameter passing for `Inner::add_link`.
298struct FragmentLinkNode {
299    fragment_id: GlobalFragmentId,
300}
301
302impl ActorGraphBuildStateInner {
303    /// Add the new downstream fragment relation to a fragment.
304    ///
305    /// - If the fragment is to be built, the fragment relation will be added to the fragment actor builder.
306    /// - If the fragment is an external existing fragment, the fragment relation will be added to the external changes.
307    fn add_dispatcher(
308        &mut self,
309        fragment_id: GlobalFragmentId,
310        downstream_fragment_id: GlobalFragmentId,
311        dispatch: DispatchStrategy,
312    ) {
313        if let Some(builder) = self.fragment_actor_builders.get_mut(&fragment_id) {
314            builder
315                .downstreams
316                .try_insert(downstream_fragment_id, dispatch)
317                .unwrap();
318        } else {
319            self.upstream_fragment_changes
320                .entry(fragment_id)
321                .or_default()
322                .add_dispatcher(downstream_fragment_id, dispatch);
323        }
324    }
325
326    /// Add the new upstream for an actor.
327    ///
328    /// - If the actor is to be built, the upstream will be added to the actor builder.
329    /// - If the actor is an external actor, the upstream will be added to the external changes.
330    fn add_upstream(
331        &mut self,
332        fragment_id: GlobalFragmentId,
333        edge_id: EdgeId,
334        upstream_fragment_id: GlobalFragmentId,
335        is_no_shuffle: bool,
336    ) {
337        if let Some(builder) = self.fragment_actor_builders.get_mut(&fragment_id) {
338            builder
339                .upstreams
340                .try_insert(edge_id, (upstream_fragment_id, is_no_shuffle))
341                .unwrap();
342        } else {
343            let EdgeId::DownstreamExternal(edge_id) = edge_id else {
344                unreachable!("edge from internal to external must be `DownstreamExternal`")
345            };
346            self.downstream_fragment_changes
347                .entry(fragment_id)
348                .or_default()
349                .add_upstream(edge_id, upstream_fragment_id);
350        }
351    }
352
353    /// Add a "link" between two fragments in the graph.
354    ///
355    /// The `edge` will be transformed into the fragment relation (downstream - upstream) pair between two fragments,
356    /// based on the distribution and the dispatch strategy. They will be
357    /// finally transformed to `Dispatcher` and `Merge` nodes when building the actors.
358    ///
359    /// If there're existing (external) fragments, the info will be recorded in `upstream_fragment_changes` and `downstream_fragment_changes`,
360    /// instead of the actor builders.
361    fn add_link(
362        &mut self,
363        upstream: FragmentLinkNode,
364        downstream: FragmentLinkNode,
365        edge: &StreamFragmentEdge,
366    ) {
367        let dt = edge.dispatch_strategy.r#type();
368
369        match dt {
370            // For `NoShuffle`, make n "1-1" links between the actors.
371            DispatcherType::NoShuffle => {
372                // Create a new dispatcher just between these two actors.
373                self.add_dispatcher(
374                    upstream.fragment_id,
375                    downstream.fragment_id,
376                    edge.dispatch_strategy.clone(),
377                );
378
379                // Also record the upstream for the downstream actor.
380                self.add_upstream(downstream.fragment_id, edge.id, upstream.fragment_id, true);
381            }
382
383            // Otherwise, make m * n links between the actors.
384            DispatcherType::Hash | DispatcherType::Broadcast | DispatcherType::Simple => {
385                self.add_dispatcher(
386                    upstream.fragment_id,
387                    downstream.fragment_id,
388                    edge.dispatch_strategy.clone(),
389                );
390                self.add_upstream(downstream.fragment_id, edge.id, upstream.fragment_id, false);
391            }
392
393            DispatcherType::Unspecified => unreachable!(),
394        }
395    }
396}
397
398/// The mutable state of building an actor graph. See [`ActorGraphBuildStateInner`].
399struct ActorGraphBuildState {
400    /// The actual state.
401    inner: ActorGraphBuildStateInner,
402}
403
404impl ActorGraphBuildState {
405    /// Create an empty state with the given id generator.
406    fn new() -> Self {
407        Self {
408            inner: Default::default(),
409        }
410    }
411
412    /// Finish the build and return the inner state.
413    fn finish(self) -> ActorGraphBuildStateInner {
414        self.inner
415    }
416}
417
418/// The result of a built actor graph. Will be further embedded into the `Context` for building
419/// actors on the compute nodes.
420pub struct ActorGraphBuildResult {
421    /// The graph of sealed fragments, including all actors.
422    pub graph: BTreeMap<FragmentId, Fragment>,
423    /// The downstream fragments of the fragments from the new graph to be created.
424    /// Including the fragment relation to external downstream fragment.
425    pub downstream_fragment_relations: FragmentDownstreamRelation,
426
427    /// The new dispatchers to be added to the upstream mview actors. Used for MV on MV.
428    pub upstream_fragment_downstreams: FragmentDownstreamRelation,
429
430    /// The updates to be applied to the downstream fragment merge node. Used for schema change (replace
431    /// table plan).
432    pub replace_upstream: FragmentReplaceUpstream,
433}
434
435/// [`ActorGraphBuilder`] builds the actor graph for the given complete fragment graph, based on the
436/// current cluster info and the required parallelism.
437#[derive(Debug)]
438pub struct ActorGraphBuilder {
439    /// The pre-scheduled distribution for each building fragment.
440    distributions: HashMap<GlobalFragmentId, Distribution>,
441
442    /// The complete fragment graph.
443    fragment_graph: CompleteStreamFragmentGraph,
444}
445
446impl ActorGraphBuilder {
447    /// Create a new actor graph builder with the given "complete" graph. Returns an error if the
448    /// graph is failed to be scheduled.
449    pub fn new(fragment_graph: CompleteStreamFragmentGraph) -> MetaResult<Self> {
450        let expected_vnode_count = fragment_graph.max_parallelism();
451        let scheduler = schedule::Scheduler::new(expected_vnode_count)?;
452
453        let distributions = scheduler.schedule(&fragment_graph)?;
454
455        // Fill the vnode count for each internal table, based on schedule result.
456        let mut fragment_graph = fragment_graph;
457        let mut fetch_tables = HashMap::new();
458        for (id, fragment) in fragment_graph.building_fragments_mut() {
459            let mut error = None;
460            let fragment_vnode_count = distributions[id].vnode_count();
461            visit_tables(fragment, |table, _| {
462                if error.is_some() {
463                    return;
464                }
465                // There are special cases where a hash-distributed fragment contains singleton
466                // internal tables, e.g., the state table of `Source` executors.
467                let vnode_count = if table.is_singleton() {
468                    if fragment_vnode_count > 1 {
469                        tracing::info!(
470                            table.name,
471                            "found singleton table in hash-distributed fragment"
472                        );
473                    }
474                    1
475                } else {
476                    fragment_vnode_count
477                };
478                match table.vnode_count_inner().value_opt() {
479                    // Vnode count of this table is not set to placeholder, meaning that we are replacing
480                    // a streaming job, and the existing state table requires a specific vnode count.
481                    // Check if it's the same with what we derived from the schedule result.
482                    //
483                    // Typically, inconsistency should not happen as we force to align the vnode count
484                    // when planning the new streaming job in the frontend.
485                    Some(required_vnode_count) if required_vnode_count != vnode_count => {
486                        error = Some(format!(
487                            "failed to align vnode count for table {}({}): required {}, but got {}",
488                            table.id, table.name, required_vnode_count, vnode_count
489                        ));
490                    }
491                    // Normal cases.
492                    _ => table.maybe_vnode_count = VnodeCount::set(vnode_count).to_protobuf(),
493                }
494            });
495            if let Some(error) = error {
496                bail!(error);
497            }
498            visit_fragment(fragment, |body| {
499                if let NodeBody::StreamFsFetch(node) = body
500                    && let Some(table) = node
501                        .node_inner
502                        .as_ref()
503                        .and_then(|source| source.state_table.as_ref())
504                {
505                    fetch_tables.insert(table.id, table.clone());
506                }
507            });
508        }
509        // The frontend only references the Fetch state table by ID. Derive the descriptor here,
510        // after vnode counts are resolved, so it always matches the persisted Fetch catalog.
511        for fragment in fragment_graph.building_fragments_mut().values_mut() {
512            let mut result = Ok(());
513            visit_iceberg_completion_tables(fragment, |desc| {
514                if result.is_err() {
515                    return;
516                }
517                result = match fetch_tables.get(&desc.table_id) {
518                    Some(fetch) => TableDesc::from_pb_table(fetch)
519                        .try_to_protobuf()
520                        .map(|filled| *desc = filled),
521                    None => Err(anyhow::anyhow!(
522                        "Iceberg completion table {} must reference a Fetch state table in this ingestion graph",
523                        desc.table_id
524                    )),
525                };
526            });
527            result?;
528        }
529
530        Ok(Self {
531            distributions,
532            fragment_graph,
533        })
534    }
535
536    /// Build a stream graph by duplicating each fragment as parallel actors. Returns
537    /// [`ActorGraphBuildResult`] that will be further used to build actors on the compute nodes.
538    pub fn generate_graph(self) -> MetaResult<ActorGraphBuildResult> {
539        // Build the actor graph and get the final state.
540        let ActorGraphBuildStateInner {
541            fragment_actor_builders,
542            downstream_fragment_changes,
543            upstream_fragment_changes,
544        } = self.build_actor_graph()?;
545
546        let mut downstream_fragment_relations: FragmentDownstreamRelation = HashMap::new();
547        // Serialize the graph into sealed fragments.
548        let graph = {
549            // As all fragments are processed, we can now `rewrite` the stream nodes where the
550            // `Exchange` and `Chain` are rewritten.
551            let mut fragment_nodes: HashMap<GlobalFragmentId, StreamNode> = HashMap::new();
552
553            for (fragment_id, builder) in fragment_actor_builders {
554                let global_fragment_id = fragment_id.as_global_id();
555                let node = builder.rewrite()?;
556                downstream_fragment_relations
557                    .try_insert(
558                        global_fragment_id,
559                        builder
560                            .downstreams
561                            .into_iter()
562                            .map(|(id, dispatch)| (id.as_global_id(), dispatch).into())
563                            .collect(),
564                    )
565                    .expect("non-duplicate");
566                fragment_nodes
567                    .try_insert(fragment_id, node)
568                    .expect("non-duplicate");
569            }
570
571            let mut graph = BTreeMap::new();
572            for (fragment_id, stream_node) in fragment_nodes {
573                let distribution = self.distributions[&fragment_id].clone();
574                let fragment =
575                    self.fragment_graph
576                        .seal_fragment(fragment_id, distribution, stream_node);
577                let fragment_id = fragment_id.as_global_id();
578                graph.insert(fragment_id, fragment);
579            }
580            graph
581        };
582
583        // Extract the new fragment relation from the external changes.
584        let upstream_fragment_downstreams = upstream_fragment_changes
585            .into_iter()
586            .map(|(fragment_id, changes)| {
587                (
588                    fragment_id.as_global_id(),
589                    changes
590                        .new_downstreams
591                        .into_iter()
592                        .map(|(downstream_fragment_id, new_dispatch)| {
593                            (downstream_fragment_id.as_global_id(), new_dispatch).into()
594                        })
595                        .collect(),
596                )
597            })
598            .collect();
599
600        // Extract the updates for merge executors from the external changes.
601        let replace_upstream = downstream_fragment_changes
602            .into_iter()
603            .map(|(fragment_id, changes)| {
604                let fragment_id = fragment_id.as_global_id();
605                (
606                    fragment_id,
607                    changes
608                        .new_upstreams
609                        .into_iter()
610                        .map(move |(edge_id, upstream_fragment_id)| {
611                            let upstream_fragment_id = upstream_fragment_id.as_global_id();
612                            let DownstreamExternalEdgeId {
613                                original_upstream_fragment_id,
614                                ..
615                            } = edge_id;
616                            (
617                                original_upstream_fragment_id.as_global_id(),
618                                upstream_fragment_id,
619                            )
620                        })
621                        .collect(),
622                )
623            })
624            .filter(|(_, fragment_changes): &(_, HashMap<_, _>)| !fragment_changes.is_empty())
625            .collect();
626
627        Ok(ActorGraphBuildResult {
628            graph,
629            downstream_fragment_relations,
630            upstream_fragment_downstreams,
631            replace_upstream,
632        })
633    }
634
635    /// Build actor graph for each fragment, using topological order.
636    fn build_actor_graph(&self) -> MetaResult<ActorGraphBuildStateInner> {
637        let mut state = ActorGraphBuildState::new();
638
639        // Use topological sort to build the graph from downstream to upstream. (The first fragment
640        // popped out from the heap will be the top-most node in plan, or the sink in stream graph.)
641        for fragment_id in self.fragment_graph.topo_order()? {
642            self.build_actor_graph_fragment(fragment_id, &mut state)?;
643        }
644
645        Ok(state.finish())
646    }
647
648    /// Build actor graph for a specific fragment.
649    fn build_actor_graph_fragment(
650        &self,
651        fragment_id: GlobalFragmentId,
652        state: &mut ActorGraphBuildState,
653    ) -> MetaResult<()> {
654        let current_fragment = self.fragment_graph.get_fragment(fragment_id);
655
656        // First, add or record the actors for the current fragment into the state.
657        match current_fragment {
658            // For building fragments, we need to generate the actor builders.
659            EitherFragment::Building(current_fragment) => {
660                let node = current_fragment.node.clone().unwrap();
661                state
662                    .inner
663                    .fragment_actor_builders
664                    .try_insert(fragment_id, FragmentActorBuilder::new(fragment_id, node))
665                    .expect("non-duplicate");
666            }
667
668            // For existing fragments, we only need to record the actor locations.
669            EitherFragment::Existing => {}
670        };
671
672        // Then, add links between the current fragment and its downstream fragments.
673        for (downstream_fragment_id, edge) in self.fragment_graph.get_downstreams(fragment_id) {
674            state.inner.add_link(
675                FragmentLinkNode { fragment_id },
676                FragmentLinkNode {
677                    fragment_id: downstream_fragment_id,
678                },
679                edge,
680            );
681        }
682
683        Ok(())
684    }
685}
686
687#[cfg(test)]
688mod tests {
689    use risingwave_common::catalog::FragmentTypeFlag;
690    use risingwave_common::hash::VirtualNode;
691    use risingwave_common::types::DataType;
692    use risingwave_common::util::sort_util::OrderType;
693    use risingwave_pb::catalog::PbTable;
694    use risingwave_pb::common::PbColumnOrder;
695    use risingwave_pb::ddl_service::TableJobType;
696    use risingwave_pb::plan_common::{PbColumnCatalog, PbColumnDesc, StorageTableDesc};
697    use risingwave_pb::stream_plan::stream_fragment_graph::{
698        StreamFragment, StreamFragmentEdge as StreamFragmentEdgeProto,
699    };
700    use risingwave_pb::stream_plan::{
701        ExchangeNode, PbDispatchOutputMapping, PbStreamContext, SourceNode,
702        StreamFragmentGraph as StreamFragmentGraphProto, StreamFsFetch, StreamFsFetchNode,
703        StreamSource,
704    };
705
706    use super::*;
707    use crate::manager::{MetaSrvEnv, StreamingJob};
708    use crate::stream::StreamFragmentGraph;
709
710    const LIST_TABLE: u32 = 0;
711    const FETCH_TABLE: u32 = 1;
712
713    fn column(name: &str, data_type: DataType) -> PbColumnCatalog {
714        PbColumnCatalog {
715            column_desc: Some(PbColumnDesc {
716                name: name.to_owned(),
717                column_type: Some(data_type.to_protobuf()),
718                ..Default::default()
719            }),
720            ..Default::default()
721        }
722    }
723
724    /// A List fragment and its dedicated Fetch fragment, with the List referencing `reference`.
725    fn update_graph(reference: u32) -> StreamFragmentGraphProto {
726        let list = StreamNode {
727            node_body: Some(NodeBody::Source(Box::new(SourceNode {
728                source_inner: Some(StreamSource {
729                    state_table: Some(PbTable {
730                        id: LIST_TABLE.into(),
731                        columns: vec![column("state", DataType::Jsonb)],
732                        value_indices: vec![0],
733                        ..Default::default()
734                    }),
735                    iceberg_fetch_state_table: Some(StorageTableDesc {
736                        table_id: reference.into(),
737                        ..Default::default()
738                    }),
739                    ..Default::default()
740                }),
741            }))),
742            ..Default::default()
743        };
744        let exchange = StreamNode {
745            node_body: Some(NodeBody::Exchange(Box::new(ExchangeNode {
746                strategy: Some(DispatchStrategy {
747                    r#type: DispatcherType::Hash as i32,
748                    dist_key_indices: vec![0],
749                    output_mapping: PbDispatchOutputMapping::simple(vec![0, 1]).into(),
750                }),
751            }))),
752            ..Default::default()
753        };
754        let fetch = StreamNode {
755            node_body: Some(NodeBody::StreamFsFetch(Box::new(StreamFsFetchNode {
756                node_inner: Some(StreamFsFetch {
757                    state_table: Some(PbTable {
758                        id: FETCH_TABLE.into(),
759                        columns: vec![
760                            column("task", DataType::Varchar),
761                            column("state", DataType::Jsonb),
762                        ],
763                        pk: vec![PbColumnOrder {
764                            column_index: 0,
765                            order_type: Some(OrderType::ascending().to_protobuf()),
766                        }],
767                        distribution_key: vec![0],
768                        stream_key: vec![0],
769                        value_indices: vec![0, 1],
770                        read_prefix_len_hint: 1,
771                        ..Default::default()
772                    }),
773                    ..Default::default()
774                }),
775            }))),
776            input: vec![exchange],
777            ..Default::default()
778        };
779        StreamFragmentGraphProto {
780            fragments: HashMap::from([
781                (
782                    1.into(),
783                    StreamFragment {
784                        fragment_id: 1.into(),
785                        node: Some(list),
786                        fragment_type_mask: FragmentTypeFlag::Source as u32,
787                        requires_singleton: true,
788                    },
789                ),
790                (
791                    0.into(),
792                    StreamFragment {
793                        fragment_id: 0.into(),
794                        node: Some(fetch),
795                        fragment_type_mask: FragmentTypeFlag::FsFetch as u32,
796                        requires_singleton: false,
797                    },
798                ),
799            ]),
800            edges: vec![StreamFragmentEdgeProto {
801                dispatch_strategy: Some(DispatchStrategy {
802                    r#type: DispatcherType::Hash as i32,
803                    dist_key_indices: vec![0],
804                    output_mapping: PbDispatchOutputMapping::simple(vec![0, 1]).into(),
805                }),
806                link_id: 1,
807                upstream_id: 1.into(),
808                downstream_id: 0.into(),
809            }],
810            ctx: Some(PbStreamContext::default()),
811            table_ids_cnt: 2,
812            max_parallelism: VirtualNode::COUNT_FOR_TEST as _,
813            ..Default::default()
814        }
815    }
816
817    fn job() -> StreamingJob {
818        StreamingJob::Table(
819            None,
820            PbTable {
821                id: 888.into(),
822                ..Default::default()
823            },
824            TableJobType::General,
825        )
826    }
827
828    fn fetch_table_and_reference(builder: &ActorGraphBuilder) -> (PbTable, StorageTableDesc) {
829        let mut fetch = None;
830        let mut reference = None;
831        for fragment in builder.fragment_graph.building_fragments().values() {
832            visit_fragment(fragment, |body| match body {
833                NodeBody::StreamFsFetch(node) => {
834                    fetch = node.node_inner.as_ref().unwrap().state_table.clone();
835                }
836                NodeBody::Source(node) => {
837                    reference = node
838                        .source_inner
839                        .as_ref()
840                        .unwrap()
841                        .iceberg_fetch_state_table
842                        .clone();
843                }
844                _ => {}
845            });
846        }
847        (fetch.unwrap(), reference.unwrap())
848    }
849
850    #[tokio::test]
851    async fn test_iceberg_completion_table_is_filled_from_fetch_table() -> MetaResult<()> {
852        let env = MetaSrvEnv::for_test().await;
853        let mut fragment_graph = StreamFragmentGraph::new(&env, update_graph(FETCH_TABLE), &job())?;
854        // Mimic catalog creation assigning new IDs; the reference must follow the Fetch table.
855        let table_id_map: HashMap<_, _> = fragment_graph
856            .incomplete_internal_tables()
857            .into_keys()
858            .map(|id| (id, (id.as_raw_id() + 1000).into()))
859            .collect();
860        fragment_graph.refill_internal_table_ids(table_id_map);
861
862        let builder =
863            ActorGraphBuilder::new(CompleteStreamFragmentGraph::for_test(fragment_graph))?;
864        let (fetch, reference) = fetch_table_and_reference(&builder);
865        assert_eq!(reference.table_id, fetch.id);
866        assert_eq!(
867            reference,
868            TableDesc::from_pb_table(&fetch).try_to_protobuf()?
869        );
870        assert_eq!(reference.columns.len(), 2);
871        assert_eq!(reference.pk, fetch.pk);
872        assert_eq!(reference.dist_key_in_pk_indices, vec![0]);
873        assert_eq!(reference.value_indices, vec![0, 1]);
874        assert_eq!(reference.stream_key, vec![0]);
875        assert_eq!(reference.read_prefix_len_hint, 1);
876        assert_eq!(reference.vnode_count(), VirtualNode::COUNT_FOR_TEST);
877        assert_eq!(reference.maybe_vnode_count, fetch.maybe_vnode_count);
878        Ok(())
879    }
880
881    #[tokio::test]
882    async fn test_iceberg_completion_table_must_reference_fetch_table() -> MetaResult<()> {
883        let env = MetaSrvEnv::for_test().await;
884        let error = StreamFragmentGraph::new(&env, update_graph(LIST_TABLE), &job())
885            .expect_err("reference must resolve to a Fetch state table");
886        assert!(error.to_string().contains("Fetch state table"), "{error}");
887
888        // Also rejected when the graph reaches the actor builder with a dangling reference.
889        let mut fragment_graph = StreamFragmentGraph::new(&env, update_graph(FETCH_TABLE), &job())?;
890        for fragment in fragment_graph.fragments.values_mut() {
891            visit_iceberg_completion_tables(fragment, |desc| desc.table_id = 4242.into());
892        }
893        let error = ActorGraphBuilder::new(CompleteStreamFragmentGraph::for_test(fragment_graph))
894            .expect_err("dangling reference must be rejected");
895        assert!(error.to_string().contains("Fetch state table"), "{error}");
896        Ok(())
897    }
898}