Skip to main content

meta_srv/procedure/repartition/
allocate_region.rs

1// Copyright 2023 Greptime Team
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::any::Any;
16use std::collections::{HashMap, HashSet};
17
18use common_meta::affinity::{
19    AffinityFallback, AffinityReference, AffinityRequirements, AffinityResolver,
20};
21use common_meta::ddl::create_table::executor::CreateTableExecutor;
22use common_meta::ddl::create_table::template::{
23    CreateRequestBuilder, build_template_from_raw_table_info_for_physical_table,
24};
25use common_meta::ddl::utils::get_region_wal_options;
26use common_meta::lock_key::TableLock;
27use common_meta::node_manager::NodeManagerRef;
28use common_meta::peer::PeerAllocContext;
29use common_meta::rpc::router::RegionRoute;
30use common_meta::wal_provider::{
31    RegionWalOptions, acquire_remote_wal_read_locks, refresh_initial_pruned_entry_ids,
32};
33use common_procedure::{Context as ProcedureContext, Status};
34use common_telemetry::{debug, info};
35use common_wal::options::WalOptions;
36use serde::{Deserialize, Deserializer, Serialize};
37use snafu::{OptionExt, ResultExt};
38use store_api::region_request::RegionRequirements;
39use store_api::storage::{RegionId, RegionNumber, TableId};
40use table::metadata::TableInfo;
41use table::table_reference::TableReference;
42use tokio::time::Instant;
43
44use crate::error::{self, Result};
45use crate::procedure::repartition::dispatch::Dispatch;
46use crate::procedure::repartition::plan::{
47    AllocationPlanEntry, RepartitionPlanEntry, TargetRegionDescriptor,
48    convert_allocation_plan_to_repartition_plan,
49};
50use crate::procedure::repartition::{Context, State};
51
52#[derive(Debug, Clone, Serialize)]
53pub enum AllocateRegion {
54    Build(BuildPlan),
55    Execute(ExecutePlan),
56}
57
58impl<'de> Deserialize<'de> for AllocateRegion {
59    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
60    where
61        D: Deserializer<'de>,
62    {
63        #[derive(Deserialize)]
64        enum CurrentAllocateRegion {
65            Build(BuildPlan),
66            Execute(ExecutePlan),
67        }
68
69        #[derive(Deserialize)]
70        struct LegacyAllocateRegion {
71            plan_entries: Vec<AllocationPlanEntry>,
72        }
73
74        #[derive(Deserialize)]
75        #[serde(untagged)]
76        enum AllocateRegionRepr {
77            Current(CurrentAllocateRegion),
78            Legacy(LegacyAllocateRegion),
79        }
80
81        match AllocateRegionRepr::deserialize(deserializer)? {
82            AllocateRegionRepr::Current(CurrentAllocateRegion::Build(build_plan)) => {
83                Ok(Self::Build(build_plan))
84            }
85            AllocateRegionRepr::Current(CurrentAllocateRegion::Execute(execute_plan)) => {
86                Ok(Self::Execute(execute_plan))
87            }
88            AllocateRegionRepr::Legacy(legacy) => Ok(Self::Build(BuildPlan {
89                plan_entries: legacy.plan_entries,
90            })),
91        }
92    }
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct BuildPlan {
97    plan_entries: Vec<AllocationPlanEntry>,
98}
99
100impl BuildPlan {
101    async fn next(
102        &mut self,
103        ctx: &mut Context,
104        _procedure_ctx: &ProcedureContext,
105    ) -> Result<(Box<dyn State>, Status)> {
106        let timer = Instant::now();
107        let table_id = ctx.persistent_ctx.table_id;
108        let table_route_value = ctx.get_table_route_value().await?;
109        let mut next_region_number =
110            AllocateRegion::get_next_region_number(table_route_value.max_region_number().unwrap());
111
112        // Converts allocation plan to repartition plan.
113        let repartition_plan_entries = AllocateRegion::convert_to_repartition_plans(
114            table_id,
115            &mut next_region_number,
116            &self.plan_entries,
117            table_route_value.region_routes().unwrap(),
118        )?;
119        let plan_count = repartition_plan_entries.len();
120        let to_allocate = AllocateRegion::count_regions_to_allocate(&repartition_plan_entries);
121        info!(
122            "Repartition allocate regions start, table_id: {}, groups: {}, regions_to_allocate: {}",
123            table_id, plan_count, to_allocate
124        );
125
126        // If no region to allocate, directly dispatch the plan.
127        if AllocateRegion::count_regions_to_allocate(&repartition_plan_entries) == 0 {
128            ctx.persistent_ctx.plans = repartition_plan_entries;
129            ctx.update_allocate_region_elapsed(timer.elapsed());
130            return Ok((Box::new(Dispatch), Status::executing(true)));
131        }
132
133        ctx.persistent_ctx.plans = repartition_plan_entries;
134        debug!(
135            "Repartition allocate regions build plan completed, table_id: {}, elapsed: {:?}",
136            table_id,
137            timer.elapsed()
138        );
139        Ok((
140            Box::new(AllocateRegion::Execute(ExecutePlan)),
141            Status::executing(true),
142        ))
143    }
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct ExecutePlan;
148
149impl ExecutePlan {
150    async fn next(
151        &mut self,
152        ctx: &mut Context,
153        procedure_ctx: &ProcedureContext,
154    ) -> Result<(Box<dyn State>, Status)> {
155        let timer = Instant::now();
156        let table_id = ctx.persistent_ctx.table_id;
157        let allocate_regions = AllocateRegion::collect_allocate_regions(&ctx.persistent_ctx.plans);
158        let region_number_and_partition_exprs =
159            AllocateRegion::prepare_region_allocation_data(&allocate_regions)?;
160        let table_info_value = ctx.get_table_info_value().await?;
161        let affinity = if let Some(resolver) = &ctx.affinity_resolver {
162            Some(resolve_allocation_affinity(ctx, resolver.as_ref()).await?)
163        } else {
164            None
165        };
166        let groups = if let Some(affinity) = &affinity {
167            group_allocations(table_id, &region_number_and_partition_exprs, affinity)
168        } else {
169            vec![AllocationGroup {
170                affinity: None,
171                regions: region_number_and_partition_exprs
172                    .iter()
173                    .map(|(number, partition)| (*number, partition.as_str()))
174                    .collect(),
175            }]
176        };
177        let mut new_allocated_region_routes = Vec::with_capacity(allocate_regions.len());
178        for AllocationGroup { affinity, regions } in groups {
179            let routes = ctx
180                .region_routes_allocator
181                .allocate(
182                    table_id,
183                    &regions,
184                    &PeerAllocContext {
185                        affinity: affinity.cloned(),
186                    },
187                )
188                .await
189                .context(error::AllocateRegionRoutesSnafu { table_id })?;
190            new_allocated_region_routes.extend(routes);
191        }
192        let skip_wal = table_info_value.table_info.meta.options.skip_wal;
193        let allocate_noop = if skip_wal {
194            let table_route_value = ctx.get_table_route_value().await?;
195            let region_wal_options =
196                get_region_wal_options(&ctx.table_metadata_manager, &table_route_value, table_id)
197                    .await
198                    .context(error::AllocateWalOptionsSnafu { table_id })?;
199            // A table disabled after creation keeps its real providers. A table
200            // created with skip_wal has Noop providers and must keep them.
201            !table_route_value
202                .region_routes()
203                .unwrap()
204                .iter()
205                .all(|route| {
206                    region_wal_options
207                        .get(&route.region.id.region_number())
208                        .is_none_or(|option| {
209                            matches!(
210                                option,
211                                WalOptions::RaftEngine
212                                    | WalOptions::Kafka(_)
213                                    | WalOptions::ObjectStore(_)
214                            )
215                        })
216                })
217        } else {
218            false
219        };
220        let mut wal_options = ctx
221            .wal_options_allocator
222            .allocate(
223                &allocate_regions
224                    .iter()
225                    .map(|r| r.region_id.region_number())
226                    .collect::<Vec<_>>(),
227                allocate_noop,
228            )
229            .await
230            .context(error::AllocateWalOptionsSnafu { table_id })?;
231        let _remote_wal_lock_guards =
232            acquire_remote_wal_read_locks(procedure_ctx, &wal_options).await;
233        refresh_initial_pruned_entry_ids(&ctx.table_metadata_manager, &mut wal_options)
234            .await
235            .context(error::AllocateWalOptionsSnafu { table_id })?;
236
237        let new_region_count = new_allocated_region_routes.len();
238        let new_regions_brief: Vec<_> = new_allocated_region_routes
239            .iter()
240            .map(|route| {
241                let region_id = route.region.id;
242                let peer = route.leader_peer.as_ref().map(|p| p.id).unwrap_or_default();
243                format!("region_id: {}, peer: {}", region_id, peer)
244            })
245            .collect();
246        info!(
247            "Allocated regions for repartition, table_id: {}, new_region_count: {}, new_regions: {:?}",
248            table_id, new_region_count, new_regions_brief
249        );
250
251        // The table route metadata is not updated yet; register it in memory for region lease renewal.
252        let _operating_guards = Context::register_operating_regions(
253            &ctx.memory_region_keeper,
254            &new_allocated_region_routes,
255        )?;
256        // Allocates the regions on datanodes.
257        AllocateRegion::allocate_regions(
258            &ctx.node_manager,
259            &table_info_value.table_info,
260            &new_allocated_region_routes,
261            &wal_options,
262        )
263        .await?;
264
265        // Updates the table routes.
266        let table_lock = TableLock::Write(table_id).into();
267        let _guard = procedure_ctx.provider.acquire_lock(&table_lock).await;
268        // MUST refresh the table route value after acquiring the lock to avoid lost update.
269        // Otherwise, the new allocated regions might be overridden by concurrent repartition procedures.
270        let table_route_value = ctx.get_table_route_value().await?;
271        // Safety: it is physical table route value.
272        let region_routes = table_route_value.region_routes().unwrap();
273        let new_region_routes =
274            AllocateRegion::generate_region_routes(region_routes, &new_allocated_region_routes);
275        ctx.update_table_route(&table_route_value, new_region_routes, wal_options)
276            .await?;
277        ctx.invalidate_table_cache().await?;
278
279        ctx.update_allocate_region_elapsed(timer.elapsed());
280        Ok((Box::new(Dispatch), Status::executing(true)))
281    }
282}
283
284#[async_trait::async_trait]
285#[typetag::serde]
286impl State for AllocateRegion {
287    async fn next(
288        &mut self,
289        ctx: &mut Context,
290        procedure_ctx: &ProcedureContext,
291    ) -> Result<(Box<dyn State>, Status)> {
292        match self {
293            AllocateRegion::Build(build_plan) => build_plan.next(ctx, procedure_ctx).await,
294            AllocateRegion::Execute(execute_plan) => execute_plan.next(ctx, procedure_ctx).await,
295        }
296    }
297
298    fn as_any(&self) -> &dyn Any {
299        self
300    }
301}
302
303impl AllocateRegion {
304    pub fn new(plan_entries: Vec<AllocationPlanEntry>) -> Self {
305        AllocateRegion::Build(BuildPlan { plan_entries })
306    }
307
308    fn generate_region_routes(
309        region_routes: &[RegionRoute],
310        new_allocated_region_ids: &[RegionRoute],
311    ) -> Vec<RegionRoute> {
312        let region_ids = region_routes
313            .iter()
314            .map(|r| r.region.id)
315            .collect::<HashSet<_>>();
316        let mut new_region_routes = region_routes.to_vec();
317        for new_allocated_region_id in new_allocated_region_ids {
318            if !region_ids.contains(&new_allocated_region_id.region.id) {
319                new_region_routes.push(new_allocated_region_id.clone());
320            }
321        }
322        new_region_routes
323    }
324
325    /// Converts allocation plan entries to repartition plan entries.
326    ///
327    /// This method converts allocation intents into concrete repartition plans,
328    /// updates `next_region_number` for newly allocated regions, and captures
329    /// each plan's `original_target_routes` from the current table-route view.
330    ///
331    /// This also persists each plan's pre-staging target routes for rollback.
332    fn convert_to_repartition_plans(
333        table_id: TableId,
334        next_region_number: &mut RegionNumber,
335        plan_entries: &[AllocationPlanEntry],
336        current_region_routes: &[RegionRoute],
337    ) -> Result<Vec<RepartitionPlanEntry>> {
338        let region_routes_map = current_region_routes
339            .iter()
340            .map(|route| (route.region.id, route))
341            .collect::<HashMap<_, _>>();
342
343        plan_entries
344            .iter()
345            .map(|plan_entry| {
346                let mut plan = convert_allocation_plan_to_repartition_plan(
347                    table_id,
348                    next_region_number,
349                    plan_entry,
350                );
351                Self::capture_plan_original_target_routes(&mut plan, &region_routes_map)?;
352                Ok(plan)
353            })
354            .collect()
355    }
356
357    fn capture_plan_original_target_routes(
358        plan: &mut RepartitionPlanEntry,
359        region_routes_map: &HashMap<RegionId, &RegionRoute>,
360    ) -> Result<()> {
361        // Persist the pre-staging target-route view on the parent plan.
362        // Newly allocated targets are skipped because rollback deletes their
363        // route metadata rather than restoring an original target route.
364        let mut original_target_routes = Vec::with_capacity(plan.target_regions.len());
365        for target in &plan.target_regions {
366            if plan.allocated_region_ids.contains(&target.region_id) {
367                // This target region is to be allocated, so it doesn't exist in current routes.
368                continue;
369            }
370            let route = region_routes_map.get(&target.region_id).context(
371                error::RepartitionTargetRegionMissingSnafu {
372                    group_id: plan.group_id,
373                    region_id: target.region_id,
374                },
375            )?;
376            {
377                original_target_routes.push((*route).clone());
378            }
379        }
380
381        plan.original_target_routes = original_target_routes;
382        Ok(())
383    }
384
385    /// Collects all regions that need to be allocated from the repartition plan entries.
386    fn collect_allocate_regions(
387        repartition_plan_entries: &[RepartitionPlanEntry],
388    ) -> Vec<&TargetRegionDescriptor> {
389        repartition_plan_entries
390            .iter()
391            .flat_map(|p| p.allocate_regions())
392            .collect()
393    }
394
395    /// Prepares region allocation data: region numbers and their partition expressions.
396    fn prepare_region_allocation_data(
397        allocate_regions: &[&TargetRegionDescriptor],
398    ) -> Result<Vec<(RegionNumber, String)>> {
399        allocate_regions
400            .iter()
401            .map(|r| {
402                Ok((
403                    r.region_id.region_number(),
404                    r.partition_expr
405                        .as_json_str()
406                        .context(error::SerializePartitionExprSnafu)?,
407                ))
408            })
409            .collect()
410    }
411
412    /// Calculates the total number of regions that need to be allocated.
413    fn count_regions_to_allocate(repartition_plan_entries: &[RepartitionPlanEntry]) -> usize {
414        repartition_plan_entries
415            .iter()
416            .map(|p| p.allocated_region_ids.len())
417            .sum()
418    }
419
420    /// Gets the next region number from the physical table route.
421    fn get_next_region_number(max_region_number: RegionNumber) -> RegionNumber {
422        max_region_number + 1
423    }
424
425    async fn allocate_regions(
426        node_manager: &NodeManagerRef,
427        raw_table_info: &TableInfo,
428        region_routes: &[RegionRoute],
429        wal_options: &RegionWalOptions,
430    ) -> Result<()> {
431        let table_ref = TableReference::full(
432            &raw_table_info.catalog_name,
433            &raw_table_info.schema_name,
434            &raw_table_info.name,
435        );
436        let table_id = raw_table_info.ident.table_id;
437        // Repartition allocation targets physical regions, so exclude metric internal columns
438        // and derive primary keys from tag semantics.
439        let request = build_template_from_raw_table_info_for_physical_table(raw_table_info)
440            .context(error::BuildCreateRequestSnafu { table_id })?;
441        common_telemetry::debug!(
442            "Allocating regions request, table_id: {}, request: {:?}",
443            table_id,
444            request
445        );
446        let builder = CreateRequestBuilder::new(request, None)
447            .with_requirements(RegionRequirements::object_storage());
448        let region_count = region_routes.len();
449        let wal_region_count = wal_options.len();
450        info!(
451            "Allocating regions on datanodes, table_id: {}, region_count: {}, wal_regions: {}",
452            table_id, region_count, wal_region_count
453        );
454        let executor = CreateTableExecutor::new(table_ref.into(), false, builder);
455        executor
456            .on_create_regions(node_manager, table_id, region_routes, wal_options)
457            .await
458            .context(error::AllocateRegionsSnafu { table_id })?;
459
460        Ok(())
461    }
462}
463
464/// Selects the lowest-ID contributing source for a target spanning multiple sources.
465/// Legacy plans without a transition map fall back to the lowest-ID source in the group.
466fn collect_allocation_sources(
467    plans: &[RepartitionPlanEntry],
468    routes: &[RegionRoute],
469) -> Vec<(RegionId, Option<u64>)> {
470    let leaders: HashMap<_, _> = routes
471        .iter()
472        .filter_map(|route| {
473            route
474                .leader_peer
475                .as_ref()
476                .map(|peer| (route.region.id, peer.id))
477        })
478        .collect();
479    let mut sources = Vec::new();
480    for plan in plans {
481        let allocated: HashSet<_> = plan.allocated_region_ids.iter().copied().collect();
482        let mut target_sources = vec![None; plan.target_regions.len()];
483        if plan.transition_map.is_empty() {
484            let source = plan
485                .source_regions
486                .iter()
487                .map(|source| source.region_id())
488                .min();
489            target_sources.fill(source);
490        } else {
491            for (source, targets) in plan.source_regions.iter().zip(&plan.transition_map) {
492                for target_index in targets {
493                    if let Some(target_source) = target_sources.get_mut(*target_index) {
494                        let source_id = source.region_id();
495                        *target_source =
496                            Some(target_source.map_or(source_id, |current| current.min(source_id)));
497                    }
498                }
499            }
500        }
501        for (target, source) in plan.target_regions.iter().zip(target_sources) {
502            if allocated.contains(&target.region_id) {
503                sources.push((
504                    target.region_id,
505                    source.and_then(|source| leaders.get(&source)).copied(),
506                ));
507            }
508        }
509    }
510    sources
511}
512
513/// Owns requirements once per source; targets refer to them by index.
514#[derive(Debug)]
515struct AllocationAffinity {
516    requirements: Vec<Option<AffinityRequirements>>,
517    target_sources: HashMap<RegionId, usize>,
518}
519
520async fn resolve_allocation_affinity(
521    ctx: &Context,
522    resolver: &dyn AffinityResolver,
523) -> Result<AllocationAffinity> {
524    let table_id = ctx.persistent_ctx.table_id;
525    let routes = ctx.get_table_route_value().await?;
526    let sources =
527        collect_allocation_sources(&ctx.persistent_ctx.plans, routes.region_routes().unwrap());
528    let mut source_indices = HashMap::new();
529    let mut references = Vec::new();
530    for (_, source_id) in &sources {
531        if let Some(source_id) = source_id {
532            source_indices.entry(*source_id).or_insert_with(|| {
533                let index = references.len();
534                references.push(AffinityReference::Datanode(*source_id));
535                index
536            });
537        }
538    }
539    let resolved = resolver
540        .resolve(&references)
541        .await
542        .context(error::AllocateRegionRoutesSnafu { table_id })?;
543    snafu::ensure!(
544        resolved.len() == source_indices.len(),
545        error::UnexpectedSnafu {
546            violated: "affinity resolution must preserve the number and order of references",
547        }
548    );
549    snafu::ensure!(
550        ctx.affinity_fallback == AffinityFallback::Unconstrained
551            || sources.iter().all(|(_, source)| {
552                source.is_some_and(|id| resolved[source_indices[&id]].is_some())
553            }),
554        error::UnexpectedSnafu {
555            violated: "source affinity requirements are unavailable",
556        }
557    );
558    Ok(AllocationAffinity {
559        requirements: resolved,
560        target_sources: sources
561            .into_iter()
562            .filter_map(|(region_id, source_id)| {
563                source_id.map(|source_id| (region_id, source_indices[&source_id]))
564            })
565            .collect(),
566    })
567}
568
569#[derive(Debug, PartialEq, Eq)]
570struct AllocationGroup<'a> {
571    affinity: Option<&'a AffinityRequirements>,
572    regions: Vec<(RegionNumber, &'a str)>,
573}
574
575fn group_allocations<'a>(
576    table_id: TableId,
577    regions: &'a [(RegionNumber, String)],
578    affinity: &'a AllocationAffinity,
579) -> Vec<AllocationGroup<'a>> {
580    let mut groups: Vec<AllocationGroup<'a>> = Vec::new();
581    for (number, partition) in regions {
582        let requirements = affinity
583            .target_sources
584            .get(&RegionId::new(table_id, *number))
585            .and_then(|index| affinity.requirements[*index].as_ref());
586        if let Some(group) = groups
587            .iter_mut()
588            .find(|group| group.affinity == requirements)
589        {
590            group.regions.push((*number, partition.as_str()));
591        } else {
592            groups.push(AllocationGroup {
593                affinity: requirements,
594                regions: vec![(*number, partition.as_str())],
595            });
596        }
597    }
598    groups
599}
600
601#[cfg(test)]
602mod tests {
603    use std::ptr;
604    use std::sync::Arc;
605
606    use api::v1::region::region_request::Body;
607    use common_meta::ddl::allocator::wal_options::WalOptionsAllocator;
608    use common_meta::ddl::test_util::datanode_handler::DatanodeWatcher;
609    use common_meta::key::TableMetadataManagerRef;
610    use common_meta::key::datanode_table::DatanodeTableKey;
611    use common_meta::key::table_route::TableRouteValue;
612    use common_meta::key::test_utils::new_test_table_info_with_name;
613    use common_meta::peer::Peer;
614    use common_meta::rpc::router::{Region, RegionRoute};
615    use common_meta::test_util::MockDatanodeManager;
616    use common_procedure::{ContextProvider, ProcedureId, ProcedureState};
617    use common_procedure_test::MockContextProvider;
618    use common_wal::options::{
619        KafkaWalOptions, ObjectStoreWalOptions, WAL_OPTIONS_KEY, WalOptions,
620    };
621    use store_api::mito_engine_options::SKIP_WAL_KEY;
622    use store_api::storage::RegionId;
623    use tokio::sync::{mpsc, watch};
624    use uuid::Uuid;
625
626    use super::*;
627    use crate::procedure::repartition::State;
628    use crate::procedure::repartition::plan::SourceRegionDescriptor;
629    use crate::procedure::repartition::test_util::{
630        TestingEnv, current_parent_region_routes, new_parent_context, range_expr,
631        test_region_wal_options,
632    };
633
634    fn create_region_descriptor(
635        table_id: TableId,
636        region_number: u32,
637        col: &str,
638        start: i64,
639        end: i64,
640    ) -> SourceRegionDescriptor {
641        SourceRegionDescriptor::partitioned(
642            RegionId::new(table_id, region_number),
643            range_expr(col, start, end),
644        )
645    }
646
647    fn create_target_region_descriptor(
648        table_id: TableId,
649        region_number: u32,
650        col: &str,
651        start: i64,
652        end: i64,
653    ) -> TargetRegionDescriptor {
654        TargetRegionDescriptor {
655            region_id: RegionId::new(table_id, region_number),
656            partition_expr: range_expr(col, start, end),
657        }
658    }
659
660    fn create_allocation_plan_entry(
661        table_id: TableId,
662        source_region_numbers: &[u32],
663        target_ranges: &[(i64, i64)],
664    ) -> AllocationPlanEntry {
665        let source_regions = source_region_numbers
666            .iter()
667            .enumerate()
668            .map(|(i, &n)| {
669                let start = i as i64 * 100;
670                let end = (i + 1) as i64 * 100;
671                create_region_descriptor(table_id, n, "x", start, end)
672            })
673            .collect();
674
675        let target_partition_exprs = target_ranges
676            .iter()
677            .map(|&(start, end)| range_expr("x", start, end))
678            .collect();
679
680        AllocationPlanEntry {
681            group_id: Uuid::new_v4(),
682            source_regions,
683            target_partition_exprs,
684            transition_map: vec![],
685        }
686    }
687
688    fn create_current_region_routes(table_id: TableId, region_numbers: &[u32]) -> Vec<RegionRoute> {
689        region_numbers
690            .iter()
691            .map(|region_number| RegionRoute {
692                region: Region {
693                    id: RegionId::new(table_id, *region_number),
694                    ..Default::default()
695                },
696                leader_peer: Some(Peer::empty(1)),
697                ..Default::default()
698            })
699            .collect()
700    }
701
702    struct ConcurrentTableRouteUpdateProvider {
703        inner: MockContextProvider,
704        table_metadata_manager: TableMetadataManagerRef,
705        table_id: TableId,
706        concurrent_region_route: RegionRoute,
707        region_wal_options: RegionWalOptions,
708    }
709
710    struct TestKafkaWalOptionsAllocator;
711
712    #[async_trait::async_trait]
713    impl WalOptionsAllocator for TestKafkaWalOptionsAllocator {
714        async fn allocate(
715            &self,
716            region_numbers: &[RegionNumber],
717            skip_wal: bool,
718        ) -> common_meta::error::Result<RegionWalOptions> {
719            Ok(region_numbers
720                .iter()
721                .map(|&region_number| {
722                    let options = if skip_wal {
723                        WalOptions::Noop
724                    } else {
725                        WalOptions::Kafka(KafkaWalOptions::new("new-topic".to_string()))
726                    };
727                    (region_number, options)
728                })
729                .collect())
730        }
731    }
732
733    #[async_trait::async_trait]
734    impl ContextProvider for ConcurrentTableRouteUpdateProvider {
735        async fn procedure_state(
736            &self,
737            procedure_id: ProcedureId,
738        ) -> common_procedure::Result<Option<ProcedureState>> {
739            self.inner.procedure_state(procedure_id).await
740        }
741
742        async fn procedure_state_receiver(
743            &self,
744            procedure_id: ProcedureId,
745        ) -> common_procedure::Result<Option<watch::Receiver<ProcedureState>>> {
746            self.inner.procedure_state_receiver(procedure_id).await
747        }
748
749        async fn try_put_poison(
750            &self,
751            key: &common_procedure::PoisonKey,
752            procedure_id: ProcedureId,
753        ) -> common_procedure::Result<()> {
754            self.inner.try_put_poison(key, procedure_id).await
755        }
756
757        async fn acquire_lock(
758            &self,
759            key: &common_procedure::StringKey,
760        ) -> common_procedure::local::DynamicKeyLockGuard {
761            let current_table_route_value = self
762                .table_metadata_manager
763                .table_route_manager()
764                .table_route_storage()
765                .get_with_raw_bytes(self.table_id)
766                .await
767                .unwrap()
768                .unwrap();
769            let mut region_routes = current_table_route_value.region_routes().unwrap().clone();
770
771            if !region_routes
772                .iter()
773                .any(|route| route.region.id == self.concurrent_region_route.region.id)
774            {
775                region_routes.push(self.concurrent_region_route.clone());
776                let datanode_id = current_table_route_value.region_routes().unwrap()[0]
777                    .leader_peer
778                    .as_ref()
779                    .unwrap()
780                    .id;
781                let datanode_table_value = self
782                    .table_metadata_manager
783                    .datanode_table_manager()
784                    .get(&DatanodeTableKey::new(datanode_id, self.table_id))
785                    .await
786                    .unwrap()
787                    .unwrap();
788                let region_options = &datanode_table_value.region_info.region_options;
789
790                self.table_metadata_manager
791                    .update_table_route(
792                        self.table_id,
793                        datanode_table_value.region_info.clone(),
794                        &current_table_route_value,
795                        region_routes,
796                        region_options,
797                        &self.region_wal_options,
798                    )
799                    .await
800                    .unwrap();
801            }
802
803            self.inner.acquire_lock(key).await
804        }
805    }
806
807    #[test]
808    fn test_convert_to_repartition_plans_no_allocation() {
809        let table_id = 1024;
810        let mut next_region_number = 10;
811
812        // 2 source -> 2 target (no allocation needed)
813        let plan_entries = vec![create_allocation_plan_entry(
814            table_id,
815            &[1, 2],
816            &[(0, 50), (50, 200)],
817        )];
818
819        let result = AllocateRegion::convert_to_repartition_plans(
820            table_id,
821            &mut next_region_number,
822            &plan_entries,
823            &create_current_region_routes(table_id, &[1, 2]),
824        )
825        .unwrap();
826
827        assert_eq!(result.len(), 1);
828        assert_eq!(result[0].target_regions.len(), 2);
829        assert!(result[0].allocated_region_ids.is_empty());
830        // next_region_number should not change
831        assert_eq!(next_region_number, 10);
832    }
833
834    #[test]
835    fn test_convert_to_repartition_plans_with_allocation() {
836        let table_id = 1024;
837        let mut next_region_number = 10;
838
839        // 2 source -> 4 target (need to allocate 2 regions)
840        let plan_entries = vec![create_allocation_plan_entry(
841            table_id,
842            &[1, 2],
843            &[(0, 50), (50, 100), (100, 150), (150, 200)],
844        )];
845
846        let result = AllocateRegion::convert_to_repartition_plans(
847            table_id,
848            &mut next_region_number,
849            &plan_entries,
850            &create_current_region_routes(table_id, &[1, 2]),
851        )
852        .unwrap();
853
854        assert_eq!(result.len(), 1);
855        assert_eq!(result[0].target_regions.len(), 4);
856        assert_eq!(result[0].allocated_region_ids.len(), 2);
857        assert_eq!(
858            result[0].allocated_region_ids[0],
859            RegionId::new(table_id, 10)
860        );
861        assert_eq!(
862            result[0].allocated_region_ids[1],
863            RegionId::new(table_id, 11)
864        );
865        // next_region_number should be incremented by 2
866        assert_eq!(next_region_number, 12);
867    }
868
869    #[test]
870    fn test_convert_to_repartition_plans_multiple_entries() {
871        let table_id = 1024;
872        let mut next_region_number = 10;
873
874        // Multiple plan entries with different allocation needs
875        let plan_entries = vec![
876            create_allocation_plan_entry(table_id, &[1], &[(0, 50), (50, 100)]), // need 1 allocation
877            create_allocation_plan_entry(table_id, &[2, 3], &[(100, 150), (150, 200)]), // no allocation
878            create_allocation_plan_entry(table_id, &[4], &[(200, 250), (250, 300), (300, 400)]), // need 2 allocations
879        ];
880
881        let result = AllocateRegion::convert_to_repartition_plans(
882            table_id,
883            &mut next_region_number,
884            &plan_entries,
885            &create_current_region_routes(table_id, &[1, 2, 3, 4]),
886        )
887        .unwrap();
888
889        assert_eq!(result.len(), 3);
890        assert_eq!(result[0].allocated_region_ids.len(), 1);
891        assert_eq!(result[1].allocated_region_ids.len(), 0);
892        assert_eq!(result[2].allocated_region_ids.len(), 2);
893        // next_region_number should be incremented by 3 total
894        assert_eq!(next_region_number, 13);
895    }
896
897    #[test]
898    fn test_count_regions_to_allocate() {
899        let table_id = 1024;
900        let mut next_region_number = 10;
901
902        let plan_entries = vec![
903            create_allocation_plan_entry(table_id, &[1], &[(0, 50), (50, 100)]), // 1 allocation
904            create_allocation_plan_entry(table_id, &[2, 3], &[(100, 200)]), // 0 allocation (deallocate)
905            create_allocation_plan_entry(table_id, &[4], &[(200, 250), (250, 300)]), // 1 allocation
906        ];
907
908        let repartition_plans = AllocateRegion::convert_to_repartition_plans(
909            table_id,
910            &mut next_region_number,
911            &plan_entries,
912            &create_current_region_routes(table_id, &[1, 2, 3, 4]),
913        )
914        .unwrap();
915
916        let count = AllocateRegion::count_regions_to_allocate(&repartition_plans);
917        assert_eq!(count, 2);
918    }
919
920    #[test]
921    fn test_collect_allocate_regions() {
922        let table_id = 1024;
923        let mut next_region_number = 10;
924
925        let plan_entries = vec![
926            create_allocation_plan_entry(table_id, &[1], &[(0, 50), (50, 100)]), // 1 allocation
927            create_allocation_plan_entry(table_id, &[2], &[(100, 150), (150, 200)]), // 1 allocation
928        ];
929
930        let repartition_plans = AllocateRegion::convert_to_repartition_plans(
931            table_id,
932            &mut next_region_number,
933            &plan_entries,
934            &create_current_region_routes(table_id, &[1, 2]),
935        )
936        .unwrap();
937
938        let allocate_regions = AllocateRegion::collect_allocate_regions(&repartition_plans);
939        assert_eq!(allocate_regions.len(), 2);
940        assert_eq!(allocate_regions[0].region_id, RegionId::new(table_id, 10));
941        assert_eq!(allocate_regions[1].region_id, RegionId::new(table_id, 11));
942    }
943
944    #[test]
945    fn test_prepare_region_allocation_data() {
946        let table_id = 1024;
947        let regions = [
948            create_target_region_descriptor(table_id, 10, "x", 0, 50),
949            create_target_region_descriptor(table_id, 11, "x", 50, 100),
950        ];
951        let region_refs: Vec<&TargetRegionDescriptor> = regions.iter().collect();
952
953        let result = AllocateRegion::prepare_region_allocation_data(&region_refs).unwrap();
954
955        assert_eq!(result.len(), 2);
956        assert_eq!(result[0].0, 10);
957        assert_eq!(result[1].0, 11);
958        // Verify partition expressions are serialized
959        assert!(!result[0].1.is_empty());
960        assert!(!result[1].1.is_empty());
961    }
962
963    #[tokio::test]
964    async fn test_execute_plan_uses_latest_table_route_after_lock() {
965        let env = TestingEnv::new();
966        let table_id = 1024;
967        let original_region_routes = create_current_region_routes(table_id, &[1]);
968        env.create_physical_table_metadata_for_repartition(
969            table_id,
970            original_region_routes,
971            test_region_wal_options(&[1]),
972        )
973        .await;
974
975        let (sender, mut receiver) = mpsc::channel(1);
976        let node_manager = Arc::new(MockDatanodeManager::new(DatanodeWatcher::new(sender)));
977        let mut ctx = new_parent_context(&env, node_manager, table_id);
978        ctx.persistent_ctx.plans = vec![RepartitionPlanEntry {
979            group_id: Uuid::new_v4(),
980            source_regions: vec![],
981            target_regions: vec![create_target_region_descriptor(table_id, 3, "x", 0, 100)],
982            allocated_region_ids: vec![RegionId::new(table_id, 3)],
983            pending_deallocate_region_ids: vec![],
984            transition_map: vec![],
985            original_target_routes: vec![],
986        }];
987        let concurrent_region_route = create_current_region_routes(table_id, &[2])
988            .into_iter()
989            .next()
990            .unwrap();
991        let procedure_ctx = ProcedureContext {
992            procedure_id: ProcedureId::random(),
993            provider: Arc::new(ConcurrentTableRouteUpdateProvider {
994                inner: MockContextProvider::default(),
995                table_metadata_manager: env.table_metadata_manager.clone(),
996                table_id,
997                concurrent_region_route,
998                region_wal_options: test_region_wal_options(&[1, 2]),
999            }),
1000            event_context: None,
1001        };
1002        let mut state = ExecutePlan;
1003
1004        state.next(&mut ctx, &procedure_ctx).await.unwrap();
1005
1006        let (_, request) = receiver.recv().await.unwrap();
1007        let Some(Body::Create(create)) = request.body else {
1008            unreachable!()
1009        };
1010        assert!(create.requirements.unwrap().object_storage);
1011
1012        let region_ids = current_parent_region_routes(&ctx)
1013            .await
1014            .into_iter()
1015            .map(|route| route.region.id)
1016            .collect::<Vec<_>>();
1017        assert_eq!(
1018            region_ids,
1019            vec![
1020                RegionId::new(table_id, 1),
1021                RegionId::new(table_id, 2),
1022                RegionId::new(table_id, 3),
1023            ]
1024        );
1025    }
1026
1027    async fn check_execute_plan_skip_wal_provider(existing_wal_options: Option<WalOptions>) {
1028        let env = TestingEnv::new();
1029        let table_id = 1024;
1030        let routes = create_current_region_routes(table_id, &[1]);
1031        let mut table_info = new_test_table_info_with_name(table_id, "test_table");
1032        table_info.meta.column_ids = vec![0, 1, 2];
1033        table_info.meta.options.skip_wal = true;
1034        let stored_wal_options = existing_wal_options
1035            .clone()
1036            .map(|options| HashMap::from([(1, options)]))
1037            .unwrap_or_default();
1038        env.table_metadata_manager
1039            .create_table_metadata(
1040                table_info,
1041                TableRouteValue::physical(routes),
1042                stored_wal_options,
1043            )
1044            .await
1045            .unwrap();
1046
1047        let (sender, mut receiver) = mpsc::channel(1);
1048        let node_manager = Arc::new(MockDatanodeManager::new(DatanodeWatcher::new(sender)));
1049        let mut ctx = new_parent_context(&env, node_manager, table_id);
1050        if matches!(&existing_wal_options, Some(WalOptions::Kafka(_))) {
1051            ctx.wal_options_allocator = Arc::new(TestKafkaWalOptionsAllocator);
1052        }
1053        ctx.persistent_ctx.plans = vec![RepartitionPlanEntry {
1054            group_id: Uuid::new_v4(),
1055            source_regions: vec![],
1056            target_regions: vec![create_target_region_descriptor(table_id, 2, "x", 0, 100)],
1057            allocated_region_ids: vec![RegionId::new(table_id, 2)],
1058            pending_deallocate_region_ids: vec![],
1059            transition_map: vec![],
1060            original_target_routes: vec![],
1061        }];
1062        let mut state = ExecutePlan;
1063        state
1064            .next(&mut ctx, &TestingEnv::procedure_context())
1065            .await
1066            .unwrap();
1067
1068        let (_, request) = receiver.recv().await.unwrap();
1069        let Some(Body::Create(create)) = request.body else {
1070            unreachable!()
1071        };
1072        assert_eq!(
1073            Some("true"),
1074            create.options.get(SKIP_WAL_KEY).map(String::as_str)
1075        );
1076        let new_wal_options: WalOptions =
1077            serde_json::from_str(create.options.get(WAL_OPTIONS_KEY).unwrap()).unwrap();
1078        let expected = match &existing_wal_options {
1079            Some(WalOptions::Noop) => WalOptions::Noop,
1080            Some(WalOptions::Kafka(_)) => WalOptions::Kafka(KafkaWalOptions {
1081                topic: "new-topic".to_string(),
1082                initial_pruned_entry_id: Some(0),
1083            }),
1084            None | Some(WalOptions::RaftEngine) | Some(WalOptions::ObjectStore(_)) => {
1085                WalOptions::RaftEngine
1086            }
1087        };
1088        assert_eq!(expected, new_wal_options);
1089        let route = ctx.get_table_route_value().await.unwrap();
1090        let wal_options = get_region_wal_options(&ctx.table_metadata_manager, &route, table_id)
1091            .await
1092            .unwrap();
1093        assert_eq!(Some(&expected), wal_options.get(&2));
1094        let legacy_default = WalOptions::RaftEngine;
1095        assert_eq!(
1096            Some(existing_wal_options.as_ref().unwrap_or(&legacy_default)),
1097            wal_options.get(&1)
1098        );
1099    }
1100
1101    #[tokio::test]
1102    async fn test_execute_plan_keeps_real_wal_provider_for_skipped_table() {
1103        check_execute_plan_skip_wal_provider(Some(WalOptions::RaftEngine)).await;
1104    }
1105
1106    #[tokio::test]
1107    async fn test_execute_plan_keeps_legacy_raft_provider_for_skipped_table() {
1108        check_execute_plan_skip_wal_provider(None).await;
1109    }
1110
1111    #[tokio::test]
1112    async fn test_execute_plan_keeps_kafka_wal_provider_for_skipped_table() {
1113        check_execute_plan_skip_wal_provider(Some(WalOptions::Kafka(KafkaWalOptions::new(
1114            "existing-topic".to_string(),
1115        ))))
1116        .await;
1117    }
1118
1119    #[tokio::test]
1120    async fn test_execute_plan_treats_object_store_as_real_wal_provider_for_skipped_table() {
1121        check_execute_plan_skip_wal_provider(Some(WalOptions::ObjectStore(
1122            ObjectStoreWalOptions::new("wal".to_string()),
1123        )))
1124        .await;
1125    }
1126
1127    #[tokio::test]
1128    async fn test_execute_plan_keeps_noop_for_table_created_without_wal() {
1129        check_execute_plan_skip_wal_provider(Some(WalOptions::Noop)).await;
1130    }
1131
1132    #[test]
1133    fn test_allocate_region_state_backward_compatibility() {
1134        // Arrange
1135        let serialized = r#"{"repartition_state":"AllocateRegion","plan_entries":[]}"#;
1136
1137        // Act
1138        let state: Box<dyn State> = serde_json::from_str(serialized).unwrap();
1139
1140        // Assert
1141        let allocate_region = state
1142            .as_any()
1143            .downcast_ref::<AllocateRegion>()
1144            .expect("expected AllocateRegion state");
1145        match allocate_region {
1146            AllocateRegion::Build(build_plan) => assert!(build_plan.plan_entries.is_empty()),
1147            AllocateRegion::Execute(_) => panic!("expected build plan"),
1148        }
1149    }
1150
1151    #[test]
1152    fn test_allocate_region_state_round_trip() {
1153        // Arrange
1154        let state: Box<dyn State> = Box::new(AllocateRegion::new(vec![]));
1155
1156        // Act
1157        let serialized = serde_json::to_string(&state).unwrap();
1158        let deserialized: Box<dyn State> = serde_json::from_str(&serialized).unwrap();
1159
1160        // Assert
1161        assert_eq!(
1162            serialized,
1163            r#"{"repartition_state":"AllocateRegion","Build":{"plan_entries":[]}}"#
1164        );
1165        let allocate_region = deserialized
1166            .as_any()
1167            .downcast_ref::<AllocateRegion>()
1168            .expect("expected AllocateRegion state");
1169        match allocate_region {
1170            AllocateRegion::Build(build_plan) => assert!(build_plan.plan_entries.is_empty()),
1171            AllocateRegion::Execute(_) => panic!("expected build plan"),
1172        }
1173    }
1174
1175    #[test]
1176    fn test_allocate_region_execute_state_round_trip() {
1177        // Arrange
1178        let state: Box<dyn State> = Box::new(AllocateRegion::Execute(ExecutePlan));
1179
1180        // Act
1181        let serialized = serde_json::to_string(&state).unwrap();
1182        let deserialized: Box<dyn State> = serde_json::from_str(&serialized).unwrap();
1183
1184        // Assert
1185        assert_eq!(
1186            serialized,
1187            r#"{"repartition_state":"AllocateRegion","Execute":null}"#
1188        );
1189        let allocate_region = deserialized
1190            .as_any()
1191            .downcast_ref::<AllocateRegion>()
1192            .expect("expected AllocateRegion state");
1193        match allocate_region {
1194            AllocateRegion::Execute(_) => {}
1195            AllocateRegion::Build(_) => panic!("expected execute plan"),
1196        }
1197    }
1198    #[test]
1199    fn test_collect_allocation_sources_use_target_overlap() {
1200        let table_id = 1024;
1201        let mut plan = RepartitionPlanEntry {
1202            group_id: Uuid::nil(),
1203            source_regions: vec![
1204                create_region_descriptor(table_id, 2, "x", 100, 200),
1205                create_region_descriptor(table_id, 1, "x", 0, 100),
1206            ],
1207            target_regions: vec![
1208                create_target_region_descriptor(table_id, 3, "x", 0, 50),
1209                create_target_region_descriptor(table_id, 4, "x", 50, 150),
1210                create_target_region_descriptor(table_id, 5, "x", 150, 200),
1211            ],
1212            allocated_region_ids: (3..=5).map(|n| RegionId::new(table_id, n)).collect(),
1213            pending_deallocate_region_ids: vec![],
1214            transition_map: vec![vec![1, 2], vec![0, 1]],
1215            original_target_routes: vec![],
1216        };
1217        let mut routes = create_current_region_routes(table_id, &[1, 2]);
1218        routes[0].leader_peer = Some(Peer::empty(10));
1219        routes[1].leader_peer = Some(Peer::empty(20));
1220        assert_eq!(
1221            collect_allocation_sources(&[plan.clone()], &routes),
1222            vec![
1223                (RegionId::new(table_id, 3), Some(10)),
1224                (RegionId::new(table_id, 4), Some(10)),
1225                (RegionId::new(table_id, 5), Some(20)),
1226            ]
1227        );
1228        // Source enumeration does not change the tie-break for overlapping targets.
1229        plan.source_regions.reverse();
1230        plan.transition_map.reverse();
1231        assert_eq!(
1232            collect_allocation_sources(std::slice::from_ref(&plan), &routes)[1],
1233            (RegionId::new(table_id, 4), Some(10))
1234        );
1235        plan.transition_map.clear();
1236        assert_eq!(
1237            collect_allocation_sources(std::slice::from_ref(&plan), &routes),
1238            (3..=5)
1239                .map(|number| (RegionId::new(table_id, number), Some(10)))
1240                .collect::<Vec<_>>()
1241        );
1242        plan.source_regions.clear();
1243        assert_eq!(
1244            collect_allocation_sources(&[plan], &routes),
1245            (3..=5)
1246                .map(|number| (RegionId::new(table_id, number), None))
1247                .collect::<Vec<_>>()
1248        );
1249    }
1250
1251    #[test]
1252    fn test_group_allocations_preserves_region_partition_mapping() {
1253        use common_meta::affinity::AffinityRequirements;
1254        let table_id = 1024;
1255        let requirements = |zone: &str| {
1256            AffinityRequirements::new(HashMap::from([("ZONE".into(), zone.into())])).unwrap()
1257        };
1258        let region_requirements = AllocationAffinity {
1259            requirements: vec![
1260                Some(requirements("a")),
1261                Some(requirements("b")),
1262                Some(requirements("a")),
1263            ],
1264            target_sources: HashMap::from([
1265                (RegionId::new(table_id, 1), 0),
1266                (RegionId::new(table_id, 2), 1),
1267                (RegionId::new(table_id, 3), 2),
1268            ]),
1269        };
1270        let regions = vec![
1271            (1, "p1".into()),
1272            (2, "p2".into()),
1273            (3, "p3".into()),
1274            (4, "p4".into()),
1275        ];
1276        let groups = group_allocations(table_id, &regions, &region_requirements);
1277        assert!(ptr::eq(
1278            groups[0].affinity.unwrap(),
1279            region_requirements.requirements[0].as_ref().unwrap(),
1280        ));
1281        assert!(!ptr::eq(
1282            region_requirements.requirements[0].as_ref().unwrap(),
1283            region_requirements.requirements[2].as_ref().unwrap(),
1284        ));
1285        assert_eq!(
1286            groups,
1287            vec![
1288                AllocationGroup {
1289                    affinity: region_requirements.requirements[0].as_ref(),
1290                    regions: vec![(1, "p1"), (3, "p3")],
1291                },
1292                AllocationGroup {
1293                    affinity: region_requirements.requirements[1].as_ref(),
1294                    regions: vec![(2, "p2")],
1295                },
1296                AllocationGroup {
1297                    affinity: None,
1298                    regions: vec![(4, "p4")],
1299                },
1300            ]
1301        );
1302    }
1303
1304    #[tokio::test]
1305    async fn test_execute_refreshes_affinity_on_retry() {
1306        use std::sync::Mutex;
1307        use std::sync::atomic::{AtomicUsize, Ordering};
1308
1309        use common_meta::affinity::AffinityResolver;
1310        use common_meta::error::{self as meta_error, Result as MetaResult};
1311        use common_meta::key::table_route::TableRouteKey;
1312        use common_meta::peer::PeerAllocator;
1313
1314        #[derive(Default)]
1315        struct ChangingResolver {
1316            calls: AtomicUsize,
1317        }
1318
1319        #[async_trait::async_trait]
1320        impl AffinityResolver for ChangingResolver {
1321            async fn resolve(
1322                &self,
1323                references: &[AffinityReference],
1324            ) -> MetaResult<Vec<Option<AffinityRequirements>>> {
1325                assert_eq!(
1326                    references,
1327                    &[
1328                        AffinityReference::Datanode(1),
1329                        AffinityReference::Datanode(2)
1330                    ]
1331                );
1332                let generation = self.calls.fetch_add(1, Ordering::Relaxed);
1333                Ok(references
1334                    .iter()
1335                    .map(|_| {
1336                        AffinityRequirements::new(HashMap::from([(
1337                            "ZONE".into(),
1338                            format!("zone-{generation}"),
1339                        )]))
1340                    })
1341                    .collect())
1342            }
1343        }
1344
1345        #[derive(Default)]
1346        struct RecordingAllocator(Mutex<Vec<Option<AffinityRequirements>>>);
1347
1348        #[async_trait::async_trait]
1349        impl PeerAllocator for RecordingAllocator {
1350            async fn alloc(&self, _num: usize, ctx: &PeerAllocContext) -> MetaResult<Vec<Peer>> {
1351                self.0.lock().unwrap().push(ctx.affinity.clone());
1352                meta_error::UnexpectedSnafu {
1353                    err_msg: "stop before creating regions",
1354                }
1355                .fail()
1356            }
1357        }
1358
1359        let env = TestingEnv::new();
1360        let table_id = 1024;
1361        let mut routes = create_current_region_routes(table_id, &[1, 2]);
1362        routes[1].leader_peer = Some(Peer::empty(2));
1363        env.create_physical_table_metadata_for_repartition(
1364            table_id,
1365            routes,
1366            test_region_wal_options(&[1, 2]),
1367        )
1368        .await;
1369        let (sender, _receiver) = mpsc::channel(1);
1370        let mut ctx = new_parent_context(
1371            &env,
1372            Arc::new(MockDatanodeManager::new(DatanodeWatcher::new(sender))),
1373            table_id,
1374        );
1375        let resolver = Arc::new(ChangingResolver::default());
1376        let allocator = Arc::new(RecordingAllocator::default());
1377        ctx.affinity_resolver = Some(resolver.clone());
1378        ctx.region_routes_allocator = allocator.clone();
1379        ctx.persistent_ctx.plans = vec![RepartitionPlanEntry {
1380            group_id: Uuid::nil(),
1381            source_regions: vec![
1382                create_region_descriptor(table_id, 1, "x", 0, 100),
1383                create_region_descriptor(table_id, 2, "x", 100, 200),
1384            ],
1385            target_regions: vec![
1386                create_target_region_descriptor(table_id, 3, "x", 0, 50),
1387                create_target_region_descriptor(table_id, 4, "x", 50, 100),
1388                create_target_region_descriptor(table_id, 5, "x", 100, 200),
1389            ],
1390            allocated_region_ids: (3..=5).map(|n| RegionId::new(table_id, n)).collect(),
1391            pending_deallocate_region_ids: vec![],
1392            transition_map: vec![vec![0, 1], vec![2]],
1393            original_target_routes: vec![],
1394        }];
1395        let procedure_ctx = ProcedureContext {
1396            procedure_id: ProcedureId::random(),
1397            provider: Arc::new(MockContextProvider::default()),
1398            event_context: None,
1399        };
1400        for _ in 0..2 {
1401            assert!(ExecutePlan.next(&mut ctx, &procedure_ctx).await.is_err());
1402        }
1403        assert_eq!(resolver.calls.load(Ordering::Relaxed), 2);
1404        assert_eq!(
1405            *allocator.0.lock().unwrap(),
1406            (0..2)
1407                .map(|generation| AffinityRequirements::new(HashMap::from([(
1408                    "ZONE".into(),
1409                    format!("zone-{generation}")
1410                ),])))
1411                .collect::<Vec<_>>()
1412        );
1413        // Without a resolver, allocation does not need source route metadata.
1414        // Removing it makes any accidental lookup fail before the recording allocator.
1415        ctx.affinity_resolver = None;
1416        env.kv_backend
1417            .delete(TableRouteKey::new(table_id).to_string().as_bytes(), false)
1418            .await
1419            .unwrap();
1420        assert!(ExecutePlan.next(&mut ctx, &procedure_ctx).await.is_err());
1421        assert_eq!(resolver.calls.load(Ordering::Relaxed), 2);
1422        let allocations = allocator.0.lock().unwrap();
1423        assert_eq!(allocations.len(), 3);
1424        assert_eq!(allocations.last(), Some(&None));
1425    }
1426
1427    #[tokio::test]
1428    async fn test_resolved_affinity_preserves_source_associations() {
1429        use common_meta::affinity::AffinityResolver;
1430        use common_meta::error::Result as MetaResult;
1431
1432        struct Resolver(Vec<Option<AffinityRequirements>>);
1433
1434        #[async_trait::async_trait]
1435        impl AffinityResolver for Resolver {
1436            async fn resolve(
1437                &self,
1438                references: &[AffinityReference],
1439            ) -> MetaResult<Vec<Option<AffinityRequirements>>> {
1440                assert_eq!(
1441                    references,
1442                    &[
1443                        AffinityReference::Datanode(2),
1444                        AffinityReference::Datanode(1)
1445                    ]
1446                );
1447                Ok(self.0.clone())
1448            }
1449        }
1450
1451        let env = TestingEnv::new();
1452        let table_id = 1024;
1453        let mut routes = create_current_region_routes(table_id, &[1, 2]);
1454        routes[1].leader_peer = Some(Peer::empty(2));
1455        env.create_physical_table_metadata_for_repartition(
1456            table_id,
1457            routes,
1458            test_region_wal_options(&[1, 2]),
1459        )
1460        .await;
1461        let (sender, _receiver) = mpsc::channel(1);
1462        let mut ctx = new_parent_context(
1463            &env,
1464            Arc::new(MockDatanodeManager::new(DatanodeWatcher::new(sender))),
1465            table_id,
1466        );
1467        ctx.persistent_ctx.plans = vec![RepartitionPlanEntry {
1468            group_id: Uuid::nil(),
1469            source_regions: vec![
1470                create_region_descriptor(table_id, 1, "x", 0, 100),
1471                create_region_descriptor(table_id, 2, "x", 100, 200),
1472            ],
1473            target_regions: vec![
1474                create_target_region_descriptor(table_id, 3, "x", 100, 200),
1475                create_target_region_descriptor(table_id, 4, "x", 0, 50),
1476                create_target_region_descriptor(table_id, 5, "x", 50, 100),
1477            ],
1478            allocated_region_ids: (3..=5).map(|n| RegionId::new(table_id, n)).collect(),
1479            pending_deallocate_region_ids: vec![],
1480            transition_map: vec![vec![1, 2], vec![0]],
1481            original_target_routes: vec![],
1482        }];
1483        let a = AffinityRequirements::new(HashMap::from([("ZONE".into(), "a".into())])).unwrap();
1484        let b = AffinityRequirements::new(HashMap::from([("ZONE".into(), "b".into())])).unwrap();
1485        assert!(matches!(
1486            resolve_allocation_affinity(&ctx, &Resolver(vec![None, Some(a.clone())])).await,
1487            Err(error::Error::Unexpected { .. })
1488        ));
1489        ctx.affinity_fallback = AffinityFallback::Unconstrained;
1490        for (resolved, expected) in [
1491            (
1492                vec![Some(b.clone()), Some(a.clone())],
1493                HashMap::from([
1494                    (RegionId::new(table_id, 3), b.clone()),
1495                    (RegionId::new(table_id, 4), a.clone()),
1496                    (RegionId::new(table_id, 5), a.clone()),
1497                ]),
1498            ),
1499            (
1500                vec![None, Some(a.clone())],
1501                HashMap::from([
1502                    (RegionId::new(table_id, 4), a.clone()),
1503                    (RegionId::new(table_id, 5), a),
1504                ]),
1505            ),
1506            (
1507                vec![Some(b.clone()), None],
1508                HashMap::from([(RegionId::new(table_id, 3), b)]),
1509            ),
1510        ] {
1511            let affinity = resolve_allocation_affinity(&ctx, &Resolver(resolved))
1512                .await
1513                .unwrap();
1514            assert_eq!(affinity.requirements.len(), 2);
1515            assert_eq!(
1516                affinity.target_sources[&RegionId::new(table_id, 4)],
1517                affinity.target_sources[&RegionId::new(table_id, 5)]
1518            );
1519            let actual = affinity
1520                .target_sources
1521                .iter()
1522                .filter_map(|(target, index)| {
1523                    affinity.requirements[*index]
1524                        .as_ref()
1525                        .map(|requirements| (*target, requirements))
1526                })
1527                .collect::<HashMap<_, _>>();
1528            if let Some(first) = actual.get(&RegionId::new(table_id, 4)) {
1529                let second = actual[&RegionId::new(table_id, 5)];
1530                assert!(ptr::eq(*first, second));
1531            }
1532            assert_eq!(
1533                actual,
1534                expected
1535                    .iter()
1536                    .map(|(target, requirements)| (*target, requirements))
1537                    .collect()
1538            );
1539        }
1540    }
1541
1542    #[tokio::test]
1543    async fn test_missing_source_leader_requires_explicit_fallback() {
1544        use common_meta::error::Result as MetaResult;
1545
1546        struct Resolver;
1547
1548        #[async_trait::async_trait]
1549        impl AffinityResolver for Resolver {
1550            async fn resolve(
1551                &self,
1552                references: &[AffinityReference],
1553            ) -> MetaResult<Vec<Option<AffinityRequirements>>> {
1554                assert!(references.is_empty());
1555                Ok(vec![])
1556            }
1557        }
1558
1559        let env = TestingEnv::new();
1560        let table_id = 1024;
1561        let mut routes = create_current_region_routes(table_id, &[1]);
1562        routes[0].leader_peer = None;
1563        env.create_physical_table_metadata_for_repartition(
1564            table_id,
1565            routes,
1566            test_region_wal_options(&[1]),
1567        )
1568        .await;
1569        let mut ctx = new_parent_context(&env, Arc::new(MockDatanodeManager::new(())), table_id);
1570        ctx.persistent_ctx.plans = vec![RepartitionPlanEntry {
1571            group_id: Uuid::nil(),
1572            source_regions: vec![create_region_descriptor(table_id, 1, "x", 0, 100)],
1573            target_regions: vec![create_target_region_descriptor(table_id, 2, "x", 0, 100)],
1574            allocated_region_ids: vec![RegionId::new(table_id, 2)],
1575            pending_deallocate_region_ids: vec![],
1576            transition_map: vec![vec![0]],
1577            original_target_routes: vec![],
1578        }];
1579        assert!(matches!(
1580            resolve_allocation_affinity(&ctx, &Resolver).await,
1581            Err(error::Error::Unexpected { .. })
1582        ));
1583        ctx.affinity_fallback = AffinityFallback::Unconstrained;
1584        assert!(
1585            resolve_allocation_affinity(&ctx, &Resolver)
1586                .await
1587                .unwrap()
1588                .target_sources
1589                .is_empty()
1590        );
1591    }
1592}