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