Skip to main content

risingwave_meta/stream/source_manager/
split_assignment.rs

1// Copyright 2025 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 anyhow::anyhow;
16use itertools::Itertools;
17use risingwave_connector::source::fill_adaptive_split;
18
19use super::*;
20use crate::model::{ActorNewNoShuffle, FragmentReplaceUpstream, StreamJobFragments};
21
22#[derive(Debug, Clone)]
23pub struct SplitState {
24    pub split_assignment: SplitAssignment,
25}
26
27impl SourceManager {
28    /// Migrates splits from previous actors to the new actors for a rescheduled fragment.
29    pub fn migrate_splits_for_backfill_actors(
30        &self,
31        fragment_id: FragmentId,
32        upstream_source_fragment_id: FragmentId,
33        curr_actor_ids: &[ActorId],
34        fragment_actor_splits: &HashMap<FragmentId, HashMap<ActorId, Vec<SplitImpl>>>,
35        no_shuffle_upstream_actor_map: &HashMap<ActorId, HashMap<FragmentId, ActorId>>,
36    ) -> MetaResult<HashMap<ActorId, Vec<SplitImpl>>> {
37        // align splits for backfill fragments with its upstream source fragment
38        let actors = no_shuffle_upstream_actor_map
39            .iter()
40            .filter(|(id, _)| curr_actor_ids.contains(id))
41            .map(|(id, upstream_fragment_actors)| {
42                (
43                    *id,
44                    *upstream_fragment_actors
45                        .get(&upstream_source_fragment_id)
46                        .unwrap(),
47                )
48            });
49        let upstream_assignment = fragment_actor_splits
50            .get(&upstream_source_fragment_id)
51            .unwrap();
52        tracing::info!(
53            %fragment_id,
54            %upstream_source_fragment_id,
55            ?upstream_assignment,
56            "migrate_splits_for_backfill_actors"
57        );
58        Ok(align_splits(
59            actors,
60            |upstream_actor_id| upstream_assignment.get(&upstream_actor_id).cloned(),
61            fragment_id,
62            upstream_source_fragment_id,
63        )?)
64    }
65
66    /// Discovers splits for a newly created source executor.
67    /// Returns fragment-level split information. Actor-level assignment
68    /// will happen during Phase 2 inside the barrier worker.
69    #[await_tree::instrument]
70    pub async fn discover_splits(
71        &self,
72        table_fragments: &StreamJobFragments,
73    ) -> MetaResult<SourceSplitAssignment> {
74        let source_fragments = table_fragments.stream_source_fragments();
75        // Avoid touching the contended `core` lock for jobs with no source fragments.
76        if source_fragments.is_empty() {
77            return Ok(HashMap::new());
78        }
79
80        let core = self.core.lock().await;
81
82        let mut assigned = HashMap::new();
83
84        for (source_id, _fragments) in source_fragments {
85            let handle = core
86                .managed_sources
87                .get(&source_id)
88                .with_context(|| format!("could not find source {}", source_id))?;
89
90            if handle.splits.lock().await.splits.is_none() {
91                handle.force_tick().await?;
92            }
93
94            let Some(discovered) = handle.discovered_splits(source_id).await? else {
95                tracing::warn!(%source_id, "no splits detected (not ready)");
96                continue;
97            };
98            if let DiscoveredSplits::Fixed(ref splits) = discovered
99                && splits.is_empty()
100            {
101                tracing::warn!(%source_id, "no splits detected");
102                continue;
103            }
104            // Store the discovered splits enum at source level.
105            // The enum is preserved until the barrier worker resolves it
106            // to concrete per-fragment, per-actor splits during actor rendering.
107            assigned.insert(source_id, discovered);
108        }
109
110        Ok(assigned)
111    }
112
113    /// Resolve a [`SourceSplitAssignment`] (source-level, containing [`DiscoveredSplits`])
114    /// into an actor-level [`SplitAssignment`] using the source→fragment mapping and
115    /// the rendered actor IDs for each fragment.
116    ///
117    /// `fragment_actor_ids` provides the actor IDs assigned to each fragment from rendering.
118    /// For `Fixed` splits, splits are distributed across actors via [`reassign_splits`].
119    /// For `Adaptive` splits, `fill_adaptive_split` expands the template per actor count.
120    pub fn resolve_fragment_to_actor_splits(
121        table_fragments: &StreamJobFragments,
122        source_assignment: &SourceSplitAssignment,
123        fragment_actor_ids: &HashMap<FragmentId, Vec<ActorId>>,
124    ) -> MetaResult<SplitAssignment> {
125        let source_fragments = table_fragments.stream_source_fragments();
126        let mut result: SplitAssignment = HashMap::new();
127        for (source_id, discovered) in source_assignment {
128            let fragment_ids = source_fragments.get(source_id).unwrap_or_else(|| {
129                panic!("source {} not found in stream job fragments", source_id)
130            });
131            for fragment_id in fragment_ids {
132                let actor_ids = fragment_actor_ids.get(fragment_id).unwrap_or_else(|| {
133                    panic!("fragment {} not found in rendered actor IDs", fragment_id)
134                });
135                let actor_count = actor_ids.len();
136                match discovered {
137                    DiscoveredSplits::Fixed(splits) => {
138                        let empty_actor_splits: HashMap<ActorId, Vec<SplitImpl>> = actor_ids
139                            .iter()
140                            .map(|actor_id| (*actor_id, vec![]))
141                            .collect();
142                        if let Some(diff) = reassign_splits(
143                            *fragment_id,
144                            empty_actor_splits,
145                            splits,
146                            SplitDiffOptions::default(),
147                        ) {
148                            result.insert(*fragment_id, diff);
149                        }
150                    }
151                    DiscoveredSplits::Adaptive(template) => {
152                        let expanded = fill_adaptive_split(template, actor_count)?;
153                        let expanded_splits: Vec<SplitImpl> = expanded.into_values().collect();
154                        let mut actor_splits = HashMap::new();
155                        for (i, actor_id) in actor_ids.iter().enumerate() {
156                            actor_splits.insert(*actor_id, vec![expanded_splits[i].clone()]);
157                        }
158                        result.insert(*fragment_id, actor_splits);
159                    }
160                }
161            }
162        }
163        Ok(result)
164    }
165
166    /// Phase 1 for replace source: gathers fragment-level split information.
167    ///
168    /// When there are no existing downstream (`upstream_updates` is empty), we can
169    /// re-allocate splits fresh by delegating to [`Self::discover_splits`].
170    /// When there are existing downstream, returns `None` — the actual
171    /// split alignment happens in Phase 2 inside the barrier worker via
172    /// [`Self::resolve_replace_source_splits`].
173    pub async fn discover_splits_for_replace_source(
174        &self,
175        table_fragments: &StreamJobFragments,
176        upstream_updates: &FragmentReplaceUpstream,
177    ) -> MetaResult<Option<SourceSplitAssignment>> {
178        if upstream_updates.is_empty() {
179            // No existing downstream. We can just re-allocate splits arbitrarily.
180            return Ok(Some(self.discover_splits(table_fragments).await?));
181        }
182        // Splits will be aligned with the previous fragment in Phase 2
183        // using new_no_shuffle and inflight database info inside the barrier worker.
184        Ok(None)
185    }
186
187    /// Phase 2 resolve for replace source: align the new source fragment's actor splits
188    /// with the previous source fragment using the `new_no_shuffle` actor mapping and
189    /// the existing split assignment looked up via the provided closure.
190    pub fn resolve_replace_source_splits(
191        table_fragments: &StreamJobFragments,
192        upstream_updates: &FragmentReplaceUpstream,
193        // actor_no_shuffle:
194        //     upstream fragment_id ->
195        //     downstream fragment_id ->
196        //     upstream actor_id ->
197        //     downstream actor_id
198        actor_no_shuffle: &ActorNewNoShuffle,
199        get_upstream_actor_splits: impl Fn(FragmentId, ActorId) -> Option<Vec<SplitImpl>>,
200    ) -> MetaResult<SplitAssignment> {
201        tracing::debug!(?upstream_updates, "allocate_splits_for_replace_source");
202        assert!(!upstream_updates.is_empty());
203
204        let source_fragments = table_fragments.stream_source_fragments();
205        assert_eq!(
206            source_fragments.len(),
207            1,
208            "replace source job should only have one source"
209        );
210        let (_source_id, fragments) = source_fragments.into_iter().next().unwrap();
211        assert_eq!(
212            fragments.len(),
213            1,
214            "replace source job should only have one fragment"
215        );
216        let fragment_id = fragments.into_iter().next().unwrap();
217
218        debug_assert!(
219            upstream_updates.values().flatten().next().is_some()
220                && upstream_updates
221                    .values()
222                    .flatten()
223                    .all(|(_, new_upstream_fragment_id)| {
224                        *new_upstream_fragment_id == fragment_id
225                    })
226                && upstream_updates
227                    .values()
228                    .flatten()
229                    .map(|(upstream_fragment_id, _)| upstream_fragment_id)
230                    .all_equal(),
231            "upstream update should only replace one fragment: {:?}",
232            upstream_updates
233        );
234        let prev_fragment_id = upstream_updates
235            .values()
236            .flatten()
237            .next()
238            .map(|(upstream_fragment_id, _)| *upstream_fragment_id)
239            .expect("non-empty");
240        // Here we align the new source executor to backfill executors
241        //
242        // old_source => new_source            backfill_1
243        // actor_x1   => actor_y1 -----┬------>actor_a1
244        // actor_x2   => actor_y2 -----┼-┬---->actor_a2
245        //                             │ │
246        //                             │ │     backfill_2
247        //                             └─┼---->actor_b1
248        //                               └---->actor_b2
249        //
250        // Note: we can choose any backfill actor to align here.
251        // We use `HashMap` to dedup.
252        let aligned_actors: HashMap<ActorId, ActorId> = actor_no_shuffle
253            .get(&fragment_id)
254            .map(HashMap::values)
255            .into_iter()
256            .flatten()
257            .flatten()
258            .map(|(upstream_actor_id, actor_id)| (*upstream_actor_id, *actor_id))
259            .collect();
260        let assignment = align_splits(
261            aligned_actors,
262            |actor_id| get_upstream_actor_splits(prev_fragment_id, actor_id),
263            fragment_id,
264            prev_fragment_id,
265        )?;
266        Ok(HashMap::from([(fragment_id, assignment)]))
267    }
268
269    /// Phase 2 resolve for source backfill: align each backfill fragment's actor splits
270    /// with its upstream source fragment using the `new_no_shuffle` actor mapping and
271    /// the existing split assignment looked up via the provided closure.
272    pub fn resolve_backfill_splits(
273        table_fragments: &StreamJobFragments,
274        actor_no_shuffle: &ActorNewNoShuffle,
275        get_upstream_actor_splits: impl Fn(FragmentId, ActorId) -> Option<Vec<SplitImpl>>,
276    ) -> MetaResult<SplitAssignment> {
277        let source_backfill_fragments = table_fragments.source_backfill_fragments();
278
279        let mut assigned = HashMap::new();
280
281        for (_source_id, fragments) in source_backfill_fragments {
282            for (fragment_id, upstream_source_fragment_id) in fragments {
283                // Get upstream actors from actor_no_shuffle mapping
284                let upstream_actors: HashSet<ActorId> = actor_no_shuffle
285                    .get(&upstream_source_fragment_id)
286                    .and_then(|m| m.get(&fragment_id))
287                    .ok_or_else(|| {
288                        anyhow!(
289                            "no upstream actors found from fragment {} to upstream source fragment {}",
290                            fragment_id,
291                            upstream_source_fragment_id
292                        )
293                    })?.keys().copied().collect();
294
295                let mut backfill_actors = vec![];
296                let Some(source_new_no_shuffle) = actor_no_shuffle
297                    .get(&upstream_source_fragment_id)
298                    .and_then(|source_upstream_actor_no_shuffle| {
299                        source_upstream_actor_no_shuffle.get(&fragment_id)
300                    })
301                else {
302                    return Err(anyhow::anyhow!(
303                            "source backfill fragment's upstream fragment should have one-on-one no_shuffle mapping, fragment_id: {fragment_id}, upstream_fragment_id: {upstream_source_fragment_id}, actor_no_shuffle: {actor_no_shuffle:?}",
304                            fragment_id = fragment_id,
305                            upstream_source_fragment_id = upstream_source_fragment_id,
306                            actor_no_shuffle = actor_no_shuffle,
307                        ).into());
308                };
309                for upstream_actor in &upstream_actors {
310                    let Some(no_shuffle_backfill_actor) = source_new_no_shuffle.get(upstream_actor)
311                    else {
312                        return Err(anyhow::anyhow!(
313                            "source backfill fragment's upstream fragment should have one-on-one no_shuffle mapping, fragment_id: {fragment_id}, upstream_fragment_id: {upstream_source_fragment_id}, upstream_actor: {upstream_actor}, source_new_no_shuffle: {source_new_no_shuffle:?}",
314                            fragment_id = fragment_id,
315                            upstream_source_fragment_id = upstream_source_fragment_id,
316                            upstream_actor = upstream_actor,
317                            source_new_no_shuffle = source_new_no_shuffle
318                        ).into());
319                    };
320                    backfill_actors.push((*no_shuffle_backfill_actor, *upstream_actor));
321                }
322                assigned.insert(
323                    fragment_id,
324                    align_splits(
325                        backfill_actors,
326                        |actor_id| get_upstream_actor_splits(upstream_source_fragment_id, actor_id),
327                        fragment_id,
328                        upstream_source_fragment_id,
329                    )?,
330                );
331            }
332        }
333
334        Ok(assigned)
335    }
336}
337
338impl SourceManagerCore {
339    /// Checks whether the external source metadata has changed,
340    /// and re-assigns splits if there's a diff.
341    ///
342    /// `self.actor_splits` will not be updated. It will be updated by `Self::apply_source_change`,
343    /// after the mutation barrier has been collected.
344    pub async fn reassign_splits(&self) -> MetaResult<HashMap<DatabaseId, SplitState>> {
345        let mut split_assignment: SplitAssignment = HashMap::new();
346
347        'loop_source: for (source_id, handle) in &self.managed_sources {
348            let source_fragment_ids = match self.source_fragments.get(source_id) {
349                Some(fragment_ids) if !fragment_ids.is_empty() => fragment_ids,
350                _ => {
351                    continue;
352                }
353            };
354            let backfill_fragment_ids = self.backfill_fragments.get(source_id);
355
356            'loop_fragment: for &fragment_id in source_fragment_ids {
357                let actors = match self
358                    .metadata_manager
359                    .get_running_actors_of_fragment(fragment_id)
360                {
361                    Ok(actors) => {
362                        if actors.is_empty() {
363                            tracing::warn!("No actors found for fragment {}", fragment_id);
364                            continue 'loop_fragment;
365                        }
366                        actors
367                    }
368                    Err(err) => {
369                        tracing::warn!(error = %err.as_report(), "Failed to get the actor of the fragment, maybe the fragment doesn't exist anymore");
370                        continue 'loop_fragment;
371                    }
372                };
373
374                let Some(discovered) = handle.discovered_splits(*source_id).await? else {
375                    // The discover loop for this source is not ready yet; we'll wait for the next run
376                    continue 'loop_source;
377                };
378                let discovered_splits = match discovered {
379                    DiscoveredSplits::Fixed(splits) => {
380                        if splits.is_empty() {
381                            continue 'loop_source;
382                        }
383                        splits
384                    }
385                    DiscoveredSplits::Adaptive(template) => {
386                        fill_adaptive_split(&template, actors.len())?
387                    }
388                };
389
390                let prev_actor_splits = {
391                    let guard = self.env.shared_actor_infos().read_guard();
392
393                    guard
394                        .get_fragment(fragment_id)
395                        .and_then(|info| {
396                            info.actors
397                                .iter()
398                                .map(|(actor_id, actor_info)| {
399                                    (*actor_id, actor_info.splits.clone())
400                                })
401                                .collect::<HashMap<_, _>>()
402                                .into()
403                        })
404                        .unwrap_or_default()
405                };
406
407                if let Some(new_assignment) = reassign_splits(
408                    fragment_id,
409                    prev_actor_splits,
410                    &discovered_splits,
411                    SplitDiffOptions {
412                        enable_scale_in: handle.enable_drop_split,
413                        enable_adaptive: handle.enable_adaptive_splits,
414                    },
415                ) {
416                    split_assignment.insert(fragment_id, new_assignment);
417                }
418            }
419
420            if let Some(backfill_fragment_ids) = backfill_fragment_ids {
421                // align splits for backfill fragments with its upstream source fragment
422                for (fragment_id, upstream_fragment_id) in backfill_fragment_ids {
423                    let Some(upstream_assignment): Option<&HashMap<ActorId, Vec<SplitImpl>>> =
424                        split_assignment.get(upstream_fragment_id)
425                    else {
426                        // upstream fragment unchanged, do not update backfill fragment too
427                        continue;
428                    };
429                    let actors = match self
430                        .metadata_manager
431                        .get_running_actors_for_source_backfill(*fragment_id, *upstream_fragment_id)
432                        .await
433                    {
434                        Ok(actors) => {
435                            if actors.is_empty() {
436                                tracing::warn!("No actors found for fragment {}", fragment_id);
437                                continue;
438                            }
439                            actors
440                        }
441                        Err(err) => {
442                            tracing::warn!(error = %err.as_report(),"Failed to get the actor of the fragment, maybe the fragment doesn't exist anymore");
443                            continue;
444                        }
445                    };
446                    split_assignment.insert(
447                        *fragment_id,
448                        align_splits(
449                            actors,
450                            |upstream_actor_id| {
451                                upstream_assignment.get(&upstream_actor_id).cloned()
452                            },
453                            *fragment_id,
454                            *upstream_fragment_id,
455                        )?,
456                    );
457                }
458            }
459        }
460
461        let assignments = self
462            .metadata_manager
463            .split_fragment_map_by_database(split_assignment)
464            .await?;
465
466        let mut result = HashMap::new();
467        for (database_id, assignment) in assignments {
468            result.insert(
469                database_id,
470                SplitState {
471                    split_assignment: assignment,
472                },
473            );
474        }
475
476        Ok(result)
477    }
478}
479
480/// Reassigns splits if there are new splits or dropped splits,
481/// i.e., `actor_splits` and `discovered_splits` differ, or actors are rescheduled.
482///
483/// The existing splits will remain unmoved in their currently assigned actor.
484///
485/// If an actor has an upstream actor, it should be a backfill executor,
486/// and its splits should be aligned with the upstream actor. **`reassign_splits` should not be used in this case.
487/// Use [`align_splits`] instead.**
488///
489/// - `fragment_id`: just for logging
490///
491/// ## Different connectors' behavior of split change
492///
493/// ### Kafka and Pulsar
494/// They only support increasing the number of splits via adding new empty splits.
495/// Old data is not moved.
496///
497/// ### Kinesis
498/// It supports *pairwise* shard split and merge.
499///
500/// In both cases, old data remain in the old shard(s) and the old shard is still available.
501/// New data are routed to the new shard(s).
502/// After the retention period has expired, the old shard will become `EXPIRED` and isn't
503/// listed any more. In other words, the total number of shards will first increase and then decrease.
504///
505/// See also:
506/// - [Kinesis resharding doc](https://docs.aws.amazon.com/streams/latest/dev/kinesis-using-sdk-java-after-resharding.html#kinesis-using-sdk-java-resharding-data-routing)
507/// - An example of how the shards can be like: <https://stackoverflow.com/questions/72272034/list-shard-show-more-shards-than-provisioned>
508pub fn reassign_splits<I, T>(
509    fragment_id: FragmentId,
510    actor_splits: HashMap<I, Vec<T>>,
511    discovered_splits: &BTreeMap<SplitId, T>,
512    opts: SplitDiffOptions,
513) -> Option<HashMap<I, Vec<T>>>
514where
515    I: Ord + std::hash::Hash + Eq,
516    T: SplitMetaData + Clone,
517{
518    // if no actors, return
519    if actor_splits.is_empty() {
520        return None;
521    }
522
523    let prev_split_ids: HashSet<_> = actor_splits
524        .values()
525        .flat_map(|splits| splits.iter().map(SplitMetaData::id))
526        .collect();
527
528    tracing::trace!(%fragment_id, prev_split_ids = ?prev_split_ids, "previous splits");
529    tracing::trace!(%fragment_id, prev_split_ids = ?discovered_splits.keys(), "discovered splits");
530
531    let discovered_split_ids: HashSet<_> = discovered_splits.keys().cloned().collect();
532
533    let dropped_splits: HashSet<_> = prev_split_ids
534        .difference(&discovered_split_ids)
535        .cloned()
536        .collect();
537
538    if !dropped_splits.is_empty() {
539        if opts.enable_scale_in {
540            tracing::info!(%fragment_id, dropped_spltis = ?dropped_splits, "new dropped splits");
541        } else {
542            tracing::warn!(%fragment_id, dropped_spltis = ?dropped_splits, "split dropping happened, but it is not allowed");
543        }
544    }
545
546    let new_discovered_splits: BTreeSet<_> = discovered_split_ids
547        .into_iter()
548        .filter(|split_id| !prev_split_ids.contains(split_id))
549        .collect();
550
551    if opts.enable_scale_in || opts.enable_adaptive {
552        // if we support scale in, no more splits are discovered, and no splits are dropped, return
553        // we need to check if discovered_split_ids is empty, because if it is empty, we need to
554        // handle the case of scale in to zero (like deleting all objects from s3)
555        if dropped_splits.is_empty()
556            && new_discovered_splits.is_empty()
557            && !discovered_splits.is_empty()
558        {
559            return None;
560        }
561    } else {
562        // if we do not support scale in, and no more splits are discovered, return
563        if new_discovered_splits.is_empty() && !discovered_splits.is_empty() {
564            return None;
565        }
566    }
567
568    tracing::info!(%fragment_id, new_discovered_splits = ?new_discovered_splits, "new discovered splits");
569
570    let mut heap = BinaryHeap::with_capacity(actor_splits.len());
571
572    for (actor_id, mut splits) in actor_splits {
573        if opts.enable_scale_in || opts.enable_adaptive {
574            splits.retain(|split| !dropped_splits.contains(&split.id()));
575        }
576
577        heap.push(SplitsAssignment { actor_id, splits })
578    }
579
580    for split_id in new_discovered_splits {
581        // SplitsAssignment's Ord is reversed, so this is min heap, i.e.,
582        // we get the assignment with the least splits here.
583
584        // Note: If multiple actors have the same number of splits, it will be randomly picked.
585        // When the number of source actors is larger than the number of splits,
586        // It's possible that the assignment is uneven.
587        // e.g., https://github.com/risingwavelabs/risingwave/issues/14324#issuecomment-1875033158
588        // TODO: We should make the assignment rack-aware to make sure it's even.
589        let mut peek_ref = heap.peek_mut().unwrap();
590        peek_ref
591            .splits
592            .push(discovered_splits.get(&split_id).cloned().unwrap());
593    }
594
595    Some(
596        heap.into_iter()
597            .map(|SplitsAssignment { actor_id, splits }| (actor_id, splits))
598            .collect(),
599    )
600}
601
602/// Assign splits to a new set of actors, according to existing assignment.
603///
604/// The `get_upstream_actor_splits` closure looks up the current splits for a given
605/// upstream actor ID. How exactly this lookup works depends on the caller:
606/// - Inside the barrier worker, it reads from `InflightDatabaseInfo`.
607/// - During reassignment, it reads from the pending upstream assignment.
608///
609/// illustration:
610/// ```text
611/// upstream                               new
612/// actor x1 [split 1, split2]      ->     actor y1 [split 1, split2]
613/// actor x2 [split 3]              ->     actor y2 [split 3]
614/// ...
615/// ```
616pub fn align_splits(
617    // (actor_id, upstream_actor_id)
618    aligned_actors: impl IntoIterator<Item = (ActorId, ActorId)>,
619    get_upstream_actor_splits: impl Fn(ActorId) -> Option<Vec<SplitImpl>>,
620    fragment_id: FragmentId,
621    upstream_source_fragment_id: FragmentId,
622) -> anyhow::Result<HashMap<ActorId, Vec<SplitImpl>>> {
623    aligned_actors
624        .into_iter()
625        .map(|(actor_id, upstream_actor_id)| {
626            let Some(splits) = get_upstream_actor_splits(upstream_actor_id) else {
627                return Err(anyhow::anyhow!("upstream assignment not found, fragment_id: {fragment_id}, upstream_fragment_id: {upstream_source_fragment_id}, actor_id: {actor_id}, upstream_actor_id: {upstream_actor_id:?}"));
628            };
629
630            Ok((
631                actor_id,
632                splits,
633            ))
634        })
635        .collect()
636}
637
638/// Note: the `PartialEq` and `Ord` impl just compares the number of splits.
639#[derive(Debug)]
640struct SplitsAssignment<I, T: SplitMetaData> {
641    actor_id: I,
642    splits: Vec<T>,
643}
644
645impl<I, T: SplitMetaData + Clone> Eq for SplitsAssignment<I, T> {}
646
647impl<I, T: SplitMetaData + Clone> PartialEq<Self> for SplitsAssignment<I, T> {
648    fn eq(&self, other: &Self) -> bool {
649        self.splits.len() == other.splits.len()
650    }
651}
652
653impl<I: Ord, T: SplitMetaData + Clone> PartialOrd<Self> for SplitsAssignment<I, T> {
654    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
655        Some(self.cmp(other))
656    }
657}
658
659impl<I: Ord, T: SplitMetaData + Clone> Ord for SplitsAssignment<I, T> {
660    fn cmp(&self, other: &Self) -> Ordering {
661        // Note: this is reversed order, to make BinaryHeap a min heap.
662        other
663            .splits
664            .len()
665            .cmp(&self.splits.len())
666            .then(self.actor_id.cmp(&other.actor_id))
667    }
668}
669
670#[derive(Debug)]
671pub struct SplitDiffOptions {
672    pub enable_scale_in: bool,
673
674    /// For most connectors, this should be false. When enabled, RisingWave will not track any progress.
675    pub enable_adaptive: bool,
676}
677
678#[expect(clippy::derivable_impls)]
679impl Default for SplitDiffOptions {
680    fn default() -> Self {
681        SplitDiffOptions {
682            enable_scale_in: false,
683            enable_adaptive: false,
684        }
685    }
686}
687
688#[cfg(test)]
689mod tests {
690    use std::collections::{BTreeMap, HashMap, HashSet};
691
692    use risingwave_common::types::JsonbVal;
693    use risingwave_connector::error::ConnectorResult;
694    use risingwave_connector::source::{SplitId, SplitMetaData};
695    use serde::{Deserialize, Serialize};
696
697    use super::*;
698    use crate::model::{ActorId, FragmentId};
699
700    #[derive(Debug, Copy, Clone, Serialize, Deserialize)]
701    struct TestSplit {
702        id: u32,
703    }
704
705    impl SplitMetaData for TestSplit {
706        fn id(&self) -> SplitId {
707            format!("{}", self.id).into()
708        }
709
710        fn encode_to_json(&self) -> JsonbVal {
711            serde_json::to_value(*self).unwrap().into()
712        }
713
714        fn restore_from_json(value: JsonbVal) -> ConnectorResult<Self> {
715            serde_json::from_value(value.take()).map_err(Into::into)
716        }
717
718        fn update_offset(&mut self, _last_read_offset: String) -> ConnectorResult<()> {
719            Ok(())
720        }
721    }
722
723    fn check_all_splits(
724        discovered_splits: &BTreeMap<SplitId, TestSplit>,
725        diff: &HashMap<ActorId, Vec<TestSplit>>,
726    ) {
727        let mut split_ids: HashSet<_> = discovered_splits.keys().cloned().collect();
728
729        for splits in diff.values() {
730            for split in splits {
731                assert!(split_ids.remove(&split.id()))
732            }
733        }
734
735        assert!(split_ids.is_empty());
736    }
737
738    #[test]
739    fn test_drop_splits() {
740        let mut actor_splits: HashMap<ActorId, _> = HashMap::new();
741        actor_splits.insert(0.into(), vec![TestSplit { id: 0 }, TestSplit { id: 1 }]);
742        actor_splits.insert(1.into(), vec![TestSplit { id: 2 }, TestSplit { id: 3 }]);
743        actor_splits.insert(2.into(), vec![TestSplit { id: 4 }, TestSplit { id: 5 }]);
744
745        let mut prev_split_to_actor = HashMap::new();
746        for (actor_id, splits) in &actor_splits {
747            for split in splits {
748                prev_split_to_actor.insert(split.id(), *actor_id);
749            }
750        }
751
752        let discovered_splits: BTreeMap<SplitId, TestSplit> = (1..5)
753            .map(|i| {
754                let split = TestSplit { id: i };
755                (split.id(), split)
756            })
757            .collect();
758
759        let opts = SplitDiffOptions {
760            enable_scale_in: true,
761            enable_adaptive: false,
762        };
763
764        let prev_split_ids: HashSet<_> = actor_splits
765            .values()
766            .flat_map(|splits| splits.iter().map(|split| split.id()))
767            .collect();
768
769        let diff = reassign_splits(
770            FragmentId::default(),
771            actor_splits,
772            &discovered_splits,
773            opts,
774        )
775        .unwrap();
776        check_all_splits(&discovered_splits, &diff);
777
778        let mut after_split_to_actor = HashMap::new();
779        for (actor_id, splits) in &diff {
780            for split in splits {
781                after_split_to_actor.insert(split.id(), *actor_id);
782            }
783        }
784
785        let discovered_split_ids: HashSet<_> = discovered_splits.keys().cloned().collect();
786
787        let retained_split_ids: HashSet<_> =
788            prev_split_ids.intersection(&discovered_split_ids).collect();
789
790        for retained_split_id in retained_split_ids {
791            assert_eq!(
792                prev_split_to_actor.get(retained_split_id),
793                after_split_to_actor.get(retained_split_id)
794            )
795        }
796    }
797
798    #[test]
799    fn test_drop_splits_to_empty() {
800        let mut actor_splits: HashMap<ActorId, _> = HashMap::new();
801        actor_splits.insert(0.into(), vec![TestSplit { id: 0 }]);
802
803        let discovered_splits: BTreeMap<SplitId, TestSplit> = BTreeMap::new();
804
805        let opts = SplitDiffOptions {
806            enable_scale_in: true,
807            enable_adaptive: false,
808        };
809
810        let diff = reassign_splits(
811            FragmentId::default(),
812            actor_splits,
813            &discovered_splits,
814            opts,
815        )
816        .unwrap();
817
818        assert!(!diff.is_empty())
819    }
820
821    #[test]
822    fn test_reassign_splits() {
823        let actor_splits: HashMap<u32, _> = HashMap::new();
824        let discovered_splits: BTreeMap<SplitId, TestSplit> = BTreeMap::new();
825        assert!(
826            reassign_splits(
827                FragmentId::default(),
828                actor_splits,
829                &discovered_splits,
830                Default::default()
831            )
832            .is_none()
833        );
834
835        let actor_splits = (0..3).map(|i| (i, vec![])).collect();
836        let discovered_splits: BTreeMap<SplitId, TestSplit> = BTreeMap::new();
837        let diff = reassign_splits(
838            FragmentId::default(),
839            actor_splits,
840            &discovered_splits,
841            Default::default(),
842        )
843        .unwrap();
844        assert_eq!(diff.len(), 3);
845        for splits in diff.values() {
846            assert!(splits.is_empty())
847        }
848
849        let actor_splits = (0..3).map(|i| (i.into(), vec![])).collect();
850        let discovered_splits: BTreeMap<SplitId, TestSplit> = (0..3)
851            .map(|i| {
852                let split = TestSplit { id: i };
853                (split.id(), split)
854            })
855            .collect();
856
857        let diff = reassign_splits(
858            FragmentId::default(),
859            actor_splits,
860            &discovered_splits,
861            Default::default(),
862        )
863        .unwrap();
864        assert_eq!(diff.len(), 3);
865        for splits in diff.values() {
866            assert_eq!(splits.len(), 1);
867        }
868
869        check_all_splits(&discovered_splits, &diff);
870
871        let actor_splits = (0..3)
872            .map(|i| (i.into(), vec![TestSplit { id: i }]))
873            .collect();
874        let discovered_splits: BTreeMap<SplitId, TestSplit> = (0..5)
875            .map(|i| {
876                let split = TestSplit { id: i };
877                (split.id(), split)
878            })
879            .collect();
880
881        let diff = reassign_splits(
882            FragmentId::default(),
883            actor_splits,
884            &discovered_splits,
885            Default::default(),
886        )
887        .unwrap();
888        assert_eq!(diff.len(), 3);
889        for splits in diff.values() {
890            let len = splits.len();
891            assert!(len == 1 || len == 2);
892        }
893
894        check_all_splits(&discovered_splits, &diff);
895
896        let mut actor_splits: HashMap<ActorId, Vec<TestSplit>> = (0..3)
897            .map(|i| (i.into(), vec![TestSplit { id: i }]))
898            .collect();
899        actor_splits.insert(3.into(), vec![]);
900        actor_splits.insert(4.into(), vec![]);
901
902        let discovered_splits: BTreeMap<SplitId, TestSplit> = (0..5)
903            .map(|i| {
904                let split = TestSplit { id: i };
905                (split.id(), split)
906            })
907            .collect();
908
909        let diff = reassign_splits(
910            FragmentId::default(),
911            actor_splits,
912            &discovered_splits,
913            Default::default(),
914        )
915        .unwrap();
916        assert_eq!(diff.len(), 5);
917        for splits in diff.values() {
918            assert_eq!(splits.len(), 1);
919        }
920
921        check_all_splits(&discovered_splits, &diff);
922    }
923}