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