1use 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 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 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, ®ion_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 ®ions,
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 !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 let _operating_guards = Context::register_operating_regions(
253 &ctx.memory_region_keeper,
254 &new_allocated_region_routes,
255 )?;
256 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 let table_lock = TableLock::Write(table_id).into();
267 let _guard = procedure_ctx.provider.acquire_lock(&table_lock).await;
268 let table_route_value = ctx.get_table_route_value().await?;
271 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 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, ®ion_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 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 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 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 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 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 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 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
464fn 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#[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(|®ion_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 ¤t_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 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 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 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 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 let plan_entries = vec![
876 create_allocation_plan_entry(table_id, &[1], &[(0, 50), (50, 100)]), create_allocation_plan_entry(table_id, &[2, 3], &[(100, 150), (150, 200)]), create_allocation_plan_entry(table_id, &[4], &[(200, 250), (250, 300), (300, 400)]), ];
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 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)]), create_allocation_plan_entry(table_id, &[2, 3], &[(100, 200)]), create_allocation_plan_entry(table_id, &[4], &[(200, 250), (250, 300)]), ];
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)]), create_allocation_plan_entry(table_id, &[2], &[(100, 150), (150, 200)]), ];
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(®ion_refs).unwrap();
954
955 assert_eq!(result.len(), 2);
956 assert_eq!(result[0].0, 10);
957 assert_eq!(result[1].0, 11);
958 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 let serialized = r#"{"repartition_state":"AllocateRegion","plan_entries":[]}"#;
1136
1137 let state: Box<dyn State> = serde_json::from_str(serialized).unwrap();
1139
1140 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 let state: Box<dyn State> = Box::new(AllocateRegion::new(vec![]));
1155
1156 let serialized = serde_json::to_string(&state).unwrap();
1158 let deserialized: Box<dyn State> = serde_json::from_str(&serialized).unwrap();
1159
1160 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 let state: Box<dyn State> = Box::new(AllocateRegion::Execute(ExecutePlan));
1179
1180 let serialized = serde_json::to_string(&state).unwrap();
1182 let deserialized: Box<dyn State> = serde_json::from_str(&serialized).unwrap();
1183
1184 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 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, ®ions, ®ion_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 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}