Skip to main content

meta_srv/procedure/
repartition.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
15pub mod allocate_region;
16pub mod collect;
17pub mod deallocate_region;
18pub mod dispatch;
19pub mod gc_requirement;
20pub mod group;
21pub mod plan;
22pub mod repartition_end;
23pub mod repartition_start;
24pub mod update_partition_metadata;
25pub mod utils;
26
27use std::any::Any;
28use std::collections::{HashMap, HashSet};
29use std::fmt::{Debug, Display};
30use std::time::{Duration, Instant};
31
32use common_error::ext::BoxedError;
33use common_meta::cache_invalidator::CacheInvalidatorRef;
34use common_meta::ddl::DdlContext;
35use common_meta::ddl::allocator::region_routes::RegionRoutesAllocatorRef;
36use common_meta::ddl::allocator::wal_options::WalOptionsAllocatorRef;
37use common_meta::ddl::utils::get_region_wal_options;
38use common_meta::ddl_manager::{RepartitionProcedureFactory, RepartitionSource};
39use common_meta::instruction::CacheIdent;
40use common_meta::key::datanode_table::RegionInfo;
41use common_meta::key::table_info::TableInfoValue;
42use common_meta::key::table_route::TableRouteValue;
43use common_meta::key::{DeserializedValueWithBytes, TableMetadataManagerRef};
44use common_meta::lock_key::{CatalogLock, SchemaLock, TableLock, TableNameLock};
45use common_meta::node_manager::NodeManagerRef;
46use common_meta::region_keeper::{MemoryRegionKeeperRef, OperatingRegionGuard};
47use common_meta::region_registry::LeaderRegionRegistryRef;
48use common_meta::rpc::router::{RegionRoute, operating_leader_region_roles};
49use common_meta::wal_provider::RegionWalOptions;
50use common_procedure::error::{FromJsonSnafu, ToJsonSnafu};
51use common_procedure::{
52    BoxedProcedure, Context as ProcedureContext, Error as ProcedureError, EventContext,
53    EventTrigger, LockKey, Procedure, ProcedureManagerRef, Result as ProcedureResult, Status,
54    StringKey,
55};
56use common_telemetry::{error, info, warn};
57use partition::expr::PartitionExpr;
58use serde::{Deserialize, Serialize};
59use snafu::{OptionExt, ResultExt};
60use store_api::storage::TableId;
61use table::table_name::TableName;
62
63use crate::error::{self, Result};
64use crate::event::repartition::{REPARTITION_EVENT_TYPE, RepartitionEvent};
65use crate::procedure::repartition::collect::ProcedureMeta;
66use crate::procedure::repartition::deallocate_region::DeallocateRegion;
67use crate::procedure::repartition::gc_requirement::RepartitionGcRequirementManagerRef;
68use crate::procedure::repartition::group::{
69    Context as RepartitionGroupContext, RepartitionGroupProcedure, region_routes,
70};
71use crate::procedure::repartition::plan::RepartitionPlanEntry;
72use crate::procedure::repartition::repartition_start::{RepartitionFrom, RepartitionStart};
73use crate::procedure::repartition::update_partition_metadata::PartitionMetadataUpdate;
74use crate::procedure::repartition::utils::{
75    get_datanode_table_value, rollback_group_metadata_routes,
76};
77use crate::service::mailbox::MailboxRef;
78
79#[cfg(test)]
80pub mod test_util;
81
82#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
83pub struct PersistentContext {
84    pub catalog_name: String,
85    pub schema_name: String,
86    pub table_name: String,
87    pub table_id: TableId,
88    pub plans: Vec<RepartitionPlanEntry>,
89    #[serde(default)]
90    /// Records failed sub-procedures for parent rollback selection.
91    ///
92    /// The parent repartition procedure uses these entries to decide which plans
93    /// require group-metadata restoration and allocated-region cleanup.
94    pub failed_procedures: Vec<ProcedureMeta>,
95    #[serde(default)]
96    /// Records unknown sub-procedures for parent rollback selection.
97    ///
98    /// Unknown procedures are treated the same as failed ones when selecting the
99    /// plan subset that must be rolled back by the parent procedure.
100    pub unknown_procedures: Vec<ProcedureMeta>,
101    /// The timeout for repartition operations.
102    #[serde(with = "humantime_serde", default = "default_timeout")]
103    pub timeout: Duration,
104    #[serde(default)]
105    /// Records table-level partition metadata updated by this repartition.
106    pub partition_metadata_update: Option<PartitionMetadataUpdate>,
107}
108
109fn default_timeout() -> Duration {
110    Duration::from_secs(2 * 60)
111}
112
113impl PersistentContext {
114    /// Creates a new [PersistentContext] with the given table name, table id and timeout.
115    ///
116    /// If the timeout is not provided, the default timeout will be used.
117    pub fn new(
118        TableName {
119            catalog_name,
120            schema_name,
121            table_name,
122        }: TableName,
123        table_id: TableId,
124        timeout: Option<Duration>,
125    ) -> Self {
126        Self {
127            catalog_name,
128            schema_name,
129            table_name,
130            table_id,
131            plans: vec![],
132            failed_procedures: vec![],
133            unknown_procedures: vec![],
134            timeout: timeout.unwrap_or_else(default_timeout),
135            partition_metadata_update: None,
136        }
137    }
138
139    pub fn lock_key(&self) -> Vec<StringKey> {
140        vec![
141            CatalogLock::Read(&self.catalog_name).into(),
142            SchemaLock::read(&self.catalog_name, &self.schema_name).into(),
143            TableLock::Write(self.table_id).into(),
144            TableNameLock::new(&self.catalog_name, &self.schema_name, &self.table_name).into(),
145        ]
146    }
147}
148
149#[derive(Clone)]
150pub struct Context {
151    pub persistent_ctx: PersistentContext,
152    pub volatile_ctx: VolatileContext,
153    pub table_metadata_manager: TableMetadataManagerRef,
154    pub memory_region_keeper: MemoryRegionKeeperRef,
155    pub node_manager: NodeManagerRef,
156    pub leader_region_registry: LeaderRegionRegistryRef,
157    pub mailbox: MailboxRef,
158    pub server_addr: String,
159    pub cache_invalidator: CacheInvalidatorRef,
160    pub region_routes_allocator: RegionRoutesAllocatorRef,
161    pub wal_options_allocator: WalOptionsAllocatorRef,
162    pub start_time: Instant,
163}
164
165#[derive(Debug, Clone, Default)]
166pub struct VolatileContext {
167    pub metrics: Metrics,
168    pub dispatch_start_time: Option<Instant>,
169}
170
171/// Metrics of repartition.
172#[derive(Debug, Clone, Default)]
173pub struct Metrics {
174    /// Elapsed time of building plan.
175    build_plan_elapsed: Duration,
176    /// Elapsed time of allocating region.
177    allocate_region_elapsed: Duration,
178    /// Elapsed time of finishing groups.
179    finish_groups_elapsed: Duration,
180    /// Elapsed time of deallocating region.
181    deallocate_region_elapsed: Duration,
182}
183
184impl Display for Metrics {
185    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
186        let total = self.build_plan_elapsed
187            + self.allocate_region_elapsed
188            + self.finish_groups_elapsed
189            + self.deallocate_region_elapsed;
190        write!(f, "total: {:?}", total)?;
191        let mut parts = Vec::with_capacity(4);
192        if self.build_plan_elapsed > Duration::ZERO {
193            parts.push(format!("build_plan_elapsed: {:?}", self.build_plan_elapsed));
194        }
195        if self.allocate_region_elapsed > Duration::ZERO {
196            parts.push(format!(
197                "allocate_region_elapsed: {:?}",
198                self.allocate_region_elapsed
199            ));
200        }
201        if self.finish_groups_elapsed > Duration::ZERO {
202            parts.push(format!(
203                "finish_groups_elapsed: {:?}",
204                self.finish_groups_elapsed
205            ));
206        }
207        if self.deallocate_region_elapsed > Duration::ZERO {
208            parts.push(format!(
209                "deallocate_region_elapsed: {:?}",
210                self.deallocate_region_elapsed
211            ));
212        }
213
214        if !parts.is_empty() {
215            write!(f, ", {}", parts.join(", "))?;
216        }
217        Ok(())
218    }
219}
220
221impl Metrics {
222    /// Updates the elapsed time of building plan.
223    pub fn update_build_plan_elapsed(&mut self, elapsed: Duration) {
224        self.build_plan_elapsed += elapsed;
225    }
226
227    /// Updates the elapsed time of allocating region.
228    pub fn update_allocate_region_elapsed(&mut self, elapsed: Duration) {
229        self.allocate_region_elapsed += elapsed;
230    }
231
232    /// Updates the elapsed time of finishing groups.
233    pub fn update_finish_groups_elapsed(&mut self, elapsed: Duration) {
234        self.finish_groups_elapsed += elapsed;
235    }
236
237    /// Updates the elapsed time of deallocating region.
238    pub fn update_deallocate_region_elapsed(&mut self, elapsed: Duration) {
239        self.deallocate_region_elapsed += elapsed;
240    }
241}
242
243impl Context {
244    pub fn new(
245        ddl_ctx: &DdlContext,
246        mailbox: MailboxRef,
247        server_addr: String,
248        persistent_ctx: PersistentContext,
249    ) -> Self {
250        Self {
251            persistent_ctx,
252            table_metadata_manager: ddl_ctx.table_metadata_manager.clone(),
253            memory_region_keeper: ddl_ctx.memory_region_keeper.clone(),
254            node_manager: ddl_ctx.node_manager.clone(),
255            leader_region_registry: ddl_ctx.leader_region_registry.clone(),
256            mailbox,
257            server_addr,
258            cache_invalidator: ddl_ctx.cache_invalidator.clone(),
259            region_routes_allocator: ddl_ctx.table_metadata_allocator.region_routes_allocator(),
260            wal_options_allocator: ddl_ctx.table_metadata_allocator.wal_options_allocator(),
261            start_time: Instant::now(),
262            volatile_ctx: VolatileContext::default(),
263        }
264    }
265
266    /// Returns the next operation's timeout.
267    pub fn next_operation_timeout(&self) -> Option<Duration> {
268        self.persistent_ctx
269            .timeout
270            .checked_sub(self.start_time.elapsed())
271    }
272
273    /// Updates the elapsed time of building plan.
274    pub fn update_build_plan_elapsed(&mut self, elapsed: Duration) {
275        self.volatile_ctx.metrics.update_build_plan_elapsed(elapsed);
276    }
277
278    /// Updates the elapsed time of allocating region.
279    pub fn update_allocate_region_elapsed(&mut self, elapsed: Duration) {
280        self.volatile_ctx
281            .metrics
282            .update_allocate_region_elapsed(elapsed);
283    }
284
285    /// Updates the elapsed time of finishing groups.
286    pub fn update_finish_groups_elapsed(&mut self, elapsed: Duration) {
287        self.volatile_ctx
288            .metrics
289            .update_finish_groups_elapsed(elapsed);
290    }
291
292    /// Updates the elapsed time of deallocating region.
293    pub fn update_deallocate_region_elapsed(&mut self, elapsed: Duration) {
294        self.volatile_ctx
295            .metrics
296            .update_deallocate_region_elapsed(elapsed);
297    }
298
299    /// Retrieves the table route value for the given table id.
300    ///
301    /// Retry:
302    /// - Failed to retrieve the metadata of table.
303    ///
304    /// Abort:
305    /// - Table route not found.
306    pub async fn get_table_route_value(
307        &self,
308    ) -> Result<DeserializedValueWithBytes<TableRouteValue>> {
309        let table_id = self.persistent_ctx.table_id;
310        let table_route_value = self
311            .table_metadata_manager
312            .table_route_manager()
313            .table_route_storage()
314            .get_with_raw_bytes(table_id)
315            .await
316            .map_err(BoxedError::new)
317            .with_context(|_| error::RetryLaterWithSourceSnafu {
318                reason: format!("Failed to get table route for table: {}", table_id),
319            })?
320            .context(error::TableRouteNotFoundSnafu { table_id })?;
321
322        Ok(table_route_value)
323    }
324
325    /// Retrieves the table info value for the given table id.
326    ///
327    /// Retry:
328    /// - Failed to retrieve the metadata of table.
329    ///
330    /// Abort:
331    /// - Table info not found.
332    pub async fn get_raw_table_info_value(
333        &self,
334    ) -> Result<DeserializedValueWithBytes<TableInfoValue>> {
335        let table_id = self.persistent_ctx.table_id;
336        let table_info_value = self
337            .table_metadata_manager
338            .table_info_manager()
339            .get(table_id)
340            .await
341            .map_err(BoxedError::new)
342            .with_context(|_| error::RetryLaterWithSourceSnafu {
343                reason: format!("Failed to get table info for table: {}", table_id),
344            })?
345            .context(error::TableInfoNotFoundSnafu { table_id })?;
346
347        Ok(table_info_value)
348    }
349
350    pub async fn get_table_info_value(&self) -> Result<TableInfoValue> {
351        let table_info_value = self.get_raw_table_info_value().await?.into_inner();
352        Ok(table_info_value)
353    }
354
355    /// Updates the table info.
356    pub async fn update_table_info(
357        &self,
358        current_table_info_value: &DeserializedValueWithBytes<TableInfoValue>,
359        new_table_info_value: TableInfoValue,
360    ) -> Result<()> {
361        let table_id = self.persistent_ctx.table_id;
362        self.table_metadata_manager
363            .update_table_info(
364                current_table_info_value,
365                None,
366                new_table_info_value.table_info,
367            )
368            .await
369            .map_err(BoxedError::new)
370            .with_context(|_| error::RetryLaterWithSourceSnafu {
371                reason: format!("Failed to update table info for table: {}", table_id),
372            })
373    }
374
375    /// Updates the table route.
376    ///
377    /// Retry:
378    /// - Failed to retrieve the metadata of datanode table.
379    ///
380    /// Abort:
381    /// - Table route not found.
382    /// - Failed to update the table route.
383    pub async fn update_table_route(
384        &self,
385        current_table_route_value: &DeserializedValueWithBytes<TableRouteValue>,
386        new_region_routes: Vec<RegionRoute>,
387        new_region_wal_options: RegionWalOptions,
388    ) -> Result<()> {
389        let table_id = self.persistent_ctx.table_id;
390        if new_region_routes.is_empty() {
391            return error::UnexpectedSnafu {
392                violated: format!("new_region_routes is empty for table: {}", table_id),
393            }
394            .fail();
395        }
396        let datanode_id = new_region_routes
397            .first()
398            .unwrap()
399            .leader_peer
400            .as_ref()
401            .context(error::NoLeaderSnafu)?
402            .id;
403        let datanode_table_value =
404            get_datanode_table_value(&self.table_metadata_manager, table_id, datanode_id).await?;
405
406        let RegionInfo { region_options, .. } = &datanode_table_value.region_info;
407
408        let mut region_wal_options = get_region_wal_options(
409            &self.table_metadata_manager,
410            current_table_route_value,
411            table_id,
412        )
413        .await
414        .context(error::TableMetadataManagerSnafu)?;
415        // Legacy regions without a persisted WAL option use RaftEngine. Only
416        // existing routes get this default; allocated regions must supply one.
417        for route in current_table_route_value.region_routes().unwrap() {
418            region_wal_options
419                .entry(route.region.id.region_number())
420                .or_default();
421        }
422
423        // Merge and validate the new region wal options.
424        let validated_region_wal_options =
425            crate::procedure::repartition::utils::merge_and_validate_region_wal_options(
426                &region_wal_options,
427                new_region_wal_options,
428                &new_region_routes,
429                table_id,
430            )?;
431        info!(
432            "Updating table route for table: {}, new region routes: {:?}",
433            table_id, new_region_routes
434        );
435        self.table_metadata_manager
436            .update_table_route(
437                table_id,
438                datanode_table_value.region_info.clone(),
439                current_table_route_value,
440                new_region_routes,
441                region_options,
442                &validated_region_wal_options,
443            )
444            .await
445            .context(error::TableMetadataManagerSnafu)
446    }
447
448    /// Broadcasts the invalidate table cache message.
449    pub async fn invalidate_table_cache(&self) -> Result<()> {
450        let table_id = self.persistent_ctx.table_id;
451        let subject = format!(
452            "Invalidate table cache for repartition table, table: {}",
453            table_id,
454        );
455        let ctx = common_meta::cache_invalidator::Context {
456            subject: Some(subject),
457        };
458        let _ = self
459            .cache_invalidator
460            .invalidate(
461                &ctx,
462                &[
463                    CacheIdent::TableId(table_id),
464                    CacheIdent::TableName(TableName {
465                        catalog_name: self.persistent_ctx.catalog_name.clone(),
466                        schema_name: self.persistent_ctx.schema_name.clone(),
467                        table_name: self.persistent_ctx.table_name.clone(),
468                    }),
469                ],
470            )
471            .await;
472        Ok(())
473    }
474
475    pub fn register_operating_regions(
476        memory_region_keeper: &MemoryRegionKeeperRef,
477        region_routes: &[RegionRoute],
478    ) -> Result<Vec<OperatingRegionGuard>> {
479        let mut operating_guards = Vec::with_capacity(region_routes.len());
480        for (region_id, datanode_id, role) in operating_leader_region_roles(region_routes) {
481            let guard = memory_region_keeper
482                .register_with_role(datanode_id, region_id, role)
483                .context(error::RegionOperatingRaceSnafu {
484                    peer_id: datanode_id,
485                    region_id,
486                })?;
487            operating_guards.push(guard);
488        }
489        Ok(operating_guards)
490    }
491}
492
493#[async_trait::async_trait]
494#[typetag::serde(tag = "repartition_state")]
495pub(crate) trait State: Sync + Send + Debug {
496    fn name(&self) -> &'static str {
497        let type_name = std::any::type_name::<Self>();
498        // short name
499        type_name.split("::").last().unwrap_or(type_name)
500    }
501
502    /// Yields the next [State] and [Status].
503    async fn next(
504        &mut self,
505        ctx: &mut Context,
506        procedure_ctx: &ProcedureContext,
507    ) -> Result<(Box<dyn State>, Status)>;
508
509    fn as_any(&self) -> &dyn Any;
510}
511
512pub struct RepartitionProcedure {
513    state: Box<dyn State>,
514    context: Context,
515}
516
517#[derive(Debug, Serialize)]
518struct RepartitionData<'a> {
519    state: &'a dyn State,
520    persistent_ctx: &'a PersistentContext,
521}
522
523#[derive(Debug, Deserialize)]
524struct RepartitionDataOwned {
525    state: Box<dyn State>,
526    persistent_ctx: PersistentContext,
527}
528
529impl RepartitionProcedure {
530    const TYPE_NAME: &'static str = "metasrv-procedure::Repartition";
531
532    pub fn new(from: RepartitionFrom, to_exprs: Vec<PartitionExpr>, context: Context) -> Self {
533        let state = Box::new(RepartitionStart::new(from, to_exprs));
534
535        Self { state, context }
536    }
537
538    pub fn from_json<F>(json: &str, ctx_factory: F) -> ProcedureResult<Self>
539    where
540        F: FnOnce(PersistentContext) -> Context,
541    {
542        let RepartitionDataOwned {
543            state,
544            persistent_ctx,
545        } = serde_json::from_str(json).context(FromJsonSnafu)?;
546        let context = ctx_factory(persistent_ctx);
547
548        Ok(Self { state, context })
549    }
550
551    /// Returns whether parent rollback should run.
552    ///
553    /// This uses an "after repartition metadata update" semantic: once execution
554    /// reaches `UpdatePartitionMetadata` or any later rollback-active state,
555    /// rollback must try to clean metadata written by the repartition procedure.
556    ///
557    /// Notes:
558    /// - `RepartitionStart`: no-op, because no metadata has been updated yet.
559    /// - `UpdatePartitionMetadata`: rollback table partition metadata.
560    /// - `AllocateRegion` / `Dispatch` / `Collect`: rollback table partition metadata
561    ///   and allocated region metadata.
562    /// - `DeallocateRegion`: is not rollback-active.
563    /// - `RepartitionEnd`: no-op.
564    fn should_rollback(&self) -> bool {
565        self.state
566            .as_any()
567            .is::<update_partition_metadata::UpdatePartitionMetadata>()
568            || self.state.as_any().is::<allocate_region::AllocateRegion>()
569            || self.state.as_any().is::<dispatch::Dispatch>()
570            || self.state.as_any().is::<collect::Collect>()
571    }
572
573    fn rollback_plan_indices(&self) -> HashSet<usize> {
574        self.context
575            .persistent_ctx
576            .failed_procedures
577            .iter()
578            .chain(self.context.persistent_ctx.unknown_procedures.iter())
579            .map(|procedure_meta| procedure_meta.plan_index)
580            .collect()
581    }
582
583    /// Returns allocated region ids that parent rollback should remove.
584    ///
585    /// Rollback uses an "after region allocation" semantic:
586    /// - in `AllocateRegion` and `Dispatch`, all allocated regions belong to the
587    ///   current repartition attempt and must be cleaned up.
588    /// - in `Collect`, only the plans referenced by failed or unknown
589    ///   sub-procedures should be rolled back.
590    fn rollback_allocated_region_ids(&self) -> HashSet<store_api::storage::RegionId> {
591        if self.state.as_any().is::<allocate_region::AllocateRegion>()
592            || self.state.as_any().is::<dispatch::Dispatch>()
593        {
594            return self
595                .context
596                .persistent_ctx
597                .plans
598                .iter()
599                .flat_map(|plan| plan.allocated_region_ids.iter().copied())
600                .collect();
601        }
602
603        self.rollback_plan_indices()
604            .into_iter()
605            .flat_map(|plan_index| {
606                self.context.persistent_ctx.plans[plan_index]
607                    .allocated_region_ids
608                    .iter()
609                    .copied()
610            })
611            .collect()
612    }
613
614    /// Restores group-level staging metadata for failed/unknown plans.
615    ///
616    /// The helper mutates `region_routes` in memory.
617    async fn rollback_group_metadata_for_selected_plans(
618        &mut self,
619        region_routes: &mut [RegionRoute],
620    ) -> Result<()> {
621        let rollback_plan_indices = self.rollback_plan_indices();
622        if rollback_plan_indices.is_empty() {
623            return Ok(());
624        }
625
626        let mut region_routes_map = region_routes
627            .iter_mut()
628            .map(|route| (route.region.id, route))
629            .collect::<HashMap<_, _>>();
630        for plan_index in rollback_plan_indices {
631            let plan = &self.context.persistent_ctx.plans[plan_index];
632            rollback_group_metadata_routes(
633                plan.group_id,
634                &plan.source_regions,
635                &plan.original_target_routes,
636                &plan.allocated_region_ids,
637                &plan.pending_deallocate_region_ids,
638                &mut region_routes_map,
639            )?;
640        }
641
642        Ok(())
643    }
644
645    async fn rollback_partition_metadata(&mut self) -> Result<()> {
646        let Some(update) = self
647            .context
648            .persistent_ctx
649            .partition_metadata_update
650            .as_ref()
651        else {
652            return Ok(());
653        };
654        let table_info_value = self.context.get_raw_table_info_value().await?;
655        let current_partition_key_indices = &table_info_value.table_info.meta.partition_key_indices;
656        let Some(new_partition_key_indices) = update.rollback_partition_key_indices(
657            self.context.persistent_ctx.table_id,
658            current_partition_key_indices,
659        )?
660        else {
661            return Ok(());
662        };
663
664        let mut new_table_info = table_info_value.table_info.clone();
665        new_table_info.meta.partition_key_indices = new_partition_key_indices;
666        self.context
667            .update_table_info(&table_info_value, table_info_value.update(new_table_info))
668            .await?;
669
670        // Do not invalidate the table cache here. The table routes may still
671        // contain partition expressions until `rollback_inner` rolls them back.
672        // Exposing cleared partition columns with partitioned routes can build
673        // an inconsistent partition rule. The cache is invalidated once after
674        // both partition metadata and routes are rolled back.
675
676        Ok(())
677    }
678
679    async fn rollback_inner(&mut self, procedure_ctx: &ProcedureContext) -> Result<()> {
680        if !self.should_rollback() {
681            return Ok(());
682        }
683
684        let table_id = self.context.persistent_ctx.table_id;
685        let allocated_region_ids = self.rollback_allocated_region_ids();
686
687        let table_lock = TableLock::Write(table_id).into();
688        let _guard = procedure_ctx.provider.acquire_lock(&table_lock).await;
689
690        self.rollback_partition_metadata().await?;
691        let table_route_value = self.context.get_table_route_value().await?;
692        let original_region_routes = region_routes(table_id, table_route_value.get_inner_ref())?;
693        let mut current_region_routes = original_region_routes.clone();
694        self.rollback_group_metadata_for_selected_plans(&mut current_region_routes)
695            .await?;
696        let allocated_region_routes = DeallocateRegion::filter_deallocatable_region_routes(
697            table_id,
698            &current_region_routes,
699            &allocated_region_ids,
700        );
701        if !allocated_region_routes.is_empty() {
702            let table = TableName {
703                catalog_name: self.context.persistent_ctx.catalog_name.clone(),
704                schema_name: self.context.persistent_ctx.schema_name.clone(),
705                table_name: self.context.persistent_ctx.table_name.clone(),
706            };
707            // Memory guards are not required here,
708            // because the table metadata still contains routes for the deallocating regions.
709            if let Err(err) = DeallocateRegion::deallocate_regions(
710                &self.context.node_manager,
711                &self.context.leader_region_registry,
712                table,
713                table_id,
714                &allocated_region_routes,
715            )
716            .await
717            {
718                warn!(err; "Failed to drop allocated regions during repartition rollback, table_id: {}, regions: {:?}", table_id, allocated_region_ids);
719            }
720        }
721
722        let new_region_routes =
723            DeallocateRegion::generate_region_routes(&current_region_routes, &allocated_region_ids);
724
725        if new_region_routes != *original_region_routes {
726            self.context
727                .update_table_route(&table_route_value, new_region_routes, HashMap::new())
728                .await
729                .map_err(BoxedError::new)
730                .with_context(|_| error::RetryLaterWithSourceSnafu {
731                    reason: format!(
732                        "Failed to rollback allocated region routes for repartition table: {}",
733                        table_id
734                    ),
735                })?;
736        }
737
738        if let Err(err) = self.context.invalidate_table_cache().await {
739            warn!(err; "Failed to invalidate table cache during repartition rollback, table_id: {}", table_id);
740        }
741
742        Ok(())
743    }
744}
745
746#[async_trait::async_trait]
747impl Procedure for RepartitionProcedure {
748    fn type_name(&self) -> &str {
749        Self::TYPE_NAME
750    }
751
752    #[tracing::instrument(skip_all, fields(
753        state = %self.state.name(),
754        table_id = %self.context.persistent_ctx.table_id
755    ))]
756    async fn execute(&mut self, _ctx: &ProcedureContext) -> ProcedureResult<Status> {
757        let state = &mut self.state;
758        let state_name = state.name();
759        // Log state transition
760        common_telemetry::info!(
761            "Repartition procedure executing state: {}, table_id: {}",
762            state_name,
763            self.context.persistent_ctx.table_id
764        );
765        match state.next(&mut self.context, _ctx).await {
766            Ok((next, status)) => {
767                *state = next;
768                Ok(status)
769            }
770            Err(e) => {
771                if e.is_retryable() {
772                    Err(ProcedureError::retry_later(e))
773                } else {
774                    error!(
775                        e;
776                        "Repartition procedure failed, table id: {}",
777                        self.context.persistent_ctx.table_id,
778                    );
779                    Err(ProcedureError::external(e))
780                }
781            }
782        }
783    }
784
785    async fn rollback(&mut self, ctx: &ProcedureContext) -> ProcedureResult<()> {
786        self.rollback_inner(ctx)
787            .await
788            .map_err(ProcedureError::external)
789    }
790
791    fn rollback_supported(&self) -> bool {
792        true
793    }
794
795    fn dump(&self) -> ProcedureResult<String> {
796        let data = RepartitionData {
797            state: self.state.as_ref(),
798            persistent_ctx: &self.context.persistent_ctx,
799        };
800        serde_json::to_string(&data).context(ToJsonSnafu)
801    }
802
803    fn lock_key(&self) -> LockKey {
804        LockKey::new(self.context.persistent_ctx.lock_key())
805    }
806
807    fn event(&self, ctx: &EventContext<'_>) -> Option<Box<dyn common_event_recorder::Event>> {
808        if !ctx.event_type_filter.allows(REPARTITION_EVENT_TYPE) {
809            return None;
810        }
811
812        let event = if matches!(ctx.trigger, EventTrigger::Submitted) {
813            let start = self.state.as_any().downcast_ref::<RepartitionStart>()?;
814            RepartitionEvent::submitted(&self.context.persistent_ctx, start)
815        } else {
816            RepartitionEvent::lifecycle(&self.context.persistent_ctx)
817        };
818        Some(Box::new(event))
819    }
820}
821
822pub struct DefaultRepartitionProcedureFactory {
823    mailbox: MailboxRef,
824    server_addr: String,
825    gc_requirement_manager: RepartitionGcRequirementManagerRef,
826}
827
828impl DefaultRepartitionProcedureFactory {
829    pub fn new(
830        mailbox: MailboxRef,
831        server_addr: String,
832        gc_requirement_manager: RepartitionGcRequirementManagerRef,
833    ) -> Self {
834        Self {
835            mailbox,
836            server_addr,
837            gc_requirement_manager,
838        }
839    }
840}
841
842/// Rejects new repartition requests when metasrv GC is disabled.
843///
844/// Procedure loaders are still delegated to the enabled factory so procedures
845/// persisted before a metasrv restart remain recoverable after GC is re-enabled.
846pub struct GcDisabledRepartitionProcedureFactory {
847    enabled_factory: DefaultRepartitionProcedureFactory,
848}
849
850impl GcDisabledRepartitionProcedureFactory {
851    pub fn new(
852        mailbox: MailboxRef,
853        server_addr: String,
854        gc_requirement_manager: RepartitionGcRequirementManagerRef,
855    ) -> Self {
856        Self {
857            enabled_factory: DefaultRepartitionProcedureFactory::new(
858                mailbox,
859                server_addr,
860                gc_requirement_manager,
861            ),
862        }
863    }
864}
865
866#[async_trait::async_trait]
867impl RepartitionProcedureFactory for GcDisabledRepartitionProcedureFactory {
868    fn create(
869        &self,
870        _ddl_ctx: &DdlContext,
871        _table_name: TableName,
872        _table_id: TableId,
873        _source: RepartitionSource,
874        _to_exprs: Vec<String>,
875        _timeout: Option<Duration>,
876    ) -> std::result::Result<BoxedProcedure, BoxedError> {
877        Err(BoxedError::new(
878            error::InvalidArgumentsSnafu {
879                err_msg: "Repartition requires metasrv GC to be enabled".to_string(),
880            }
881            .build(),
882        ))
883    }
884
885    fn register_loaders(
886        &self,
887        ddl_ctx: &DdlContext,
888        procedure_manager: &ProcedureManagerRef,
889    ) -> std::result::Result<(), BoxedError> {
890        self.enabled_factory
891            .register_loaders(ddl_ctx, procedure_manager)
892    }
893
894    async fn ensure_gc_requirement(&self) -> std::result::Result<(), BoxedError> {
895        Err(BoxedError::new(
896            error::InvalidArgumentsSnafu {
897                err_msg: "Repartition requires metasrv GC to be enabled".to_string(),
898            }
899            .build(),
900        ))
901    }
902}
903
904#[async_trait::async_trait]
905impl RepartitionProcedureFactory for DefaultRepartitionProcedureFactory {
906    fn create(
907        &self,
908        ddl_ctx: &DdlContext,
909        table_name: TableName,
910        table_id: TableId,
911        source: RepartitionSource,
912        to_exprs: Vec<String>,
913        timeout: Option<Duration>,
914    ) -> std::result::Result<BoxedProcedure, BoxedError> {
915        let persistent_ctx = PersistentContext::new(table_name, table_id, timeout);
916        let from = match source {
917            RepartitionSource::Partitioned {
918                exprs,
919                target_partition_columns,
920            } => {
921                let exprs = exprs
922                    .iter()
923                    .map(|e| {
924                        PartitionExpr::from_json_str(e)
925                            .context(error::DeserializePartitionExprSnafu)?
926                            .context(error::EmptyPartitionExprSnafu)
927                    })
928                    .collect::<Result<Vec<_>>>()
929                    .map_err(BoxedError::new)?;
930                RepartitionFrom::Partitioned {
931                    exprs,
932                    target_partition_columns,
933                }
934            }
935            RepartitionSource::Unpartitioned { partition_columns } => {
936                RepartitionFrom::Unpartitioned { partition_columns }
937            }
938        };
939        let to_exprs = to_exprs
940            .iter()
941            .map(|e| {
942                PartitionExpr::from_json_str(e)
943                    .context(error::DeserializePartitionExprSnafu)?
944                    .context(error::EmptyPartitionExprSnafu)
945            })
946            .collect::<Result<Vec<_>>>()
947            .map_err(BoxedError::new)?;
948
949        let procedure = RepartitionProcedure::new(
950            from,
951            to_exprs,
952            Context::new(
953                ddl_ctx,
954                self.mailbox.clone(),
955                self.server_addr.clone(),
956                persistent_ctx,
957            ),
958        );
959
960        Ok(Box::new(procedure))
961    }
962
963    fn register_loaders(
964        &self,
965        ddl_ctx: &DdlContext,
966        procedure_manager: &ProcedureManagerRef,
967    ) -> std::result::Result<(), BoxedError> {
968        // Registers the repartition procedure loader.
969        let mailbox = self.mailbox.clone();
970        let server_addr = self.server_addr.clone();
971        let moved_ddl_ctx = ddl_ctx.clone();
972        procedure_manager
973            .register_loader(
974                RepartitionProcedure::TYPE_NAME,
975                Box::new(move |json| {
976                    let mailbox = mailbox.clone();
977                    let server_addr = server_addr.clone();
978                    let ddl_ctx = moved_ddl_ctx.clone();
979                    let factory = move |persistent_ctx| {
980                        Context::new(&ddl_ctx, mailbox, server_addr, persistent_ctx)
981                    };
982                    RepartitionProcedure::from_json(json, factory).map(|p| Box::new(p) as _)
983                }),
984            )
985            .map_err(BoxedError::new)?;
986
987        // Registers the repartition group procedure loader.
988        let mailbox = self.mailbox.clone();
989        let server_addr = self.server_addr.clone();
990        let moved_ddl_ctx = ddl_ctx.clone();
991        procedure_manager
992            .register_loader(
993                RepartitionGroupProcedure::TYPE_NAME,
994                Box::new(move |json| {
995                    let mailbox = mailbox.clone();
996                    let server_addr = server_addr.clone();
997                    let ddl_ctx = moved_ddl_ctx.clone();
998                    let factory = move |persistent_ctx| {
999                        RepartitionGroupContext::new(&ddl_ctx, mailbox, server_addr, persistent_ctx)
1000                    };
1001                    RepartitionGroupProcedure::from_json(json, factory).map(|p| Box::new(p) as _)
1002                }),
1003            )
1004            .map_err(BoxedError::new)?;
1005
1006        Ok(())
1007    }
1008
1009    async fn ensure_gc_requirement(&self) -> std::result::Result<(), BoxedError> {
1010        self.gc_requirement_manager
1011            .require_gc()
1012            .await
1013            .map_err(BoxedError::new)
1014    }
1015}
1016
1017#[cfg(test)]
1018mod tests {
1019    use std::collections::HashMap;
1020    use std::sync::Arc;
1021    use std::sync::atomic::{AtomicBool, Ordering};
1022
1023    use common_error::ext::{BoxedError, ErrorExt};
1024    use common_error::mock::MockError;
1025    use common_error::status_code::StatusCode;
1026    use common_event_recorder::EventTypeFilter;
1027    use common_meta::ddl::test_util::datanode_handler::{
1028        DatanodeWatcher, NaiveDatanodeHandler, UnexpectedErrorDatanodeHandler,
1029    };
1030    use common_meta::error;
1031    use common_meta::peer::Peer;
1032    use common_meta::region_keeper::MemoryRegionKeeper;
1033    use common_meta::rpc::router::{LeaderState, Region, RegionRoute};
1034    use common_meta::state_store::KvStateStore;
1035    use common_meta::test_util::MockDatanodeManager;
1036    use common_procedure::local::{LocalManager, ManagerConfig};
1037    use common_procedure::{Error as ProcedureError, Procedure, ProcedureId, ProcedureState};
1038    use store_api::region_engine::RegionRole;
1039    use store_api::storage::RegionId;
1040    use table::table_name::TableName;
1041    use tokio::sync::mpsc;
1042    use uuid::Uuid;
1043
1044    use super::*;
1045    use crate::procedure::repartition::allocate_region::AllocateRegion;
1046    use crate::procedure::repartition::collect::Collect;
1047    use crate::procedure::repartition::deallocate_region::DeallocateRegion;
1048    use crate::procedure::repartition::dispatch::Dispatch;
1049    use crate::procedure::repartition::gc_requirement::RepartitionGcRequirementManager;
1050    use crate::procedure::repartition::group::update_metadata::UpdateMetadata;
1051    use crate::procedure::repartition::plan::{SourceRegionDescriptor, TargetRegionDescriptor};
1052    use crate::procedure::repartition::repartition_end::RepartitionEnd;
1053    use crate::procedure::repartition::test_util::{
1054        TestingEnv, assert_parent_state, current_parent_region_routes, extract_subprocedure_ids,
1055        new_parent_context, procedure_context_with_receivers, procedure_state_receiver, range_expr,
1056        test_region_route, test_region_wal_options,
1057    };
1058    use crate::procedure::repartition::update_partition_metadata::{
1059        PartitionMetadataUpdate, UpdatePartitionMetadata,
1060    };
1061
1062    fn test_plan(table_id: TableId) -> RepartitionPlanEntry {
1063        RepartitionPlanEntry {
1064            group_id: uuid::Uuid::new_v4(),
1065            source_regions: vec![SourceRegionDescriptor::partitioned(
1066                RegionId::new(table_id, 1),
1067                range_expr("x", 0, 100),
1068            )],
1069            target_regions: vec![
1070                TargetRegionDescriptor {
1071                    region_id: RegionId::new(table_id, 1),
1072                    partition_expr: range_expr("x", 0, 50),
1073                },
1074                TargetRegionDescriptor {
1075                    region_id: RegionId::new(table_id, 3),
1076                    partition_expr: range_expr("x", 50, 100),
1077                },
1078            ],
1079            allocated_region_ids: vec![RegionId::new(table_id, 3)],
1080            pending_deallocate_region_ids: vec![],
1081            transition_map: vec![vec![0, 1]],
1082            original_target_routes: vec![],
1083        }
1084    }
1085
1086    fn with_rollback_metadata(
1087        mut plan: RepartitionPlanEntry,
1088        original_target_routes: Vec<RegionRoute>,
1089    ) -> RepartitionPlanEntry {
1090        plan.original_target_routes = original_target_routes;
1091        plan
1092    }
1093
1094    fn apply_group_staging(
1095        plan: &RepartitionPlanEntry,
1096        current_region_routes: &[RegionRoute],
1097    ) -> Vec<RegionRoute> {
1098        UpdateMetadata::apply_staging_region_routes(
1099            plan.group_id,
1100            &plan.source_regions,
1101            &plan.target_regions,
1102            &plan.pending_deallocate_region_ids,
1103            current_region_routes,
1104        )
1105        .unwrap()
1106    }
1107
1108    fn exit_group_staging(
1109        plan: &RepartitionPlanEntry,
1110        current_region_routes: &[RegionRoute],
1111    ) -> Vec<RegionRoute> {
1112        UpdateMetadata::exit_staging_region_routes(
1113            plan.group_id,
1114            &plan.source_regions,
1115            &plan.target_regions,
1116            current_region_routes,
1117        )
1118        .unwrap()
1119    }
1120
1121    fn region_route_by_id(region_routes: &[RegionRoute], region_id: RegionId) -> &RegionRoute {
1122        region_routes
1123            .iter()
1124            .find(|route| route.region.id == region_id)
1125            .unwrap()
1126    }
1127
1128    async fn table_partition_key_indices(ctx: &Context) -> Vec<usize> {
1129        ctx.get_table_info_value()
1130            .await
1131            .unwrap()
1132            .table_info
1133            .meta
1134            .partition_key_indices
1135    }
1136
1137    fn test_procedure(state: Box<dyn State>, context: Context) -> RepartitionProcedure {
1138        RepartitionProcedure { state, context }
1139    }
1140
1141    fn test_context(env: &TestingEnv, table_id: TableId) -> Context {
1142        let node_manager = Arc::new(MockDatanodeManager::new(UnexpectedErrorDatanodeHandler));
1143        let ddl_ctx = env.ddl_context(node_manager);
1144        let persistent_ctx = PersistentContext::new(
1145            TableName::new("test_catalog", "test_schema", "test_table"),
1146            table_id,
1147            None,
1148        );
1149
1150        Context::new(
1151            &ddl_ctx,
1152            env.mailbox_ctx.mailbox().clone(),
1153            env.server_addr.clone(),
1154            persistent_ctx,
1155        )
1156    }
1157
1158    #[test]
1159    fn test_gc_disabled_factory_rejects_repartition_and_registers_loaders() {
1160        let env = TestingEnv::new();
1161        let node_manager = Arc::new(MockDatanodeManager::new(UnexpectedErrorDatanodeHandler));
1162        let ddl_ctx = env.ddl_context(node_manager);
1163        let factory = GcDisabledRepartitionProcedureFactory::new(
1164            env.mailbox_ctx.mailbox().clone(),
1165            env.server_addr.clone(),
1166            Arc::new(RepartitionGcRequirementManager::new(env.kv_backend.clone())),
1167        );
1168
1169        let err = factory
1170            .create(
1171                &ddl_ctx,
1172                TableName::new("test_catalog", "test_schema", "test_table"),
1173                1024,
1174                RepartitionSource::Unpartitioned {
1175                    partition_columns: vec![],
1176                },
1177                vec![],
1178                None,
1179            )
1180            .err()
1181            .expect("GC-disabled factory must reject repartition");
1182
1183        assert_eq!(StatusCode::InvalidArguments, err.status_code());
1184        assert_eq!(
1185            "Invalid arguments: Repartition requires metasrv GC to be enabled",
1186            err.to_string()
1187        );
1188
1189        let state_store = Arc::new(KvStateStore::new(env.kv_backend));
1190        let procedure_manager = Arc::new(LocalManager::new(
1191            ManagerConfig::default(),
1192            state_store.clone(),
1193            state_store,
1194            None,
1195            None,
1196        ));
1197        let procedure_manager_ref: ProcedureManagerRef = procedure_manager.clone();
1198        factory
1199            .register_loaders(&ddl_ctx, &procedure_manager_ref)
1200            .unwrap();
1201        assert!(procedure_manager.contains_loader(RepartitionProcedure::TYPE_NAME));
1202        assert!(procedure_manager.contains_loader(RepartitionGroupProcedure::TYPE_NAME));
1203    }
1204
1205    #[test]
1206    fn test_filter_allocated_region_routes() {
1207        let table_id = 1024;
1208        let region_routes = vec![
1209            test_region_route(RegionId::new(table_id, 1), "a"),
1210            test_region_route(RegionId::new(table_id, 2), "b"),
1211        ];
1212        let allocated_region_ids = HashSet::from([RegionId::new(table_id, 2)]);
1213
1214        let new_region_routes =
1215            DeallocateRegion::generate_region_routes(&region_routes, &allocated_region_ids);
1216
1217        assert_eq!(new_region_routes.len(), 1);
1218        assert_eq!(new_region_routes[0].region.id, RegionId::new(table_id, 1));
1219    }
1220
1221    #[test]
1222    fn test_should_rollback_after_metadata_update() {
1223        let env = TestingEnv::new();
1224        let table_id = 1024;
1225
1226        let procedure = test_procedure(
1227            Box::new(RepartitionStart::new(
1228                RepartitionFrom::Partitioned {
1229                    exprs: vec![],
1230                    target_partition_columns: None,
1231                },
1232                vec![],
1233            )),
1234            test_context(&env, table_id),
1235        );
1236        assert!(!procedure.should_rollback());
1237
1238        let procedure = test_procedure(
1239            Box::new(UpdatePartitionMetadata::new(vec![])),
1240            test_context(&env, table_id),
1241        );
1242        assert!(procedure.should_rollback());
1243
1244        let procedure = test_procedure(
1245            Box::new(AllocateRegion::new(vec![])),
1246            test_context(&env, table_id),
1247        );
1248        assert!(procedure.should_rollback());
1249
1250        let procedure = test_procedure(Box::new(Dispatch), test_context(&env, table_id));
1251        assert!(procedure.should_rollback());
1252
1253        let procedure =
1254            test_procedure(Box::new(Collect::new(vec![])), test_context(&env, table_id));
1255        assert!(procedure.should_rollback());
1256
1257        let procedure = test_procedure(Box::new(DeallocateRegion), test_context(&env, table_id));
1258        assert!(!procedure.should_rollback());
1259
1260        let procedure = test_procedure(Box::new(RepartitionEnd), test_context(&env, table_id));
1261        assert!(!procedure.should_rollback());
1262    }
1263
1264    #[test]
1265    fn test_event_hook_records_submitted_and_lightweight_lifecycle_events() {
1266        let env = TestingEnv::new();
1267        let procedure = test_procedure(
1268            Box::new(RepartitionStart::new(
1269                RepartitionFrom::Unpartitioned {
1270                    partition_columns: vec!["x".to_string()],
1271                },
1272                vec![range_expr("x", 0, 100)],
1273            )),
1274            test_context(&env, 1024),
1275        );
1276        let state = ProcedureState::Running;
1277        let all = Arc::new(EventTypeFilter::All);
1278
1279        let submitted = procedure
1280            .event(&EventContext {
1281                procedure_id: ProcedureId::random(),
1282                lifecycle_state: &state,
1283                trigger: EventTrigger::Submitted,
1284                event_type_filter: all.clone(),
1285                event_context: None,
1286            })
1287            .unwrap();
1288        assert_eq!(submitted.event_type(), REPARTITION_EVENT_TYPE);
1289        assert_ne!(submitted.json_payload().unwrap(), serde_json::Value::Null);
1290
1291        let allowed = procedure
1292            .event(&EventContext {
1293                procedure_id: ProcedureId::random(),
1294                lifecycle_state: &state,
1295                trigger: EventTrigger::Submitted,
1296                event_type_filter: Arc::new(EventTypeFilter::Only(HashSet::from([
1297                    REPARTITION_EVENT_TYPE.to_string(),
1298                ]))),
1299                event_context: None,
1300            })
1301            .unwrap();
1302        assert_eq!(allowed.event_type(), REPARTITION_EVENT_TYPE);
1303
1304        let succeeded = procedure
1305            .event(&EventContext {
1306                procedure_id: ProcedureId::random(),
1307                lifecycle_state: &state,
1308                trigger: EventTrigger::Succeeded,
1309                event_type_filter: all,
1310                event_context: None,
1311            })
1312            .unwrap();
1313        assert_eq!(succeeded.json_payload().unwrap(), serde_json::Value::Null);
1314
1315        let filtered = procedure.event(&EventContext {
1316            procedure_id: ProcedureId::random(),
1317            lifecycle_state: &state,
1318            trigger: EventTrigger::Submitted,
1319            event_type_filter: Arc::new(EventTypeFilter::Only(HashSet::from([
1320                "another_event".to_string()
1321            ]))),
1322            event_context: None,
1323        });
1324        assert!(filtered.is_none());
1325
1326        let empty = procedure.event(&EventContext {
1327            procedure_id: ProcedureId::random(),
1328            lifecycle_state: &state,
1329            trigger: EventTrigger::Submitted,
1330            event_type_filter: Arc::new(EventTypeFilter::Only(HashSet::new())),
1331            event_context: None,
1332        });
1333        assert!(empty.is_none());
1334    }
1335
1336    #[test]
1337    fn test_register_operating_regions_preserves_route_roles() {
1338        let keeper = Arc::new(MemoryRegionKeeper::new());
1339        let region_routes = vec![
1340            RegionRoute {
1341                region: Region::new_test(RegionId::new(1024, 1)),
1342                leader_peer: Some(Peer::empty(1)),
1343                follower_peers: vec![],
1344                leader_state: None,
1345                leader_down_since: None,
1346                write_route_policy: None,
1347            },
1348            RegionRoute {
1349                region: Region::new_test(RegionId::new(1024, 2)),
1350                leader_peer: Some(Peer::empty(2)),
1351                follower_peers: vec![],
1352                leader_state: Some(LeaderState::Staging),
1353                leader_down_since: None,
1354                write_route_policy: None,
1355            },
1356            RegionRoute {
1357                region: Region::new_test(RegionId::new(1024, 3)),
1358                leader_peer: Some(Peer::empty(3)),
1359                follower_peers: vec![],
1360                leader_state: Some(LeaderState::Downgrading),
1361                leader_down_since: None,
1362                write_route_policy: None,
1363            },
1364        ];
1365
1366        let _guards = Context::register_operating_regions(&keeper, &region_routes).unwrap();
1367
1368        let leader_roles =
1369            keeper.extract_operating_region_roles(1, &HashSet::from([RegionId::new(1024, 1)]));
1370        let staging_roles =
1371            keeper.extract_operating_region_roles(2, &HashSet::from([RegionId::new(1024, 2)]));
1372        let downgrading_roles =
1373            keeper.extract_operating_region_roles(3, &HashSet::from([RegionId::new(1024, 3)]));
1374
1375        assert_eq!(
1376            leader_roles.get(&RegionId::new(1024, 1)),
1377            Some(&RegionRole::Leader)
1378        );
1379        assert_eq!(
1380            staging_roles.get(&RegionId::new(1024, 2)),
1381            Some(&RegionRole::StagingLeader)
1382        );
1383        assert_eq!(
1384            downgrading_roles.get(&RegionId::new(1024, 3)),
1385            Some(&RegionRole::DowngradingLeader)
1386        );
1387    }
1388
1389    #[test]
1390    fn test_persistent_context_partition_metadata_update_serde_default() {
1391        let json = r#"{
1392            "catalog_name":"test_catalog",
1393            "schema_name":"test_schema",
1394            "table_name":"test_table",
1395            "table_id":1024,
1396            "plans":[],
1397            "timeout":"120s"
1398        }"#;
1399
1400        let persistent_ctx: PersistentContext = serde_json::from_str(json).unwrap();
1401
1402        assert!(persistent_ctx.partition_metadata_update.is_none());
1403    }
1404
1405    #[tokio::test]
1406    async fn test_repartition_rollback_removes_partition_metadata_indices() {
1407        let env = TestingEnv::new();
1408        let table_id = 1024;
1409        let node_manager = Arc::new(MockDatanodeManager::new(UnexpectedErrorDatanodeHandler));
1410        env.create_physical_table_metadata_for_repartition(
1411            table_id,
1412            vec![test_region_route(RegionId::new(table_id, 1), "")],
1413            test_region_wal_options(&[1]),
1414        )
1415        .await;
1416
1417        let mut context = new_parent_context(&env, node_manager, table_id);
1418        let current = context.get_raw_table_info_value().await.unwrap();
1419        let mut table_info = current.table_info.clone();
1420        table_info.meta.partition_key_indices = vec![0, 1];
1421        context
1422            .update_table_info(&current, current.update(table_info))
1423            .await
1424            .unwrap();
1425        context.persistent_ctx.partition_metadata_update = Some(
1426            PartitionMetadataUpdate::from_partitioned(vec![1], vec![0, 1]),
1427        );
1428        let mut procedure = RepartitionProcedure {
1429            state: Box::new(UpdatePartitionMetadata::new(vec![])),
1430            context,
1431        };
1432
1433        procedure
1434            .rollback(&TestingEnv::procedure_context())
1435            .await
1436            .unwrap();
1437
1438        assert_eq!(
1439            procedure
1440                .context
1441                .get_table_info_value()
1442                .await
1443                .unwrap()
1444                .table_info
1445                .meta
1446                .partition_key_indices,
1447            vec![1]
1448        );
1449    }
1450
1451    #[tokio::test]
1452    async fn test_repartition_rollback_removes_allocated_routes_from_dispatch() {
1453        let env = TestingEnv::new();
1454        let table_id = 1024;
1455        let node_manager = Arc::new(MockDatanodeManager::new(UnexpectedErrorDatanodeHandler));
1456        let ddl_ctx = env.ddl_context(node_manager);
1457        let original_region_routes = vec![
1458            test_region_route(
1459                RegionId::new(table_id, 1),
1460                &range_expr("x", 0, 100).as_json_str().unwrap(),
1461            ),
1462            test_region_route(
1463                RegionId::new(table_id, 2),
1464                &range_expr("x", 50, 100).as_json_str().unwrap(),
1465            ),
1466            test_region_route(RegionId::new(table_id, 3), ""),
1467        ];
1468        env.create_physical_table_metadata_with_wal_options(
1469            table_id,
1470            original_region_routes,
1471            test_region_wal_options(&[1, 2]),
1472        )
1473        .await;
1474
1475        let mut persistent_ctx = PersistentContext::new(
1476            TableName::new("test_catalog", "test_schema", "test_table"),
1477            table_id,
1478            None,
1479        );
1480        persistent_ctx.plans = vec![with_rollback_metadata(
1481            test_plan(table_id),
1482            vec![
1483                test_region_route(
1484                    RegionId::new(table_id, 1),
1485                    &range_expr("x", 0, 100).as_json_str().unwrap(),
1486                ),
1487                test_region_route(RegionId::new(table_id, 3), ""),
1488            ],
1489        )];
1490        persistent_ctx.failed_procedures = vec![ProcedureMeta {
1491            plan_index: 0,
1492            group_id: Uuid::new_v4(),
1493            procedure_id: ProcedureId::random(),
1494        }];
1495        let context = Context::new(
1496            &ddl_ctx,
1497            env.mailbox_ctx.mailbox().clone(),
1498            env.server_addr.clone(),
1499            persistent_ctx,
1500        );
1501        let mut procedure = RepartitionProcedure {
1502            state: Box::new(Dispatch),
1503            context,
1504        };
1505
1506        procedure
1507            .rollback(&TestingEnv::procedure_context())
1508            .await
1509            .unwrap();
1510
1511        let region_routes = current_parent_region_routes(&procedure.context).await;
1512        assert_eq!(region_routes.len(), 2);
1513        assert_eq!(region_routes[0].region.id, RegionId::new(table_id, 1));
1514        assert_eq!(region_routes[1].region.id, RegionId::new(table_id, 2));
1515    }
1516
1517    #[tokio::test]
1518    async fn test_repartition_rollback_removes_allocated_routes_from_allocate() {
1519        let env = TestingEnv::new();
1520        let table_id = 1024;
1521        let node_manager = Arc::new(MockDatanodeManager::new(UnexpectedErrorDatanodeHandler));
1522        let ddl_ctx = env.ddl_context(node_manager);
1523        let original_region_routes = vec![
1524            test_region_route(
1525                RegionId::new(table_id, 1),
1526                &range_expr("x", 0, 100).as_json_str().unwrap(),
1527            ),
1528            test_region_route(
1529                RegionId::new(table_id, 2),
1530                &range_expr("x", 50, 100).as_json_str().unwrap(),
1531            ),
1532            test_region_route(RegionId::new(table_id, 3), ""),
1533        ];
1534        env.create_physical_table_metadata_with_wal_options(
1535            table_id,
1536            original_region_routes,
1537            test_region_wal_options(&[1, 2]),
1538        )
1539        .await;
1540
1541        let mut persistent_ctx = PersistentContext::new(
1542            TableName::new("test_catalog", "test_schema", "test_table"),
1543            table_id,
1544            None,
1545        );
1546        persistent_ctx.plans = vec![test_plan(table_id)];
1547        let context = Context::new(
1548            &ddl_ctx,
1549            env.mailbox_ctx.mailbox().clone(),
1550            env.server_addr.clone(),
1551            persistent_ctx,
1552        );
1553        let mut procedure = RepartitionProcedure {
1554            state: Box::new(AllocateRegion::new(vec![])),
1555            context,
1556        };
1557
1558        procedure
1559            .rollback(&TestingEnv::procedure_context())
1560            .await
1561            .unwrap();
1562
1563        let region_routes = current_parent_region_routes(&procedure.context).await;
1564        assert_eq!(region_routes.len(), 2);
1565        assert_eq!(region_routes[0].region.id, RegionId::new(table_id, 1));
1566        assert_eq!(region_routes[1].region.id, RegionId::new(table_id, 2));
1567    }
1568
1569    #[tokio::test]
1570    async fn test_repartition_rollback_from_collect_only_removes_failed_allocated_routes() {
1571        let env = TestingEnv::new();
1572        let table_id = 1024;
1573        let node_manager = Arc::new(MockDatanodeManager::new(UnexpectedErrorDatanodeHandler));
1574        let ddl_ctx = env.ddl_context(node_manager);
1575        let original_region_routes = vec![
1576            test_region_route(
1577                RegionId::new(table_id, 1),
1578                &range_expr("x", 0, 100).as_json_str().unwrap(),
1579            ),
1580            test_region_route(
1581                RegionId::new(table_id, 2),
1582                &range_expr("x", 100, 200).as_json_str().unwrap(),
1583            ),
1584            test_region_route(RegionId::new(table_id, 3), ""),
1585            test_region_route(RegionId::new(table_id, 4), ""),
1586        ];
1587        env.create_physical_table_metadata_with_wal_options(
1588            table_id,
1589            original_region_routes,
1590            test_region_wal_options(&[1, 2, 3, 4]),
1591        )
1592        .await;
1593
1594        let mut persistent_ctx = PersistentContext::new(
1595            TableName::new("test_catalog", "test_schema", "test_table"),
1596            table_id,
1597            None,
1598        );
1599        let failed_plan = test_plan(table_id);
1600        let failed_plan = with_rollback_metadata(
1601            failed_plan,
1602            vec![
1603                test_region_route(
1604                    RegionId::new(table_id, 1),
1605                    &range_expr("x", 0, 100).as_json_str().unwrap(),
1606                ),
1607                test_region_route(RegionId::new(table_id, 3), ""),
1608            ],
1609        );
1610        let succeeded_plan = RepartitionPlanEntry {
1611            group_id: Uuid::new_v4(),
1612            source_regions: vec![SourceRegionDescriptor::partitioned(
1613                RegionId::new(table_id, 2),
1614                range_expr("x", 100, 200),
1615            )],
1616            target_regions: vec![
1617                TargetRegionDescriptor {
1618                    region_id: RegionId::new(table_id, 2),
1619                    partition_expr: range_expr("x", 100, 150),
1620                },
1621                TargetRegionDescriptor {
1622                    region_id: RegionId::new(table_id, 4),
1623                    partition_expr: range_expr("x", 150, 200),
1624                },
1625            ],
1626            allocated_region_ids: vec![RegionId::new(table_id, 4)],
1627            pending_deallocate_region_ids: vec![],
1628            transition_map: vec![vec![0]],
1629            original_target_routes: vec![
1630                test_region_route(
1631                    RegionId::new(table_id, 2),
1632                    &range_expr("x", 100, 200).as_json_str().unwrap(),
1633                ),
1634                test_region_route(RegionId::new(table_id, 4), ""),
1635            ],
1636        };
1637        persistent_ctx.plans = vec![failed_plan, succeeded_plan];
1638        persistent_ctx.failed_procedures = vec![ProcedureMeta {
1639            plan_index: 0,
1640            group_id: persistent_ctx.plans[0].group_id,
1641            procedure_id: ProcedureId::random(),
1642        }];
1643
1644        let context = Context::new(
1645            &ddl_ctx,
1646            env.mailbox_ctx.mailbox().clone(),
1647            env.server_addr.clone(),
1648            persistent_ctx,
1649        );
1650        let mut procedure = RepartitionProcedure {
1651            state: Box::new(Collect::new(vec![])),
1652            context,
1653        };
1654
1655        procedure
1656            .rollback(&TestingEnv::procedure_context())
1657            .await
1658            .unwrap();
1659
1660        let region_routes = current_parent_region_routes(&procedure.context).await;
1661        assert_eq!(region_routes.len(), 3);
1662        assert_eq!(region_routes[0].region.id, RegionId::new(table_id, 1));
1663        assert_eq!(region_routes[1].region.id, RegionId::new(table_id, 2));
1664        assert_eq!(region_routes[2].region.id, RegionId::new(table_id, 4));
1665    }
1666
1667    #[tokio::test]
1668    async fn test_repartition_rollback_from_collect_restores_failed_group_metadata_only() {
1669        let env = TestingEnv::new();
1670        let table_id = 1024;
1671        let node_manager = Arc::new(MockDatanodeManager::new(UnexpectedErrorDatanodeHandler));
1672        let ddl_ctx = env.ddl_context(node_manager);
1673        let original_region_routes = vec![
1674            test_region_route(
1675                RegionId::new(table_id, 1),
1676                &range_expr("x", 0, 100).as_json_str().unwrap(),
1677            ),
1678            test_region_route(
1679                RegionId::new(table_id, 2),
1680                &range_expr("x", 100, 200).as_json_str().unwrap(),
1681            ),
1682            test_region_route(RegionId::new(table_id, 3), ""),
1683            test_region_route(RegionId::new(table_id, 4), ""),
1684        ];
1685
1686        let failed_plan = with_rollback_metadata(
1687            test_plan(table_id),
1688            vec![
1689                original_region_routes[0].clone(),
1690                original_region_routes[2].clone(),
1691            ],
1692        );
1693        let succeeded_plan = RepartitionPlanEntry {
1694            group_id: Uuid::new_v4(),
1695            source_regions: vec![SourceRegionDescriptor::partitioned(
1696                RegionId::new(table_id, 2),
1697                range_expr("x", 100, 200),
1698            )],
1699            target_regions: vec![
1700                TargetRegionDescriptor {
1701                    region_id: RegionId::new(table_id, 2),
1702                    partition_expr: range_expr("x", 100, 150),
1703                },
1704                TargetRegionDescriptor {
1705                    region_id: RegionId::new(table_id, 4),
1706                    partition_expr: range_expr("x", 150, 200),
1707                },
1708            ],
1709            allocated_region_ids: vec![RegionId::new(table_id, 4)],
1710            pending_deallocate_region_ids: vec![],
1711            transition_map: vec![vec![0, 1]],
1712            original_target_routes: vec![
1713                original_region_routes[1].clone(),
1714                original_region_routes[3].clone(),
1715            ],
1716        };
1717        let current_region_routes = apply_group_staging(&failed_plan, &original_region_routes);
1718        let current_region_routes = apply_group_staging(&succeeded_plan, &current_region_routes);
1719        let current_region_routes = exit_group_staging(&succeeded_plan, &current_region_routes);
1720        env.create_physical_table_metadata_with_wal_options(
1721            table_id,
1722            current_region_routes,
1723            test_region_wal_options(&[1, 2, 3, 4]),
1724        )
1725        .await;
1726
1727        let mut persistent_ctx = PersistentContext::new(
1728            TableName::new("test_catalog", "test_schema", "test_table"),
1729            table_id,
1730            None,
1731        );
1732        persistent_ctx.plans = vec![failed_plan, succeeded_plan.clone()];
1733        persistent_ctx.failed_procedures = vec![ProcedureMeta {
1734            plan_index: 0,
1735            group_id: persistent_ctx.plans[0].group_id,
1736            procedure_id: ProcedureId::random(),
1737        }];
1738
1739        let context = Context::new(
1740            &ddl_ctx,
1741            env.mailbox_ctx.mailbox().clone(),
1742            env.server_addr.clone(),
1743            persistent_ctx,
1744        );
1745        let mut procedure = RepartitionProcedure {
1746            state: Box::new(Collect::new(vec![])),
1747            context,
1748        };
1749
1750        procedure
1751            .rollback(&TestingEnv::procedure_context())
1752            .await
1753            .unwrap();
1754
1755        assert_eq!(
1756            current_parent_region_routes(&procedure.context).await,
1757            vec![
1758                test_region_route(
1759                    RegionId::new(table_id, 1),
1760                    &range_expr("x", 0, 100).as_json_str().unwrap(),
1761                ),
1762                RegionRoute {
1763                    region: Region {
1764                        id: RegionId::new(table_id, 2),
1765                        partition_expr: range_expr("x", 100, 150).as_json_str().unwrap(),
1766                        ..Default::default()
1767                    },
1768                    leader_peer: Some(Peer::empty(1)),
1769                    ..Default::default()
1770                },
1771                RegionRoute {
1772                    region: Region {
1773                        id: RegionId::new(table_id, 4),
1774                        partition_expr: range_expr("x", 150, 200).as_json_str().unwrap(),
1775                        ..Default::default()
1776                    },
1777                    leader_peer: Some(Peer::empty(1)),
1778                    ..Default::default()
1779                },
1780            ]
1781        );
1782    }
1783
1784    #[tokio::test]
1785    async fn test_repartition_rollback_from_collect_restores_unknown_group_metadata() {
1786        let env = TestingEnv::new();
1787        let table_id = 1024;
1788        let node_manager = Arc::new(MockDatanodeManager::new(UnexpectedErrorDatanodeHandler));
1789        let ddl_ctx = env.ddl_context(node_manager);
1790        let original_region_routes = vec![
1791            test_region_route(
1792                RegionId::new(table_id, 1),
1793                &range_expr("x", 0, 100).as_json_str().unwrap(),
1794            ),
1795            test_region_route(
1796                RegionId::new(table_id, 2),
1797                &range_expr("x", 100, 200).as_json_str().unwrap(),
1798            ),
1799            test_region_route(RegionId::new(table_id, 3), ""),
1800        ];
1801        let plan = with_rollback_metadata(
1802            test_plan(table_id),
1803            vec![
1804                original_region_routes[0].clone(),
1805                original_region_routes[2].clone(),
1806            ],
1807        );
1808        let staged_region_routes = apply_group_staging(&plan, &original_region_routes);
1809        assert_eq!(
1810            region_route_by_id(&staged_region_routes, RegionId::new(table_id, 1))
1811                .region
1812                .partition_expr(),
1813            range_expr("x", 0, 50).as_json_str().unwrap()
1814        );
1815        assert!(
1816            region_route_by_id(&staged_region_routes, RegionId::new(table_id, 1))
1817                .is_leader_staging()
1818        );
1819        assert_eq!(
1820            region_route_by_id(&staged_region_routes, RegionId::new(table_id, 3))
1821                .region
1822                .partition_expr(),
1823            range_expr("x", 50, 100).as_json_str().unwrap()
1824        );
1825        assert!(
1826            region_route_by_id(&staged_region_routes, RegionId::new(table_id, 3))
1827                .is_leader_staging()
1828        );
1829        env.create_physical_table_metadata_with_wal_options(
1830            table_id,
1831            staged_region_routes,
1832            test_region_wal_options(&[1, 2, 3]),
1833        )
1834        .await;
1835
1836        let mut persistent_ctx = PersistentContext::new(
1837            TableName::new("test_catalog", "test_schema", "test_table"),
1838            table_id,
1839            None,
1840        );
1841        persistent_ctx.plans = vec![plan.clone()];
1842        persistent_ctx.unknown_procedures = vec![ProcedureMeta {
1843            plan_index: 0,
1844            group_id: plan.group_id,
1845            procedure_id: ProcedureId::random(),
1846        }];
1847
1848        let context = Context::new(
1849            &ddl_ctx,
1850            env.mailbox_ctx.mailbox().clone(),
1851            env.server_addr.clone(),
1852            persistent_ctx,
1853        );
1854        let mut procedure = RepartitionProcedure {
1855            state: Box::new(Collect::new(vec![])),
1856            context,
1857        };
1858
1859        procedure
1860            .rollback(&TestingEnv::procedure_context())
1861            .await
1862            .unwrap();
1863
1864        assert_eq!(
1865            current_parent_region_routes(&procedure.context).await,
1866            vec![
1867                original_region_routes[0].clone(),
1868                original_region_routes[1].clone()
1869            ]
1870        );
1871    }
1872
1873    #[tokio::test]
1874    async fn test_repartition_rollback_is_idempotent() {
1875        let env = TestingEnv::new();
1876        let table_id = 1024;
1877        let node_manager = Arc::new(MockDatanodeManager::new(UnexpectedErrorDatanodeHandler));
1878        let ddl_ctx = env.ddl_context(node_manager);
1879        let original_region_routes = vec![
1880            test_region_route(
1881                RegionId::new(table_id, 1),
1882                &range_expr("x", 0, 100).as_json_str().unwrap(),
1883            ),
1884            test_region_route(
1885                RegionId::new(table_id, 2),
1886                &range_expr("x", 50, 100).as_json_str().unwrap(),
1887            ),
1888            test_region_route(RegionId::new(table_id, 3), ""),
1889        ];
1890        env.create_physical_table_metadata_with_wal_options(
1891            table_id,
1892            original_region_routes,
1893            test_region_wal_options(&[1, 2]),
1894        )
1895        .await;
1896
1897        let mut persistent_ctx = PersistentContext::new(
1898            TableName::new("test_catalog", "test_schema", "test_table"),
1899            table_id,
1900            None,
1901        );
1902        persistent_ctx.plans = vec![with_rollback_metadata(
1903            test_plan(table_id),
1904            vec![
1905                test_region_route(
1906                    RegionId::new(table_id, 1),
1907                    &range_expr("x", 0, 100).as_json_str().unwrap(),
1908                ),
1909                test_region_route(RegionId::new(table_id, 3), ""),
1910            ],
1911        )];
1912        persistent_ctx.failed_procedures = vec![ProcedureMeta {
1913            plan_index: 0,
1914            group_id: Uuid::new_v4(),
1915            procedure_id: ProcedureId::random(),
1916        }];
1917        let context = Context::new(
1918            &ddl_ctx,
1919            env.mailbox_ctx.mailbox().clone(),
1920            env.server_addr.clone(),
1921            persistent_ctx,
1922        );
1923        let mut procedure = RepartitionProcedure {
1924            state: Box::new(Dispatch),
1925            context,
1926        };
1927
1928        procedure
1929            .rollback(&TestingEnv::procedure_context())
1930            .await
1931            .unwrap();
1932        let once = current_parent_region_routes(&procedure.context).await;
1933
1934        procedure
1935            .rollback(&TestingEnv::procedure_context())
1936            .await
1937            .unwrap();
1938        let twice = current_parent_region_routes(&procedure.context).await;
1939
1940        assert_eq!(once, twice);
1941        assert_eq!(once.len(), 2);
1942        assert_eq!(once[0].region.id, RegionId::new(table_id, 1));
1943        assert_eq!(once[1].region.id, RegionId::new(table_id, 2));
1944    }
1945
1946    #[tokio::test]
1947    async fn test_repartition_rollback_from_collect_restores_failed_merge_group_metadata_only() {
1948        let env = TestingEnv::new();
1949        let table_id = 1024;
1950        let node_manager = Arc::new(MockDatanodeManager::new(UnexpectedErrorDatanodeHandler));
1951        let ddl_ctx = env.ddl_context(node_manager);
1952        let original_region_routes = vec![
1953            test_region_route(
1954                RegionId::new(table_id, 1),
1955                &range_expr("x", 0, 100).as_json_str().unwrap(),
1956            ),
1957            test_region_route(
1958                RegionId::new(table_id, 2),
1959                &range_expr("x", 100, 200).as_json_str().unwrap(),
1960            ),
1961            test_region_route(
1962                RegionId::new(table_id, 3),
1963                &range_expr("x", 200, 300).as_json_str().unwrap(),
1964            ),
1965            test_region_route(RegionId::new(table_id, 4), ""),
1966        ];
1967        let failed_merge_plan = RepartitionPlanEntry {
1968            group_id: Uuid::new_v4(),
1969            source_regions: vec![
1970                SourceRegionDescriptor::partitioned(
1971                    RegionId::new(table_id, 1),
1972                    range_expr("x", 0, 100),
1973                ),
1974                SourceRegionDescriptor::partitioned(
1975                    RegionId::new(table_id, 2),
1976                    range_expr("x", 100, 200),
1977                ),
1978            ],
1979            target_regions: vec![TargetRegionDescriptor {
1980                region_id: RegionId::new(table_id, 1),
1981                partition_expr: range_expr("x", 0, 200),
1982            }],
1983            allocated_region_ids: vec![],
1984            pending_deallocate_region_ids: vec![RegionId::new(table_id, 2)],
1985            transition_map: vec![vec![0], vec![0]],
1986            original_target_routes: vec![original_region_routes[0].clone()],
1987        };
1988        let succeeded_split_plan = RepartitionPlanEntry {
1989            group_id: Uuid::new_v4(),
1990            source_regions: vec![SourceRegionDescriptor::partitioned(
1991                RegionId::new(table_id, 3),
1992                range_expr("x", 200, 300),
1993            )],
1994            target_regions: vec![
1995                TargetRegionDescriptor {
1996                    region_id: RegionId::new(table_id, 3),
1997                    partition_expr: range_expr("x", 200, 250),
1998                },
1999                TargetRegionDescriptor {
2000                    region_id: RegionId::new(table_id, 4),
2001                    partition_expr: range_expr("x", 250, 300),
2002                },
2003            ],
2004            allocated_region_ids: vec![RegionId::new(table_id, 4)],
2005            pending_deallocate_region_ids: vec![],
2006            transition_map: vec![vec![0, 1]],
2007            original_target_routes: vec![
2008                original_region_routes[2].clone(),
2009                original_region_routes[3].clone(),
2010            ],
2011        };
2012        let current_region_routes =
2013            apply_group_staging(&failed_merge_plan, &original_region_routes);
2014        let current_region_routes =
2015            apply_group_staging(&succeeded_split_plan, &current_region_routes);
2016        let staged_region_routes =
2017            exit_group_staging(&succeeded_split_plan, &current_region_routes);
2018        env.create_physical_table_metadata_with_wal_options(
2019            table_id,
2020            staged_region_routes,
2021            test_region_wal_options(&[1, 2, 3, 4]),
2022        )
2023        .await;
2024
2025        let mut persistent_ctx = PersistentContext::new(
2026            TableName::new("test_catalog", "test_schema", "test_table"),
2027            table_id,
2028            None,
2029        );
2030        persistent_ctx.plans = vec![failed_merge_plan, succeeded_split_plan.clone()];
2031        persistent_ctx.failed_procedures = vec![ProcedureMeta {
2032            plan_index: 0,
2033            group_id: persistent_ctx.plans[0].group_id,
2034            procedure_id: ProcedureId::random(),
2035        }];
2036
2037        let context = Context::new(
2038            &ddl_ctx,
2039            env.mailbox_ctx.mailbox().clone(),
2040            env.server_addr.clone(),
2041            persistent_ctx,
2042        );
2043        let mut procedure = RepartitionProcedure {
2044            state: Box::new(Collect::new(vec![])),
2045            context,
2046        };
2047
2048        procedure
2049            .rollback(&TestingEnv::procedure_context())
2050            .await
2051            .unwrap();
2052
2053        let region_routes = current_parent_region_routes(&procedure.context).await;
2054        assert_eq!(
2055            region_routes,
2056            vec![
2057                test_region_route(
2058                    RegionId::new(table_id, 1),
2059                    &range_expr("x", 0, 100).as_json_str().unwrap(),
2060                ),
2061                test_region_route(
2062                    RegionId::new(table_id, 2),
2063                    &range_expr("x", 100, 200).as_json_str().unwrap(),
2064                ),
2065                RegionRoute {
2066                    region: Region {
2067                        id: RegionId::new(table_id, 3),
2068                        partition_expr: range_expr("x", 200, 250).as_json_str().unwrap(),
2069                        ..Default::default()
2070                    },
2071                    leader_peer: Some(Peer::empty(1)),
2072                    ..Default::default()
2073                },
2074                RegionRoute {
2075                    region: Region {
2076                        id: RegionId::new(table_id, 4),
2077                        partition_expr: range_expr("x", 250, 300).as_json_str().unwrap(),
2078                        ..Default::default()
2079                    },
2080                    leader_peer: Some(Peer::empty(1)),
2081                    ..Default::default()
2082                },
2083            ]
2084        );
2085    }
2086
2087    #[tokio::test]
2088    async fn test_repartition_procedure_flow_split_failed_and_full_rollback() {
2089        let env = TestingEnv::new();
2090        let table_id = 1024;
2091        let node_manager = Arc::new(MockDatanodeManager::new(NaiveDatanodeHandler));
2092
2093        env.create_physical_table_metadata_for_repartition(
2094            table_id,
2095            vec![
2096                test_region_route(
2097                    RegionId::new(table_id, 1),
2098                    &range_expr("x", 0, 100).as_json_str().unwrap(),
2099                ),
2100                test_region_route(
2101                    RegionId::new(table_id, 2),
2102                    &range_expr("x", 100, 200).as_json_str().unwrap(),
2103                ),
2104            ],
2105            test_region_wal_options(&[1, 2]),
2106        )
2107        .await;
2108
2109        let context = new_parent_context(&env, node_manager, table_id);
2110        let mut procedure = RepartitionProcedure::new(
2111            RepartitionFrom::Partitioned {
2112                exprs: vec![range_expr("x", 0, 100)],
2113                target_partition_columns: None,
2114            },
2115            vec![range_expr("x", 0, 50), range_expr("x", 50, 100)],
2116            context,
2117        );
2118
2119        let start_status = procedure
2120            .execute(&TestingEnv::procedure_context())
2121            .await
2122            .unwrap();
2123        assert!(!start_status.need_persist());
2124        let start_status = procedure
2125            .execute(&TestingEnv::procedure_context())
2126            .await
2127            .unwrap();
2128        assert!(start_status.need_persist());
2129        assert_parent_state::<AllocateRegion>(&procedure);
2130
2131        let allocate_status = procedure
2132            .execute(&TestingEnv::procedure_context())
2133            .await
2134            .unwrap();
2135        assert!(allocate_status.need_persist());
2136        assert_parent_state::<Dispatch>(&procedure);
2137        assert_eq!(procedure.context.persistent_ctx.plans.len(), 1);
2138        let plan = &procedure.context.persistent_ctx.plans[0];
2139        let expected_plan = test_plan(table_id);
2140        assert_eq!(plan.source_regions, expected_plan.source_regions);
2141        assert_eq!(plan.target_regions, expected_plan.target_regions);
2142        assert_eq!(
2143            plan.allocated_region_ids,
2144            expected_plan.allocated_region_ids
2145        );
2146        assert_eq!(
2147            plan.pending_deallocate_region_ids,
2148            expected_plan.pending_deallocate_region_ids
2149        );
2150        assert_eq!(plan.transition_map, expected_plan.transition_map);
2151        assert_eq!(
2152            current_parent_region_routes(&procedure.context).await,
2153            vec![
2154                test_region_route(
2155                    RegionId::new(table_id, 1),
2156                    &range_expr("x", 0, 100).as_json_str().unwrap(),
2157                ),
2158                test_region_route(
2159                    RegionId::new(table_id, 2),
2160                    &range_expr("x", 100, 200).as_json_str().unwrap(),
2161                ),
2162                RegionRoute {
2163                    region: Region {
2164                        id: RegionId::new(table_id, 3),
2165                        partition_expr: range_expr("x", 50, 100).as_json_str().unwrap(),
2166                        ..Default::default()
2167                    },
2168                    leader_peer: Some(Peer::empty(0)),
2169                    ..Default::default()
2170                },
2171            ]
2172        );
2173
2174        let dispatch_status = procedure
2175            .execute(&TestingEnv::procedure_context())
2176            .await
2177            .unwrap();
2178        assert!(!dispatch_status.need_persist());
2179        let subprocedure_ids = extract_subprocedure_ids(dispatch_status);
2180        assert_eq!(subprocedure_ids.len(), 1);
2181        assert_parent_state::<Collect>(&procedure);
2182
2183        let failed_state = ProcedureState::failed(Arc::new(ProcedureError::external(
2184            MockError::new(StatusCode::Internal),
2185        )));
2186        let collect_ctx = procedure_context_with_receivers(HashMap::from([(
2187            subprocedure_ids[0],
2188            procedure_state_receiver(failed_state),
2189        )]));
2190
2191        let err = procedure.execute(&collect_ctx).await.unwrap_err();
2192        assert!(!err.is_retry_later());
2193        assert_parent_state::<Collect>(&procedure);
2194
2195        procedure
2196            .rollback(&TestingEnv::procedure_context())
2197            .await
2198            .unwrap();
2199
2200        let region_routes = current_parent_region_routes(&procedure.context).await;
2201        assert_eq!(
2202            region_routes,
2203            vec![
2204                test_region_route(
2205                    RegionId::new(table_id, 1),
2206                    &range_expr("x", 0, 100).as_json_str().unwrap(),
2207                ),
2208                test_region_route(
2209                    RegionId::new(table_id, 2),
2210                    &range_expr("x", 100, 200).as_json_str().unwrap(),
2211                ),
2212            ]
2213        );
2214    }
2215
2216    #[tokio::test]
2217    async fn test_repartition_procedure_flow_unpartitioned_failed_and_full_rollback() {
2218        let env = TestingEnv::new();
2219        let table_id = 1024;
2220        let node_manager = Arc::new(MockDatanodeManager::new(NaiveDatanodeHandler));
2221
2222        env.create_physical_table_metadata_for_repartition(
2223            table_id,
2224            vec![test_region_route(RegionId::new(table_id, 1), "")],
2225            test_region_wal_options(&[1]),
2226        )
2227        .await;
2228
2229        let context = new_parent_context(&env, node_manager, table_id);
2230        let to_exprs = vec![range_expr("col1", 0, 50), range_expr("col1", 50, 100)];
2231        let mut procedure = RepartitionProcedure::new(
2232            RepartitionFrom::Unpartitioned {
2233                partition_columns: vec!["col1".to_string()],
2234            },
2235            to_exprs.clone(),
2236            context,
2237        );
2238
2239        let start_status = procedure
2240            .execute(&TestingEnv::procedure_context())
2241            .await
2242            .unwrap();
2243        assert!(start_status.need_persist());
2244        assert_parent_state::<UpdatePartitionMetadata>(&procedure);
2245        assert_eq!(
2246            procedure
2247                .context
2248                .persistent_ctx
2249                .partition_metadata_update
2250                .as_ref()
2251                .unwrap()
2252                .target_partition_key_indices,
2253            vec![0]
2254        );
2255
2256        let update_status = procedure
2257            .execute(&TestingEnv::procedure_context())
2258            .await
2259            .unwrap();
2260        assert!(update_status.need_persist());
2261        assert_parent_state::<AllocateRegion>(&procedure);
2262        assert_eq!(
2263            table_partition_key_indices(&procedure.context).await,
2264            vec![0]
2265        );
2266
2267        let build_allocate_status = procedure
2268            .execute(&TestingEnv::procedure_context())
2269            .await
2270            .unwrap();
2271        assert!(build_allocate_status.need_persist());
2272        assert_parent_state::<AllocateRegion>(&procedure);
2273        assert_eq!(procedure.context.persistent_ctx.plans.len(), 1);
2274        let plan = &procedure.context.persistent_ctx.plans[0];
2275        assert_eq!(
2276            plan.source_regions,
2277            vec![SourceRegionDescriptor::Default {
2278                region_id: RegionId::new(table_id, 1)
2279            }]
2280        );
2281        assert_eq!(plan.target_regions.len(), 2);
2282        assert_eq!(plan.target_regions[0].region_id, RegionId::new(table_id, 1));
2283        assert_eq!(plan.target_regions[0].partition_expr, to_exprs[0]);
2284        assert_eq!(
2285            plan.allocated_region_ids,
2286            vec![plan.target_regions[1].region_id]
2287        );
2288        assert!(plan.pending_deallocate_region_ids.is_empty());
2289        assert_eq!(plan.transition_map, vec![vec![0, 1]]);
2290        let target_regions = plan.target_regions.clone();
2291
2292        let execute_allocate_status = procedure
2293            .execute(&TestingEnv::procedure_context())
2294            .await
2295            .unwrap();
2296        assert!(execute_allocate_status.need_persist());
2297        assert_parent_state::<Dispatch>(&procedure);
2298        let region_routes = current_parent_region_routes(&procedure.context).await;
2299        assert_eq!(region_routes.len(), 2);
2300        assert_eq!(
2301            region_route_by_id(&region_routes, target_regions[0].region_id)
2302                .region
2303                .partition_expr(),
2304            ""
2305        );
2306        assert_eq!(
2307            region_route_by_id(&region_routes, target_regions[1].region_id)
2308                .region
2309                .partition_expr(),
2310            to_exprs[1].as_json_str().unwrap()
2311        );
2312
2313        let dispatch_status = procedure
2314            .execute(&TestingEnv::procedure_context())
2315            .await
2316            .unwrap();
2317        let subprocedure_ids = extract_subprocedure_ids(dispatch_status);
2318        assert_eq!(subprocedure_ids.len(), 1);
2319        assert_parent_state::<Collect>(&procedure);
2320
2321        let failed_state = ProcedureState::failed(Arc::new(ProcedureError::external(
2322            MockError::new(StatusCode::Internal),
2323        )));
2324        let collect_ctx = procedure_context_with_receivers(HashMap::from([(
2325            subprocedure_ids[0],
2326            procedure_state_receiver(failed_state),
2327        )]));
2328        let err = procedure.execute(&collect_ctx).await.unwrap_err();
2329        assert!(!err.is_retry_later());
2330        assert_parent_state::<Collect>(&procedure);
2331
2332        procedure
2333            .rollback(&TestingEnv::procedure_context())
2334            .await
2335            .unwrap();
2336
2337        assert!(
2338            table_partition_key_indices(&procedure.context)
2339                .await
2340                .is_empty()
2341        );
2342        assert_eq!(
2343            current_parent_region_routes(&procedure.context).await,
2344            vec![test_region_route(RegionId::new(table_id, 1), "")]
2345        );
2346    }
2347
2348    #[tokio::test]
2349    async fn test_repartition_procedure_flow_unpartitioned_rollback_is_idempotent() {
2350        let env = TestingEnv::new();
2351        let table_id = 1024;
2352        let node_manager = Arc::new(MockDatanodeManager::new(NaiveDatanodeHandler));
2353
2354        env.create_physical_table_metadata_for_repartition(
2355            table_id,
2356            vec![test_region_route(RegionId::new(table_id, 1), "")],
2357            test_region_wal_options(&[1]),
2358        )
2359        .await;
2360
2361        let context = new_parent_context(&env, node_manager, table_id);
2362        let mut procedure = RepartitionProcedure::new(
2363            RepartitionFrom::Unpartitioned {
2364                partition_columns: vec!["col1".to_string()],
2365            },
2366            vec![range_expr("col1", 0, 50), range_expr("col1", 50, 100)],
2367            context,
2368        );
2369
2370        procedure
2371            .execute(&TestingEnv::procedure_context())
2372            .await
2373            .unwrap();
2374        procedure
2375            .execute(&TestingEnv::procedure_context())
2376            .await
2377            .unwrap();
2378        procedure
2379            .execute(&TestingEnv::procedure_context())
2380            .await
2381            .unwrap();
2382        procedure
2383            .execute(&TestingEnv::procedure_context())
2384            .await
2385            .unwrap();
2386        assert_eq!(
2387            table_partition_key_indices(&procedure.context).await,
2388            vec![0]
2389        );
2390        assert_eq!(
2391            current_parent_region_routes(&procedure.context).await.len(),
2392            2
2393        );
2394
2395        let dispatch_status = procedure
2396            .execute(&TestingEnv::procedure_context())
2397            .await
2398            .unwrap();
2399        let subprocedure_ids = extract_subprocedure_ids(dispatch_status);
2400        assert_eq!(subprocedure_ids.len(), 1);
2401        assert_parent_state::<Collect>(&procedure);
2402
2403        let failed_state = ProcedureState::failed(Arc::new(ProcedureError::external(
2404            MockError::new(StatusCode::Internal),
2405        )));
2406        let collect_ctx = procedure_context_with_receivers(HashMap::from([(
2407            subprocedure_ids[0],
2408            procedure_state_receiver(failed_state),
2409        )]));
2410        let err = procedure.execute(&collect_ctx).await.unwrap_err();
2411        assert!(!err.is_retry_later());
2412
2413        procedure
2414            .rollback(&TestingEnv::procedure_context())
2415            .await
2416            .unwrap();
2417        let once_indices = table_partition_key_indices(&procedure.context).await;
2418        let once_routes = current_parent_region_routes(&procedure.context).await;
2419
2420        procedure
2421            .rollback(&TestingEnv::procedure_context())
2422            .await
2423            .unwrap();
2424        let twice_indices = table_partition_key_indices(&procedure.context).await;
2425        let twice_routes = current_parent_region_routes(&procedure.context).await;
2426
2427        assert_eq!(once_indices, twice_indices);
2428        assert_eq!(once_routes, twice_routes);
2429        assert!(twice_indices.is_empty());
2430        assert_eq!(
2431            twice_routes,
2432            vec![test_region_route(RegionId::new(table_id, 1), "")]
2433        );
2434    }
2435
2436    #[tokio::test]
2437    async fn test_repartition_procedure_flow_split_allocate_retryable_then_resume() {
2438        common_telemetry::init_default_ut_logging();
2439        let env = TestingEnv::new();
2440        let table_id = 1024;
2441        let (tx, _rx) = mpsc::channel(8);
2442        let should_retry = Arc::new(AtomicBool::new(true));
2443        let datanode_handler = DatanodeWatcher::new(tx).with_handler(move |_, _| {
2444            if should_retry.swap(false, Ordering::SeqCst) {
2445                return Err(error::Error::RetryLater {
2446                    source: BoxedError::new(
2447                        error::UnexpectedSnafu {
2448                            err_msg: "retry later",
2449                        }
2450                        .build(),
2451                    ),
2452                    clean_poisons: false,
2453                });
2454            }
2455
2456            Ok(api::region::RegionResponse::new(0))
2457        });
2458        let node_manager = Arc::new(MockDatanodeManager::new(datanode_handler));
2459
2460        env.create_physical_table_metadata_for_repartition(
2461            table_id,
2462            vec![
2463                test_region_route(
2464                    RegionId::new(table_id, 1),
2465                    &range_expr("x", 0, 100).as_json_str().unwrap(),
2466                ),
2467                test_region_route(
2468                    RegionId::new(table_id, 2),
2469                    &range_expr("x", 100, 200).as_json_str().unwrap(),
2470                ),
2471            ],
2472            test_region_wal_options(&[1, 2]),
2473        )
2474        .await;
2475
2476        let context = new_parent_context(&env, node_manager, table_id);
2477        let mut procedure = RepartitionProcedure::new(
2478            RepartitionFrom::Partitioned {
2479                exprs: vec![range_expr("x", 0, 100)],
2480                target_partition_columns: None,
2481            },
2482            vec![range_expr("x", 0, 50), range_expr("x", 50, 100)],
2483            context,
2484        );
2485
2486        let start_status = procedure
2487            .execute(&TestingEnv::procedure_context())
2488            .await
2489            .unwrap();
2490        assert!(!start_status.need_persist());
2491        let start_status = procedure
2492            .execute(&TestingEnv::procedure_context())
2493            .await
2494            .unwrap();
2495        assert!(start_status.need_persist());
2496        assert_parent_state::<AllocateRegion>(&procedure);
2497
2498        let err = procedure
2499            .execute(&TestingEnv::procedure_context())
2500            .await
2501            .unwrap_err();
2502        assert!(err.is_retry_later());
2503        assert_parent_state::<AllocateRegion>(&procedure);
2504        assert!(!procedure.context.persistent_ctx.plans.is_empty());
2505        assert_eq!(
2506            current_parent_region_routes(&procedure.context).await,
2507            vec![
2508                test_region_route(
2509                    RegionId::new(table_id, 1),
2510                    &range_expr("x", 0, 100).as_json_str().unwrap(),
2511                ),
2512                test_region_route(
2513                    RegionId::new(table_id, 2),
2514                    &range_expr("x", 100, 200).as_json_str().unwrap(),
2515                ),
2516            ]
2517        );
2518
2519        let allocate_status = procedure
2520            .execute(&TestingEnv::procedure_context())
2521            .await
2522            .unwrap();
2523        assert!(allocate_status.need_persist());
2524        assert_parent_state::<Dispatch>(&procedure);
2525
2526        assert_eq!(procedure.context.persistent_ctx.plans.len(), 1);
2527        let plan = &procedure.context.persistent_ctx.plans[0];
2528        let expected_plan = test_plan(table_id);
2529        assert_eq!(plan.source_regions, expected_plan.source_regions);
2530        assert_eq!(plan.target_regions, expected_plan.target_regions);
2531        assert_eq!(
2532            plan.allocated_region_ids,
2533            expected_plan.allocated_region_ids
2534        );
2535        assert_eq!(plan.transition_map, expected_plan.transition_map);
2536        assert_eq!(
2537            current_parent_region_routes(&procedure.context).await,
2538            vec![
2539                test_region_route(
2540                    RegionId::new(table_id, 1),
2541                    &range_expr("x", 0, 100).as_json_str().unwrap(),
2542                ),
2543                test_region_route(
2544                    RegionId::new(table_id, 2),
2545                    &range_expr("x", 100, 200).as_json_str().unwrap(),
2546                ),
2547                RegionRoute {
2548                    region: Region {
2549                        id: RegionId::new(table_id, 3),
2550                        partition_expr: range_expr("x", 50, 100).as_json_str().unwrap(),
2551                        ..Default::default()
2552                    },
2553                    leader_peer: Some(Peer::empty(0)),
2554                    ..Default::default()
2555                },
2556            ]
2557        );
2558
2559        let dispatch_status = procedure
2560            .execute(&TestingEnv::procedure_context())
2561            .await
2562            .unwrap();
2563        assert!(!dispatch_status.need_persist());
2564        let subprocedure_ids = extract_subprocedure_ids(dispatch_status);
2565        assert_eq!(subprocedure_ids.len(), 1);
2566        assert_parent_state::<Collect>(&procedure);
2567    }
2568}