Skip to main content

risingwave_meta/stream/stream_graph/
schedule.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 anyhow::Context;
18use enum_as_inner::EnumAsInner;
19use itertools::Itertools;
20use risingwave_common::bail;
21use risingwave_common::hash::{ActorAlignmentId, VnodeCountCompat};
22use risingwave_common::util::stream_graph_visitor::visit_fragment;
23use risingwave_connector::source::cdc::{CDC_BACKFILL_MAX_PARALLELISM, CdcScanOptions};
24use risingwave_meta_model::WorkerId;
25use risingwave_pb::common::WorkerNode;
26use risingwave_pb::meta::table_fragments::fragment::{
27    FragmentDistributionType, PbFragmentDistributionType,
28};
29use risingwave_pb::stream_plan::DispatcherType::{self, *};
30
31use crate::MetaResult;
32use crate::model::{ActorId, Fragment};
33use crate::stream::stream_graph::fragment::CompleteStreamFragmentGraph;
34use crate::stream::stream_graph::id::GlobalFragmentId as Id;
35
36type HashMappingId = usize;
37
38/// The internal structure for processing scheduling requirements in the scheduler.
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
40enum Req {
41    /// The fragment must be singleton and is scheduled to the given worker id.
42    Singleton,
43    /// The fragment must be hash-distributed and is scheduled by the given hash mapping.
44    Hash(HashMappingId),
45    /// The fragment must have the given vnode count, but can be scheduled anywhere.
46    /// When the vnode count is 1, it means the fragment must be singleton.
47    AnyVnodeCount(usize),
48}
49
50impl Req {
51    /// Equivalent to `Req::AnyVnodeCount(1)`.
52    #[expect(non_upper_case_globals)]
53    const AnySingleton: Self = Self::AnyVnodeCount(1);
54
55    /// Merge two requirements. Returns an error if the requirements are incompatible.
56    ///
57    /// The `mapping_len` function is used to get the vnode count of a hash mapping by its id.
58    fn merge(a: Self, b: Self, mapping_len: impl Fn(HashMappingId) -> usize) -> MetaResult<Self> {
59        // Note that a and b are always different, as they come from a set.
60        let merge = |a, b| match (a, b) {
61            (Self::AnySingleton, Self::Singleton) => Some(Self::Singleton),
62            (Self::AnyVnodeCount(count), Self::Hash(id)) if mapping_len(id) == count => {
63                Some(Self::Hash(id))
64            }
65            _ => None,
66        };
67
68        match merge(a, b).or_else(|| merge(b, a)) {
69            Some(req) => Ok(req),
70            None => bail!("incompatible requirements `{a:?}` and `{b:?}`"),
71        }
72    }
73}
74
75/// Facts as the input of the scheduler.
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
77enum Fact {
78    /// An edge in the fragment graph.
79    Edge {
80        from: Id,
81        to: Id,
82        dt: DispatcherType,
83    },
84    /// A scheduling requirement for a fragment.
85    Req { id: Id, req: Req },
86}
87
88crepe::crepe! {
89    @input
90    struct Input(Fact);
91
92    struct Edge(Id, Id, DispatcherType);
93    struct ExternalReq(Id, Req);
94
95    @output
96    struct Requirement(Id, Req);
97
98    // Extract facts.
99    Edge(from, to, dt) <- Input(f), let Fact::Edge { from, to, dt } = f;
100    Requirement(id, req) <- Input(f), let Fact::Req { id, req } = f;
101
102    // The downstream fragment of a `Simple` edge must be singleton.
103    Requirement(y, Req::AnySingleton) <- Edge(_, y, Simple);
104    // Requirements propagate through `NoShuffle` edges.
105    Requirement(x, d) <- Edge(x, y, NoShuffle), Requirement(y, d);
106    Requirement(y, d) <- Edge(x, y, NoShuffle), Requirement(x, d);
107}
108
109/// The distribution (scheduling result) of a fragment.
110#[derive(Debug, Clone, EnumAsInner)]
111pub(super) enum Distribution {
112    /// The fragment is singleton and is scheduled to the given worker slot.
113    Singleton,
114
115    /// The fragment is hash-distributed and is scheduled by the given hash mapping.
116    Hash(usize),
117}
118
119impl Distribution {
120    /// Get the vnode count of the distribution.
121    pub fn vnode_count(&self) -> usize {
122        match self {
123            Distribution::Singleton => 1, // only `SINGLETON_VNODE`
124            Distribution::Hash(vnode_count) => *vnode_count,
125        }
126    }
127
128    /// Create a distribution from a persisted protobuf `Fragment`.
129    pub fn from_fragment(fragment: &Fragment) -> Self {
130        match fragment.distribution_type {
131            FragmentDistributionType::Single => Distribution::Singleton,
132            FragmentDistributionType::Hash => Distribution::Hash(fragment.vnode_count()),
133            PbFragmentDistributionType::Unspecified => {
134                unreachable!()
135            }
136        }
137    }
138
139    /// Convert the distribution to [`PbFragmentDistributionType`].
140    pub fn to_distribution_type(&self) -> PbFragmentDistributionType {
141        match self {
142            Distribution::Singleton => PbFragmentDistributionType::Single,
143            Distribution::Hash(_) => PbFragmentDistributionType::Hash,
144        }
145    }
146}
147
148/// [`Scheduler`] schedules the distribution of fragments in a stream graph.
149pub(super) struct Scheduler {
150    /// The default hash mapping for hash-distributed fragments, if there's no requirement derived.
151    default_vnode_count: usize,
152}
153
154impl Scheduler {
155    /// Create a new [`Scheduler`] with the expected vnode count of the streaming job.
156    pub fn new(expected_vnode_count: usize) -> MetaResult<Self> {
157        Ok(Self {
158            default_vnode_count: expected_vnode_count,
159        })
160    }
161
162    /// Schedule the given complete graph and returns the distribution of each **building
163    /// fragment**.
164    pub fn schedule(
165        &self,
166        graph: &CompleteStreamFragmentGraph,
167    ) -> MetaResult<HashMap<Id, Distribution>> {
168        let existing_distribution = graph.existing_distribution();
169
170        // Build an index map for all hash mappings.
171        let all_hash_mappings = existing_distribution
172            .values()
173            .flat_map(|dist| dist.as_hash())
174            .cloned()
175            .unique()
176            .collect_vec();
177        let hash_mapping_id: HashMap<_, _> = all_hash_mappings
178            .iter()
179            .enumerate()
180            .map(|(i, m)| (*m, i))
181            .collect();
182
183        let mut facts = Vec::new();
184
185        // Singletons.
186        for (&id, fragment) in graph.building_fragments() {
187            if fragment.requires_singleton {
188                facts.push(Fact::Req {
189                    id,
190                    req: Req::AnySingleton,
191                });
192            }
193        }
194        let mut force_parallelism_fragment_ids: HashMap<_, _> = HashMap::default();
195        // Vnode count requirements: if a fragment is going to look up an existing table,
196        // it must have the same vnode count as that table.
197        for (&id, fragment) in graph.building_fragments() {
198            visit_fragment(fragment, |node| {
199                use risingwave_pb::stream_plan::stream_node::NodeBody;
200                let vnode_count = match node {
201                    NodeBody::StreamScan(node) => {
202                        if let Some(table) = &node.arrangement_table {
203                            table.vnode_count()
204                        } else if let Some(table) = &node.table_desc {
205                            table.vnode_count()
206                        } else {
207                            return;
208                        }
209                    }
210                    NodeBody::TemporalJoin(node) => node.get_table_desc().unwrap().vnode_count(),
211                    NodeBody::BatchPlan(node) => node.get_table_desc().unwrap().vnode_count(),
212                    NodeBody::Lookup(node) => node
213                        .get_arrangement_table_info()
214                        .unwrap()
215                        .get_table_desc()
216                        .unwrap()
217                        .vnode_count(),
218                    NodeBody::StreamCdcScan(node) => {
219                        let Some(ref options) = node.options else {
220                            return;
221                        };
222                        let options = CdcScanOptions::from_proto(options);
223                        if options.is_parallelized_backfill() {
224                            force_parallelism_fragment_ids
225                                .insert(id, options.backfill_parallelism as usize);
226                            CDC_BACKFILL_MAX_PARALLELISM as usize
227                        } else {
228                            return;
229                        }
230                    }
231                    _ => return,
232                };
233                facts.push(Fact::Req {
234                    id,
235                    req: Req::AnyVnodeCount(vnode_count),
236                });
237            });
238        }
239        // Distributions of existing fragments.
240        for (id, dist) in existing_distribution {
241            let req = match dist {
242                Distribution::Singleton => Req::Singleton,
243                Distribution::Hash(mapping) => Req::Hash(hash_mapping_id[&mapping]),
244            };
245            facts.push(Fact::Req { id, req });
246        }
247        // Edges.
248        for (from, to, edge) in graph.all_edges() {
249            facts.push(Fact::Edge {
250                from,
251                to,
252                dt: edge.dispatch_strategy.r#type(),
253            });
254        }
255
256        // Run the algorithm to propagate requirements.
257        let mut crepe = Crepe::new();
258        crepe.extend(facts.into_iter().map(Input));
259        let (reqs,) = crepe.run();
260        let reqs = reqs
261            .into_iter()
262            .map(|Requirement(id, req)| (id, req))
263            .into_group_map();
264
265        // Derive scheduling result from requirements.
266        let mut distributions = HashMap::new();
267        for &id in graph.building_fragments().keys() {
268            let dist = match reqs.get(&id) {
269                // Merge all requirements.
270                Some(reqs) => {
271                    let req = (reqs.iter().copied())
272                        .try_reduce(|a, b| Req::merge(a, b, |id| all_hash_mappings[id]))
273                        .with_context(|| {
274                            format!("cannot fulfill scheduling requirements for fragment {id:?}")
275                        })?
276                        .unwrap();
277
278                    // Derive distribution from the merged requirement.
279                    match req {
280                        Req::Singleton => Distribution::Singleton,
281                        Req::Hash(mapping) => Distribution::Hash(all_hash_mappings[mapping]),
282                        Req::AnySingleton => Distribution::Singleton,
283                        Req::AnyVnodeCount(vnode_count) => Distribution::Hash(vnode_count),
284                    }
285                }
286                // No requirement, use the default.
287                None => Distribution::Hash(self.default_vnode_count),
288            };
289
290            distributions.insert(id, dist);
291        }
292
293        tracing::debug!(?distributions, "schedule fragments");
294
295        Ok(distributions)
296    }
297}
298
299/// [`Locations`] represents the locations of the actors.
300#[cfg_attr(test, derive(Default))]
301pub struct Locations {
302    /// actor location map.
303    pub actor_locations: BTreeMap<ActorId, ActorAlignmentId>,
304    /// worker location map.
305    pub worker_locations: HashMap<WorkerId, WorkerNode>,
306}
307
308#[cfg(test)]
309mod tests {
310    use super::*;
311
312    #[derive(Debug)]
313    enum Result {
314        DefaultHash,
315        Required(Req),
316    }
317
318    impl Result {
319        #[expect(non_upper_case_globals)]
320        const DefaultSingleton: Self = Self::Required(Req::AnySingleton);
321    }
322
323    fn run_and_merge(
324        facts: impl IntoIterator<Item = Fact>,
325        mapping_len: impl Fn(HashMappingId) -> usize,
326    ) -> MetaResult<HashMap<Id, Req>> {
327        let mut crepe = Crepe::new();
328        crepe.extend(facts.into_iter().map(Input));
329        let (reqs,) = crepe.run();
330
331        let reqs = reqs
332            .into_iter()
333            .map(|Requirement(id, req)| (id, req))
334            .into_group_map();
335
336        let mut merged = HashMap::new();
337        for (id, reqs) in reqs {
338            let req = (reqs.iter().copied())
339                .try_reduce(|a, b| Req::merge(a, b, &mapping_len))
340                .with_context(|| {
341                    format!("cannot fulfill scheduling requirements for fragment {id:?}")
342                })?
343                .unwrap();
344            merged.insert(id, req);
345        }
346
347        Ok(merged)
348    }
349
350    fn test_success(facts: impl IntoIterator<Item = Fact>, expected: HashMap<Id, Result>) {
351        test_success_with_mapping_len(facts, expected, |_| 0);
352    }
353
354    fn test_success_with_mapping_len(
355        facts: impl IntoIterator<Item = Fact>,
356        expected: HashMap<Id, Result>,
357        mapping_len: impl Fn(HashMappingId) -> usize,
358    ) {
359        let reqs = run_and_merge(facts, mapping_len).unwrap();
360
361        for (id, expected) in expected {
362            match (reqs.get(&id), expected) {
363                (None, Result::DefaultHash) => {}
364                (Some(actual), Result::Required(expected)) if *actual == expected => {}
365                (actual, expected) => panic!(
366                    "unexpected result for fragment {id:?}\nactual: {actual:?}\nexpected: {expected:?}"
367                ),
368            }
369        }
370    }
371
372    fn test_failed(facts: impl IntoIterator<Item = Fact>) {
373        run_and_merge(facts, |_| 0).unwrap_err();
374    }
375
376    // 101
377    #[test]
378    fn test_single_fragment_hash() {
379        #[rustfmt::skip]
380        let facts = [];
381
382        let expected = maplit::hashmap! {
383            101.into() => Result::DefaultHash,
384        };
385
386        test_success(facts, expected);
387    }
388
389    // 101
390    #[test]
391    fn test_single_fragment_singleton() {
392        #[rustfmt::skip]
393        let facts = [
394            Fact::Req { id: 101.into(), req: Req::AnySingleton },
395        ];
396
397        let expected = maplit::hashmap! {
398            101.into() => Result::DefaultSingleton,
399        };
400
401        test_success(facts, expected);
402    }
403
404    // 1 -|-> 101 -->
405    //                103 --> 104
406    // 2 -|-> 102 -->
407    #[test]
408    fn test_scheduling_mv_on_mv() {
409        #[rustfmt::skip]
410        let facts = [
411            Fact::Req { id: 1.into(), req: Req::Hash(1) },
412            Fact::Req { id: 2.into(), req: Req::Singleton },
413            Fact::Edge { from: 1.into(), to: 101.into(), dt: NoShuffle },
414            Fact::Edge { from: 2.into(), to: 102.into(), dt: NoShuffle },
415            Fact::Edge { from: 101.into(), to: 103.into(), dt: Hash },
416            Fact::Edge { from: 102.into(), to: 103.into(), dt: Hash },
417            Fact::Edge { from: 103.into(), to: 104.into(), dt: Simple },
418        ];
419
420        let expected = maplit::hashmap! {
421            101.into() => Result::Required(Req::Hash(1)),
422            102.into() => Result::Required(Req::Singleton),
423            103.into() => Result::DefaultHash,
424            104.into() => Result::DefaultSingleton,
425        };
426
427        test_success(facts, expected);
428    }
429
430    // 1 -|-> 101 --> 103 -->
431    //             X          105
432    // 2 -|-> 102 --> 104 -->
433    #[test]
434    fn test_delta_join() {
435        #[rustfmt::skip]
436        let facts = [
437            Fact::Req { id: 1.into(), req: Req::Hash(1) },
438            Fact::Req { id: 2.into(), req: Req::Hash(2) },
439            Fact::Edge { from: 1.into(), to: 101.into(), dt: NoShuffle },
440            Fact::Edge { from: 2.into(), to: 102.into(), dt: NoShuffle },
441            Fact::Edge { from: 101.into(), to: 103.into(), dt: NoShuffle },
442            Fact::Edge { from: 102.into(), to: 104.into(), dt: NoShuffle },
443            Fact::Edge { from: 101.into(), to: 104.into(), dt: Hash },
444            Fact::Edge { from: 102.into(), to: 103.into(), dt: Hash },
445            Fact::Edge { from: 103.into(), to: 105.into(), dt: Hash },
446            Fact::Edge { from: 104.into(), to: 105.into(), dt: Hash },
447        ];
448
449        let expected = maplit::hashmap! {
450            101.into() => Result::Required(Req::Hash(1)),
451            102.into() => Result::Required(Req::Hash(2)),
452            103.into() => Result::Required(Req::Hash(1)),
453            104.into() => Result::Required(Req::Hash(2)),
454            105.into() => Result::DefaultHash,
455        };
456
457        test_success(facts, expected);
458    }
459
460    // 1 -|-> 101 -->
461    //                103
462    //        102 -->
463    #[test]
464    fn test_singleton_leaf() {
465        #[rustfmt::skip]
466        let facts = [
467            Fact::Req { id: 1.into(), req: Req::Hash(1) },
468            Fact::Edge { from: 1.into(), to: 101.into(), dt: NoShuffle },
469            Fact::Req { id: 102.into(), req: Req::AnySingleton }, // like `Now`
470            Fact::Edge { from: 101.into(), to: 103.into(), dt: Hash },
471            Fact::Edge { from: 102.into(), to: 103.into(), dt: Broadcast },
472        ];
473
474        let expected = maplit::hashmap! {
475            101.into() => Result::Required(Req::Hash(1)),
476            102.into() => Result::DefaultSingleton,
477            103.into() => Result::DefaultHash,
478        };
479
480        test_success(facts, expected);
481    }
482
483    // 1 -|->
484    //        101
485    // 2 -|->
486    #[test]
487    fn test_upstream_hash_shard_failed() {
488        #[rustfmt::skip]
489        let facts = [
490            Fact::Req { id: 1.into(), req: Req::Hash(1) },
491            Fact::Req { id: 2.into(), req: Req::Hash(2) },
492            Fact::Edge { from: 1.into(), to: 101.into(), dt: NoShuffle },
493            Fact::Edge { from: 2.into(), to: 101.into(), dt: NoShuffle },
494        ];
495
496        test_failed(facts);
497    }
498
499    // 1 -|~> 101
500    #[test]
501    fn test_arrangement_backfill_vnode_count() {
502        #[rustfmt::skip]
503        let facts = [
504            Fact::Req { id: 1.into(), req: Req::Hash(1) },
505            Fact::Req { id: 101.into(), req: Req::AnyVnodeCount(128) },
506            Fact::Edge { from: 1.into(), to: 101.into(), dt: Hash },
507        ];
508
509        let expected = maplit::hashmap! {
510            101.into() => Result::Required(Req::AnyVnodeCount(128)),
511        };
512
513        test_success(facts, expected);
514    }
515
516    // 1 -|~> 101
517    #[test]
518    fn test_no_shuffle_backfill_vnode_count() {
519        #[rustfmt::skip]
520        let facts = [
521            Fact::Req { id: 1.into(), req: Req::Hash(1) },
522            Fact::Req { id: 101.into(), req: Req::AnyVnodeCount(128) },
523            Fact::Edge { from: 1.into(), to: 101.into(), dt: NoShuffle },
524        ];
525
526        let expected = maplit::hashmap! {
527            101.into() => Result::Required(Req::Hash(1)),
528        };
529
530        test_success_with_mapping_len(facts, expected, |id| {
531            assert_eq!(id, 1);
532            128
533        });
534    }
535
536    // 1 -|~> 101
537    #[test]
538    fn test_no_shuffle_backfill_mismatched_vnode_count() {
539        #[rustfmt::skip]
540        let facts = [
541            Fact::Req { id: 1.into(), req: Req::Hash(1) },
542            Fact::Req { id: 101.into(), req: Req::AnyVnodeCount(128) },
543            Fact::Edge { from: 1.into(), to: 101.into(), dt: NoShuffle },
544        ];
545
546        // Not specifying `mapping_len` should fail.
547        test_failed(facts);
548    }
549
550    // 1 -|~> 101
551    #[test]
552    fn test_backfill_singleton_vnode_count() {
553        #[rustfmt::skip]
554        let facts = [
555            Fact::Req { id: 1.into(), req: Req::Singleton },
556            Fact::Req { id: 101.into(), req: Req::AnySingleton },
557            Fact::Edge { from: 1.into(), to: 101.into(), dt: NoShuffle }, // or `Simple`
558        ];
559
560        let expected = maplit::hashmap! {
561            101.into() => Result::Required(Req::Singleton),
562        };
563
564        test_success(facts, expected);
565    }
566}