Skip to main content

meta_srv/procedure/
region_migration.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(crate) mod close_downgraded_region;
16pub(crate) mod downgrade_leader_region;
17pub(crate) mod flush_leader_region;
18pub(crate) mod manager;
19pub(crate) mod migration_abort;
20pub(crate) mod migration_end;
21pub(crate) mod migration_start;
22pub(crate) mod open_candidate_region;
23#[cfg(test)]
24pub mod test_util;
25pub(crate) mod update_metadata;
26pub(crate) mod upgrade_candidate_region;
27pub(crate) mod utils;
28
29use std::any::Any;
30use std::collections::{HashMap, HashSet};
31use std::fmt::{Debug, Display};
32use std::time::Duration;
33
34use common_error::ext::BoxedError;
35use common_event_recorder::{Event, PersistentEventContext};
36use common_meta::cache_invalidator::CacheInvalidatorRef;
37use common_meta::ddl::RegionFailureDetectorControllerRef;
38use common_meta::instruction::CacheIdent;
39use common_meta::key::datanode_table::{DatanodeTableKey, DatanodeTableValue};
40use common_meta::key::table_route::TableRouteValue;
41use common_meta::key::topic_name::TopicNameKey;
42use common_meta::key::topic_region::{ReplayCheckpoint, TopicRegionKey};
43use common_meta::key::{DeserializedValueWithBytes, TableMetadataManagerRef};
44use common_meta::kv_backend::{KvBackendRef, ResettableKvBackendRef};
45use common_meta::lock_key::{CatalogLock, RegionLock, SchemaLock, TableLock};
46use common_meta::peer::Peer;
47use common_meta::region_keeper::{MemoryRegionKeeperRef, OperatingRegionGuard};
48use common_meta::rpc::ddl::TriggerReason;
49use common_procedure::error::{
50    Error as ProcedureError, FromJsonSnafu, Result as ProcedureResult, ToJsonSnafu,
51};
52use common_procedure::{
53    Context as ProcedureContext, EventContext, LockKey, Procedure, Status, StringKey,
54};
55use common_telemetry::{debug, error, info};
56use manager::RegionMigrationProcedureGuard;
57pub use manager::{
58    RegionMigrationManagerRef, RegionMigrationProcedureTask, RegionMigrationProcedureTracker,
59    RegionMigrationTriggerReason,
60};
61use serde::{Deserialize, Deserializer, Serialize};
62use snafu::{OptionExt, ResultExt};
63use store_api::storage::{RegionId, TableId};
64use tokio::time::Instant;
65
66use self::migration_start::RegionMigrationStart;
67use crate::error::{self, Result};
68use crate::event::region_migration::{REGION_MIGRATION_EVENT_TYPE, RegionMigrationEvent};
69use crate::metrics::{
70    METRIC_META_REGION_MIGRATION_ERROR, METRIC_META_REGION_MIGRATION_EXECUTE,
71    METRIC_META_REGION_MIGRATION_STAGE_ELAPSED,
72};
73use crate::service::mailbox::MailboxRef;
74
75/// The default timeout for region migration.
76pub const DEFAULT_REGION_MIGRATION_TIMEOUT: Duration = Duration::from_secs(120);
77
78#[derive(Debug, Deserialize)]
79#[serde(untagged)]
80enum SingleOrMultiple<T> {
81    Single(T),
82    Multiple(Vec<T>),
83}
84
85fn single_or_multiple_from<'de, D, T>(deserializer: D) -> std::result::Result<Vec<T>, D::Error>
86where
87    D: Deserializer<'de>,
88    T: Deserialize<'de>,
89{
90    let helper = SingleOrMultiple::<T>::deserialize(deserializer)?;
91    Ok(match helper {
92        SingleOrMultiple::Single(x) => vec![x],
93        SingleOrMultiple::Multiple(xs) => xs,
94    })
95}
96
97/// It's shared in each step and available even after recovering.
98///
99/// It will only be updated/stored after the Red node has succeeded.
100///
101/// **Notes: Stores with too large data in the context might incur replication overhead.**
102#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
103pub struct PersistentContext {
104    /// The table catalog.
105    #[deprecated(note = "use `catalog_and_schema` instead")]
106    #[serde(default, skip_serializing_if = "Option::is_none")]
107    pub(crate) catalog: Option<String>,
108    /// The table schema.
109    #[deprecated(note = "use `catalog_and_schema` instead")]
110    #[serde(default, skip_serializing_if = "Option::is_none")]
111    pub(crate) schema: Option<String>,
112    /// The catalog and schema of the regions.
113    #[serde(default, skip_serializing_if = "Vec::is_empty")]
114    pub(crate) catalog_and_schema: Vec<(String, String)>,
115    /// The [Peer] of migration source.
116    pub(crate) from_peer: Peer,
117    /// The [Peer] of migration destination.
118    pub(crate) to_peer: Peer,
119    /// The [RegionId] of migration region.
120    #[serde(deserialize_with = "single_or_multiple_from", alias = "region_id")]
121    pub(crate) region_ids: Vec<RegionId>,
122    /// The timeout for downgrading leader region and upgrading candidate region operations.
123    #[serde(with = "humantime_serde", default = "default_timeout")]
124    pub(crate) timeout: Duration,
125    /// The trigger reason persisted for compatibility with versions without procedure context.
126    #[serde(default)]
127    trigger_reason: RegionMigrationTriggerReason,
128}
129
130impl PersistentContext {
131    pub fn new(
132        catalog_and_schema: Vec<(String, String)>,
133        from_peer: Peer,
134        to_peer: Peer,
135        region_ids: Vec<RegionId>,
136        timeout: Duration,
137        trigger_reason: RegionMigrationTriggerReason,
138    ) -> Self {
139        #[allow(deprecated)]
140        Self {
141            catalog: None,
142            schema: None,
143            catalog_and_schema,
144            from_peer,
145            to_peer,
146            region_ids,
147            timeout,
148            trigger_reason,
149        }
150    }
151}
152
153impl RegionMigrationTriggerReason {
154    fn to_trigger_reason(self) -> TriggerReason {
155        match self {
156            Self::Manual => TriggerReason::Manual,
157            Self::AutoRebalance => TriggerReason::AutoRebalance,
158            Self::Failover => TriggerReason::RegionFailover,
159            Self::Unknown => TriggerReason::Unknown,
160        }
161    }
162
163    pub(crate) fn from_trigger_reason(reason: TriggerReason) -> Self {
164        match reason {
165            TriggerReason::Manual => Self::Manual,
166            TriggerReason::AutoRebalance => Self::AutoRebalance,
167            TriggerReason::RegionFailover => Self::Failover,
168            _ => Self::Unknown,
169        }
170    }
171}
172
173fn default_timeout() -> Duration {
174    Duration::from_secs(10)
175}
176
177impl PersistentContext {
178    pub fn lock_key(&self) -> Vec<StringKey> {
179        let mut lock_keys =
180            Vec::with_capacity(self.region_ids.len() + 2 + self.catalog_and_schema.len() * 2);
181        #[allow(deprecated)]
182        if let (Some(catalog), Some(schema)) = (&self.catalog, &self.schema) {
183            lock_keys.push(CatalogLock::Read(catalog).into());
184            lock_keys.push(SchemaLock::read(catalog, schema).into());
185        }
186        for (catalog, schema) in self.catalog_and_schema.iter() {
187            lock_keys.push(CatalogLock::Read(catalog).into());
188            lock_keys.push(SchemaLock::read(catalog, schema).into());
189        }
190
191        // Sort the region ids to ensure the same order of region ids.
192        let mut region_ids = self.region_ids.clone();
193        region_ids.sort_unstable();
194        for region_id in region_ids {
195            lock_keys.push(RegionLock::Write(region_id).into());
196        }
197        lock_keys
198    }
199
200    /// Returns the table ids of the regions.
201    ///
202    /// The return value is a set of table ids.
203    pub fn region_table_ids(&self) -> Vec<TableId> {
204        self.region_ids
205            .iter()
206            .map(|region_id| region_id.table_id())
207            .collect::<HashSet<_>>()
208            .into_iter()
209            .collect()
210    }
211
212    /// Returns the table regions map.
213    ///
214    /// The key is the table id, the value is the region ids of the table.
215    pub fn table_regions(&self) -> HashMap<TableId, Vec<RegionId>> {
216        let mut table_regions = HashMap::new();
217        for region_id in &self.region_ids {
218            table_regions
219                .entry(region_id.table_id())
220                .or_insert_with(Vec::new)
221                .push(*region_id);
222        }
223        table_regions
224    }
225}
226
227/// Metrics of region migration.
228#[derive(Debug, Clone, Default)]
229pub struct Metrics {
230    /// Elapsed time of downgrading region and upgrading region.
231    operations_elapsed: Duration,
232    /// Elapsed time of flushing leader region.
233    flush_leader_region_elapsed: Duration,
234    /// Elapsed time of downgrading leader region.
235    downgrade_leader_region_elapsed: Duration,
236    /// Elapsed time of open candidate region.
237    open_candidate_region_elapsed: Duration,
238    /// Elapsed time of upgrade candidate region.
239    upgrade_candidate_region_elapsed: Duration,
240}
241
242impl Display for Metrics {
243    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
244        let total = self.flush_leader_region_elapsed
245            + self.downgrade_leader_region_elapsed
246            + self.open_candidate_region_elapsed
247            + self.upgrade_candidate_region_elapsed;
248        write!(
249            f,
250            "total: {:?}, flush_leader_region_elapsed: {:?}, downgrade_leader_region_elapsed: {:?}, open_candidate_region_elapsed: {:?}, upgrade_candidate_region_elapsed: {:?}",
251            total,
252            self.flush_leader_region_elapsed,
253            self.downgrade_leader_region_elapsed,
254            self.open_candidate_region_elapsed,
255            self.upgrade_candidate_region_elapsed
256        )
257    }
258}
259
260impl Metrics {
261    /// Updates the elapsed time of downgrading region and upgrading region.
262    pub fn update_operations_elapsed(&mut self, elapsed: Duration) {
263        self.operations_elapsed += elapsed;
264    }
265
266    /// Updates the elapsed time of flushing leader region.
267    pub fn update_flush_leader_region_elapsed(&mut self, elapsed: Duration) {
268        self.flush_leader_region_elapsed += elapsed;
269    }
270
271    /// Updates the elapsed time of downgrading leader region.
272    pub fn update_downgrade_leader_region_elapsed(&mut self, elapsed: Duration) {
273        self.downgrade_leader_region_elapsed += elapsed;
274    }
275
276    /// Updates the elapsed time of open candidate region.
277    pub fn update_open_candidate_region_elapsed(&mut self, elapsed: Duration) {
278        self.open_candidate_region_elapsed += elapsed;
279    }
280
281    /// Updates the elapsed time of upgrade candidate region.
282    pub fn update_upgrade_candidate_region_elapsed(&mut self, elapsed: Duration) {
283        self.upgrade_candidate_region_elapsed += elapsed;
284    }
285}
286
287impl Drop for Metrics {
288    fn drop(&mut self) {
289        let total = self.flush_leader_region_elapsed
290            + self.downgrade_leader_region_elapsed
291            + self.open_candidate_region_elapsed
292            + self.upgrade_candidate_region_elapsed;
293        METRIC_META_REGION_MIGRATION_STAGE_ELAPSED
294            .with_label_values(&["total"])
295            .observe(total.as_secs_f64());
296
297        if !self.flush_leader_region_elapsed.is_zero() {
298            METRIC_META_REGION_MIGRATION_STAGE_ELAPSED
299                .with_label_values(&["flush_leader_region"])
300                .observe(self.flush_leader_region_elapsed.as_secs_f64());
301        }
302
303        if !self.downgrade_leader_region_elapsed.is_zero() {
304            METRIC_META_REGION_MIGRATION_STAGE_ELAPSED
305                .with_label_values(&["downgrade_leader_region"])
306                .observe(self.downgrade_leader_region_elapsed.as_secs_f64());
307        }
308
309        if !self.open_candidate_region_elapsed.is_zero() {
310            METRIC_META_REGION_MIGRATION_STAGE_ELAPSED
311                .with_label_values(&["open_candidate_region"])
312                .observe(self.open_candidate_region_elapsed.as_secs_f64());
313        }
314
315        if !self.upgrade_candidate_region_elapsed.is_zero() {
316            METRIC_META_REGION_MIGRATION_STAGE_ELAPSED
317                .with_label_values(&["upgrade_candidate_region"])
318                .observe(self.upgrade_candidate_region_elapsed.as_secs_f64());
319        }
320    }
321}
322
323/// It's shared in each step and available in executing (including retrying).
324///
325/// It will be dropped if the procedure runner crashes.
326///
327/// The additional remote fetches are only required in the worst cases.
328#[derive(Debug, Clone, Default)]
329pub struct VolatileContext {
330    /// `opening_region_guard` will be set after the
331    /// [OpenCandidateRegion](crate::procedure::region_migration::open_candidate_region::OpenCandidateRegion) step.
332    ///
333    /// `opening_region_guards` should be consumed after
334    /// the corresponding [RegionRoute](common_meta::rpc::router::RegionRoute) of the opening region
335    /// was written into [TableRouteValue](common_meta::key::table_route::TableRouteValue).
336    opening_region_guards: Vec<OperatingRegionGuard>,
337    /// The deadline of leader region lease.
338    leader_region_lease_deadline: Option<Instant>,
339    /// The datanode table values.
340    from_peer_datanode_table_values: Option<HashMap<TableId, DatanodeTableValue>>,
341    /// The last_entry_ids of leader regions.
342    leader_region_last_entry_ids: HashMap<RegionId, u64>,
343    /// The last_entry_ids of leader metadata regions (Only used for metric engine).
344    leader_region_metadata_last_entry_ids: HashMap<RegionId, u64>,
345    /// Metrics of region migration.
346    metrics: Metrics,
347}
348
349impl VolatileContext {
350    /// Sets the `leader_region_lease_deadline` if it does not exist.
351    pub fn set_leader_region_lease_deadline(&mut self, lease_timeout: Duration) {
352        if self.leader_region_lease_deadline.is_none() {
353            self.leader_region_lease_deadline = Some(Instant::now() + lease_timeout);
354        }
355    }
356
357    /// Resets the `leader_region_lease_deadline`.
358    pub fn reset_leader_region_lease_deadline(&mut self) {
359        self.leader_region_lease_deadline = None;
360    }
361
362    /// Sets the `leader_region_last_entry_id`.
363    pub fn set_last_entry_id(&mut self, region_id: RegionId, last_entry_id: u64) {
364        self.leader_region_last_entry_ids
365            .insert(region_id, last_entry_id);
366    }
367
368    /// Sets the `leader_region_metadata_last_entry_id`.
369    pub fn set_metadata_last_entry_id(&mut self, region_id: RegionId, last_entry_id: u64) {
370        self.leader_region_metadata_last_entry_ids
371            .insert(region_id, last_entry_id);
372    }
373}
374
375/// Used to generate new [Context].
376pub trait ContextFactory {
377    fn new_context(self, persistent_ctx: PersistentContext) -> Context;
378}
379
380/// Default implementation.
381#[derive(Clone)]
382pub struct DefaultContextFactory {
383    volatile_ctx: VolatileContext,
384    in_memory_key: ResettableKvBackendRef,
385    table_metadata_manager: TableMetadataManagerRef,
386    opening_region_keeper: MemoryRegionKeeperRef,
387    region_failure_detector_controller: RegionFailureDetectorControllerRef,
388    mailbox: MailboxRef,
389    server_addr: String,
390    cache_invalidator: CacheInvalidatorRef,
391}
392
393impl DefaultContextFactory {
394    /// Returns an [`DefaultContextFactory`].
395    pub fn new(
396        in_memory_key: ResettableKvBackendRef,
397        table_metadata_manager: TableMetadataManagerRef,
398        opening_region_keeper: MemoryRegionKeeperRef,
399        region_failure_detector_controller: RegionFailureDetectorControllerRef,
400        mailbox: MailboxRef,
401        server_addr: String,
402        cache_invalidator: CacheInvalidatorRef,
403    ) -> Self {
404        Self {
405            volatile_ctx: VolatileContext::default(),
406            in_memory_key,
407            table_metadata_manager,
408            opening_region_keeper,
409            region_failure_detector_controller,
410            mailbox,
411            server_addr,
412            cache_invalidator,
413        }
414    }
415}
416
417impl ContextFactory for DefaultContextFactory {
418    fn new_context(self, persistent_ctx: PersistentContext) -> Context {
419        Context {
420            persistent_ctx,
421            volatile_ctx: self.volatile_ctx,
422            in_memory: self.in_memory_key,
423            table_metadata_manager: self.table_metadata_manager,
424            opening_region_keeper: self.opening_region_keeper,
425            region_failure_detector_controller: self.region_failure_detector_controller,
426            mailbox: self.mailbox,
427            server_addr: self.server_addr,
428            cache_invalidator: self.cache_invalidator,
429        }
430    }
431}
432
433/// The context of procedure execution.
434pub struct Context {
435    persistent_ctx: PersistentContext,
436    volatile_ctx: VolatileContext,
437    in_memory: KvBackendRef,
438    table_metadata_manager: TableMetadataManagerRef,
439    opening_region_keeper: MemoryRegionKeeperRef,
440    region_failure_detector_controller: RegionFailureDetectorControllerRef,
441    mailbox: MailboxRef,
442    server_addr: String,
443    cache_invalidator: CacheInvalidatorRef,
444}
445
446impl Context {
447    pub(crate) fn trigger_reason(
448        &self,
449        event_context: Option<&PersistentEventContext>,
450    ) -> RegionMigrationTriggerReason {
451        event_context
452            .map(|ctx| RegionMigrationTriggerReason::from_trigger_reason(ctx.reason))
453            .unwrap_or(self.persistent_ctx.trigger_reason)
454    }
455    /// Returns the next operation's timeout.
456    pub fn next_operation_timeout(&self) -> Option<Duration> {
457        self.persistent_ctx
458            .timeout
459            .checked_sub(self.volatile_ctx.metrics.operations_elapsed)
460    }
461
462    /// Updates operations elapsed.
463    pub fn update_operations_elapsed(&mut self, instant: Instant) {
464        self.volatile_ctx
465            .metrics
466            .update_operations_elapsed(instant.elapsed());
467    }
468
469    /// Updates the elapsed time of flushing leader region.
470    pub fn update_flush_leader_region_elapsed(&mut self, instant: Instant) {
471        self.volatile_ctx
472            .metrics
473            .update_flush_leader_region_elapsed(instant.elapsed());
474    }
475
476    /// Updates the elapsed time of downgrading leader region.
477    pub fn update_downgrade_leader_region_elapsed(&mut self, instant: Instant) {
478        self.volatile_ctx
479            .metrics
480            .update_downgrade_leader_region_elapsed(instant.elapsed());
481    }
482
483    /// Updates the elapsed time of open candidate region.
484    pub fn update_open_candidate_region_elapsed(&mut self, instant: Instant) {
485        self.volatile_ctx
486            .metrics
487            .update_open_candidate_region_elapsed(instant.elapsed());
488    }
489
490    /// Updates the elapsed time of upgrade candidate region.
491    pub fn update_upgrade_candidate_region_elapsed(&mut self, instant: Instant) {
492        self.volatile_ctx
493            .metrics
494            .update_upgrade_candidate_region_elapsed(instant.elapsed());
495    }
496
497    /// Returns address of meta server.
498    pub fn server_addr(&self) -> &str {
499        &self.server_addr
500    }
501
502    /// Returns the table ids of the regions.
503    pub fn region_table_ids(&self) -> Vec<TableId> {
504        self.persistent_ctx
505            .region_ids
506            .iter()
507            .map(|region_id| region_id.table_id())
508            .collect::<HashSet<_>>()
509            .into_iter()
510            .collect()
511    }
512
513    /// Returns the `table_routes` of [VolatileContext] if any.
514    /// Otherwise, returns the value retrieved from remote.
515    ///
516    /// Retry:
517    /// - Failed to retrieve the metadata of table.
518    pub async fn get_table_route_values(
519        &self,
520    ) -> Result<HashMap<TableId, DeserializedValueWithBytes<TableRouteValue>>> {
521        let table_ids = self.persistent_ctx.region_table_ids();
522        let table_routes = self
523            .table_metadata_manager
524            .table_route_manager()
525            .table_route_storage()
526            .batch_get_with_raw_bytes(&table_ids)
527            .await
528            .context(error::TableMetadataManagerSnafu)
529            .map_err(BoxedError::new)
530            .with_context(|_| error::RetryLaterWithSourceSnafu {
531                reason: format!("Failed to get table routes: {table_ids:?}"),
532            })?;
533        let table_routes = table_ids
534            .into_iter()
535            .zip(table_routes)
536            .filter_map(|(table_id, table_route)| {
537                table_route.map(|table_route| (table_id, table_route))
538            })
539            .collect::<HashMap<_, _>>();
540        Ok(table_routes)
541    }
542
543    /// Returns the `table_route` of [VolatileContext] if any.
544    /// Otherwise, returns the value retrieved from remote.
545    ///
546    /// Retry:
547    /// - Failed to retrieve the metadata of table.
548    pub async fn get_table_route_value(
549        &self,
550        table_id: TableId,
551    ) -> Result<DeserializedValueWithBytes<TableRouteValue>> {
552        let table_route_value = self
553            .table_metadata_manager
554            .table_route_manager()
555            .table_route_storage()
556            .get_with_raw_bytes(table_id)
557            .await
558            .context(error::TableMetadataManagerSnafu)
559            .map_err(BoxedError::new)
560            .with_context(|_| error::RetryLaterWithSourceSnafu {
561                reason: format!("Failed to get table routes: {table_id:}"),
562            })?
563            .context(error::TableRouteNotFoundSnafu { table_id })?;
564        Ok(table_route_value)
565    }
566
567    /// Returns the `from_peer_datanode_table_values` of [VolatileContext] if any.
568    /// Otherwise, returns the value retrieved from remote.
569    ///
570    /// Retry:
571    /// - Failed to retrieve the metadata of datanode table.
572    pub async fn get_from_peer_datanode_table_values(
573        &mut self,
574    ) -> Result<&HashMap<TableId, DatanodeTableValue>> {
575        let from_peer_datanode_table_values =
576            &mut self.volatile_ctx.from_peer_datanode_table_values;
577        if from_peer_datanode_table_values.is_none() {
578            let table_ids = self.persistent_ctx.region_table_ids();
579            let datanode_table_keys = table_ids
580                .iter()
581                .map(|table_id| DatanodeTableKey {
582                    datanode_id: self.persistent_ctx.from_peer.id,
583                    table_id: *table_id,
584                })
585                .collect::<Vec<_>>();
586            let datanode_table_values = self
587                .table_metadata_manager
588                .datanode_table_manager()
589                .batch_get(&datanode_table_keys)
590                .await
591                .context(error::TableMetadataManagerSnafu)
592                .map_err(BoxedError::new)
593                .with_context(|_| error::RetryLaterWithSourceSnafu {
594                    reason: format!("Failed to get DatanodeTable: {table_ids:?}"),
595                })?
596                .into_iter()
597                .map(|(k, v)| (k.table_id, v))
598                .collect();
599            *from_peer_datanode_table_values = Some(datanode_table_values);
600        }
601        Ok(from_peer_datanode_table_values.as_ref().unwrap())
602    }
603
604    /// Returns the `from_peer_datanode_table_value` of [VolatileContext] if any.
605    /// Otherwise, returns the value retrieved from remote.
606    ///
607    /// Retry:
608    /// - Failed to retrieve the metadata of datanode table.
609    pub async fn get_from_peer_datanode_table_value(
610        &self,
611        table_id: TableId,
612    ) -> Result<DatanodeTableValue> {
613        let datanode_table_value = self
614            .table_metadata_manager
615            .datanode_table_manager()
616            .get(&DatanodeTableKey {
617                datanode_id: self.persistent_ctx.from_peer.id,
618                table_id,
619            })
620            .await
621            .context(error::TableMetadataManagerSnafu)
622            .map_err(BoxedError::new)
623            .with_context(|_| error::RetryLaterWithSourceSnafu {
624                reason: format!("Failed to get DatanodeTable: {table_id}"),
625            })?
626            .context(error::DatanodeTableNotFoundSnafu {
627                table_id,
628                datanode_id: self.persistent_ctx.from_peer.id,
629            })?;
630        Ok(datanode_table_value)
631    }
632
633    /// Notifies the RegionSupervisor to register failure detectors of failed region.
634    ///
635    /// The original failure detector was removed once the procedure was triggered.
636    /// Now, we need to register the failure detector for the failed region again.
637    pub async fn register_failure_detectors(&self) {
638        let datanode_id = self.persistent_ctx.from_peer.id;
639        let region_ids = &self.persistent_ctx.region_ids;
640        let detecting_regions = region_ids
641            .iter()
642            .map(|region_id| (datanode_id, *region_id))
643            .collect::<Vec<_>>();
644        self.region_failure_detector_controller
645            .register_failure_detectors(detecting_regions)
646            .await;
647        info!(
648            "Registered failure detectors after migration failures for datanode {}, regions {:?}",
649            datanode_id, region_ids
650        );
651    }
652
653    /// Notifies the RegionSupervisor to reset failure detectors of candidate regions.
654    pub async fn reset_failure_detectors_for_candidate_regions(&self) {
655        let datanode_id = self.persistent_ctx.to_peer.id;
656        let region_ids = &self.persistent_ctx.region_ids;
657        let detecting_regions = region_ids
658            .iter()
659            .map(|region_id| (datanode_id, *region_id))
660            .collect::<Vec<_>>();
661        self.region_failure_detector_controller
662            .reset_failure_detectors(detecting_regions)
663            .await;
664        info!(
665            "Reset failure detectors after migration success for datanode {}, regions {:?}",
666            datanode_id, region_ids
667        );
668    }
669
670    /// Notifies the RegionSupervisor to deregister failure detectors.
671    ///
672    /// The original failure detectors won't be removed once the procedure was triggered.
673    /// We need to deregister the failure detectors for the original region if the procedure is finished.
674    pub async fn deregister_failure_detectors(&self) {
675        let datanode_id = self.persistent_ctx.from_peer.id;
676        let region_ids = &self.persistent_ctx.region_ids;
677        let detecting_regions = region_ids
678            .iter()
679            .map(|region_id| (datanode_id, *region_id))
680            .collect::<Vec<_>>();
681
682        self.region_failure_detector_controller
683            .deregister_failure_detectors(detecting_regions)
684            .await;
685    }
686
687    /// Notifies the RegionSupervisor to deregister failure detectors for the candidate regions on the destination peer.
688    ///
689    /// The candidate regions may be created on the destination peer,
690    /// so we need to deregister the failure detectors for the candidate regions if the procedure is aborted.
691    pub async fn deregister_failure_detectors_for_candidate_regions(&self) {
692        let to_peer_id = self.persistent_ctx.to_peer.id;
693        let region_ids = &self.persistent_ctx.region_ids;
694        let detecting_regions = region_ids
695            .iter()
696            .map(|region_id| (to_peer_id, *region_id))
697            .collect::<Vec<_>>();
698
699        self.region_failure_detector_controller
700            .deregister_failure_detectors(detecting_regions)
701            .await;
702    }
703
704    /// Fetches replay checkpoints and merges them with topic pruned entry ids.
705    pub async fn get_replay_checkpoints_with_topic_pruned_entry_ids(
706        &self,
707        region_topics: &[(RegionId, String, bool)],
708    ) -> Result<HashMap<RegionId, ReplayCheckpoint>> {
709        let topic_region_keys = region_topics
710            .iter()
711            .map(|(region_id, topic, _)| TopicRegionKey::new(*region_id, topic))
712            .collect::<Vec<_>>();
713        let topic_region_values = self
714            .table_metadata_manager
715            .topic_region_manager()
716            .batch_get(topic_region_keys)
717            .await
718            .context(error::TableMetadataManagerSnafu)?;
719
720        let topic_name_keys = region_topics
721            .iter()
722            .map(|(_, topic, _)| topic.as_str())
723            .collect::<HashSet<_>>()
724            .into_iter()
725            .map(TopicNameKey::new)
726            .collect::<Vec<_>>();
727        let topic_name_values = self
728            .table_metadata_manager
729            .topic_name_manager()
730            .batch_get(topic_name_keys)
731            .await
732            .context(error::TableMetadataManagerSnafu)?;
733        debug!(
734            "Fetched topic region values: {:?}, topic name values: {:?}",
735            topic_region_values, topic_name_values
736        );
737
738        let replay_checkpoints = region_topics
739            .iter()
740            .filter_map(|(region_id, topic, is_metric_engine)| {
741                let checkpoint = topic_region_values
742                    .get(region_id)
743                    .and_then(|value| value.checkpoint);
744                let pruned_entry_id = topic_name_values
745                    .get(topic)
746                    .map(|value| value.pruned_entry_id);
747
748                ReplayCheckpoint::merge_with_topic_pruned_entry_id(
749                    checkpoint,
750                    pruned_entry_id,
751                    *is_metric_engine,
752                )
753                .map(|checkpoint| (*region_id, checkpoint))
754            })
755            .collect::<HashMap<_, _>>();
756
757        Ok(replay_checkpoints)
758    }
759
760    /// Broadcasts the invalidate table cache message.
761    pub async fn invalidate_table_cache(&self) -> Result<()> {
762        let table_ids = self.region_table_ids();
763        let mut cache_idents = Vec::with_capacity(table_ids.len());
764        for table_id in &table_ids {
765            cache_idents.push(CacheIdent::TableId(*table_id));
766        }
767        // ignore the result
768        let ctx = common_meta::cache_invalidator::Context::default();
769        let _ = self.cache_invalidator.invalidate(&ctx, &cache_idents).await;
770        Ok(())
771    }
772
773    /// Returns the [PersistentContext] of the procedure.
774    pub fn persistent_ctx(&self) -> PersistentContext {
775        self.persistent_ctx.clone()
776    }
777}
778
779#[async_trait::async_trait]
780#[typetag::serde(tag = "region_migration_state")]
781pub(crate) trait State: Sync + Send + Debug {
782    fn name(&self) -> &'static str {
783        let type_name = std::any::type_name::<Self>();
784        // short name
785        type_name.split("::").last().unwrap_or(type_name)
786    }
787
788    /// Yields the next [State] and [Status].
789    async fn next(
790        &mut self,
791        ctx: &mut Context,
792        procedure_ctx: &ProcedureContext,
793    ) -> Result<(Box<dyn State>, Status)>;
794
795    /// Returns as [Any](std::any::Any).
796    fn as_any(&self) -> &dyn Any;
797}
798
799/// Persistent data of [RegionMigrationProcedure].
800#[derive(Debug, Serialize, Deserialize)]
801pub struct RegionMigrationDataOwned {
802    persistent_ctx: PersistentContext,
803    state: Box<dyn State>,
804}
805
806/// Persistent data of [RegionMigrationProcedure].
807#[derive(Debug, Serialize)]
808pub struct RegionMigrationData<'a> {
809    persistent_ctx: &'a PersistentContext,
810    state: &'a dyn State,
811}
812
813pub(crate) struct RegionMigrationProcedure {
814    state: Box<dyn State>,
815    context: Context,
816    _guards: Vec<RegionMigrationProcedureGuard>,
817}
818
819impl RegionMigrationProcedure {
820    const TYPE_NAME: &'static str = "metasrv-procedure::RegionMigration";
821
822    pub fn new(
823        persistent_context: PersistentContext,
824        context_factory: impl ContextFactory,
825        guards: Vec<RegionMigrationProcedureGuard>,
826    ) -> Self {
827        let state = Box::new(RegionMigrationStart {});
828        Self::new_inner(state, persistent_context, context_factory, guards)
829    }
830
831    fn new_inner(
832        state: Box<dyn State>,
833        persistent_context: PersistentContext,
834        context_factory: impl ContextFactory,
835        guards: Vec<RegionMigrationProcedureGuard>,
836    ) -> Self {
837        Self {
838            state,
839            context: context_factory.new_context(persistent_context),
840            _guards: guards,
841        }
842    }
843
844    fn from_json(
845        json: &str,
846        context_factory: impl ContextFactory,
847        tracker: RegionMigrationProcedureTracker,
848    ) -> ProcedureResult<Self> {
849        let RegionMigrationDataOwned {
850            persistent_ctx,
851            state,
852        } = serde_json::from_str(json).context(FromJsonSnafu)?;
853        let guards = persistent_ctx
854            .region_ids
855            .iter()
856            .flat_map(|region_id| {
857                tracker.insert_running_procedure(&RegionMigrationProcedureTask {
858                    region_id: *region_id,
859                    from_peer: persistent_ctx.from_peer.clone(),
860                    to_peer: persistent_ctx.to_peer.clone(),
861                    timeout: persistent_ctx.timeout,
862                    trigger_reason: persistent_ctx.trigger_reason,
863                })
864            })
865            .collect::<Vec<_>>();
866
867        let context = context_factory.new_context(persistent_ctx);
868
869        Ok(Self {
870            state,
871            context,
872            _guards: guards,
873        })
874    }
875
876    async fn rollback_inner(&mut self, procedure_ctx: &ProcedureContext) -> Result<()> {
877        let _timer = METRIC_META_REGION_MIGRATION_EXECUTE
878            .with_label_values(&["rollback"])
879            .start_timer();
880        let ctx = &self.context;
881        let table_regions = ctx.persistent_ctx.table_regions();
882        for (table_id, regions) in table_regions {
883            let table_lock = TableLock::Write(table_id).into();
884            let _guard = procedure_ctx.provider.acquire_lock(&table_lock).await;
885            let table_route = ctx.get_table_route_value(table_id).await?;
886            let region_routes = table_route.region_routes().unwrap();
887            let downgraded = region_routes
888                .iter()
889                .filter(|route| regions.contains(&route.region.id))
890                .any(|route| route.is_leader_downgrading());
891            if downgraded {
892                info!(
893                    "Rollbacking downgraded region leader table route, table: {table_id}, regions: {regions:?}"
894                );
895                let table_metadata_manager = &ctx.table_metadata_manager;
896                table_metadata_manager
897                    .update_leader_region_status(table_id, &table_route, |route| {
898                        if regions.contains(&route.region.id) {
899                            Some(None)
900                        } else {
901                            None
902                        }
903                    })
904                    .await
905                    .context(error::TableMetadataManagerSnafu)
906                    .map_err(BoxedError::new)
907                    .with_context(|_| error::RetryLaterWithSourceSnafu {
908                        reason: format!("Failed to update the table route during the rollback downgraded leader region: {regions:?}"),
909                    })?;
910            }
911        }
912        self.context
913            .deregister_failure_detectors_for_candidate_regions()
914            .await;
915        self.context.register_failure_detectors().await;
916
917        Ok(())
918    }
919}
920
921#[async_trait::async_trait]
922impl Procedure for RegionMigrationProcedure {
923    fn type_name(&self) -> &str {
924        Self::TYPE_NAME
925    }
926
927    async fn rollback(&mut self, ctx: &ProcedureContext) -> ProcedureResult<()> {
928        self.rollback_inner(ctx)
929            .await
930            .map_err(ProcedureError::external)
931    }
932
933    fn rollback_supported(&self) -> bool {
934        true
935    }
936
937    #[tracing::instrument(skip_all, fields(
938        state = %self.state.name(),
939        region_count = self.context.persistent_ctx.region_ids.len(),
940        from_peer = self.context.persistent_ctx.from_peer.id,
941        to_peer = self.context.persistent_ctx.to_peer.id,
942    ))]
943    async fn execute(&mut self, ctx: &ProcedureContext) -> ProcedureResult<Status> {
944        let state = &mut self.state;
945
946        let name = state.name();
947        let _timer = METRIC_META_REGION_MIGRATION_EXECUTE
948            .with_label_values(&[name])
949            .start_timer();
950        match state.next(&mut self.context, ctx).await {
951            Ok((next, status)) => {
952                *state = next;
953                Ok(status)
954            }
955            Err(e) => {
956                if e.is_retryable() {
957                    METRIC_META_REGION_MIGRATION_ERROR
958                        .with_label_values(&[name, "retryable"])
959                        .inc();
960                    Err(ProcedureError::retry_later(e))
961                } else {
962                    // Consumes the opening region guard before deregistering the failure detectors.
963                    self.context.volatile_ctx.opening_region_guards.clear();
964                    self.context
965                        .deregister_failure_detectors_for_candidate_regions()
966                        .await;
967                    error!(
968                        e;
969                        "Region migration procedure failed, regions: {:?}, from_peer: {}, to_peer: {}, {}",
970                        self.context.persistent_ctx.region_ids,
971                        self.context.persistent_ctx.from_peer,
972                        self.context.persistent_ctx.to_peer,
973                        self.context.volatile_ctx.metrics,
974                    );
975                    METRIC_META_REGION_MIGRATION_ERROR
976                        .with_label_values(&[name, "external"])
977                        .inc();
978                    Err(ProcedureError::external(e))
979                }
980            }
981        }
982    }
983
984    fn dump(&self) -> ProcedureResult<String> {
985        let data = RegionMigrationData {
986            state: self.state.as_ref(),
987            persistent_ctx: &self.context.persistent_ctx,
988        };
989        serde_json::to_string(&data).context(ToJsonSnafu)
990    }
991
992    fn lock_key(&self) -> LockKey {
993        LockKey::new(self.context.persistent_ctx.lock_key())
994    }
995
996    fn event(&self, ctx: &EventContext<'_>) -> Option<Box<dyn Event>> {
997        if !ctx.event_type_filter.allows(REGION_MIGRATION_EVENT_TYPE) {
998            return None;
999        }
1000
1001        Some(Box::new(RegionMigrationEvent::from_persistent_ctx(
1002            &self.context.persistent_ctx,
1003            self.context
1004                .trigger_reason(ctx.event_context)
1005                .to_trigger_reason(),
1006        )))
1007    }
1008}
1009
1010#[cfg(test)]
1011mod tests {
1012    use std::assert_matches;
1013    use std::sync::Arc;
1014
1015    use common_meta::distributed_time_constants::default_distributed_time_constants;
1016    use common_meta::instruction::Instruction;
1017    use common_meta::key::test_utils::new_test_table_info;
1018    use common_meta::rpc::router::{Region, RegionRoute};
1019
1020    use super::*;
1021    use crate::handler::HeartbeatMailbox;
1022    use crate::procedure::region_migration::open_candidate_region::OpenCandidateRegion;
1023    use crate::procedure::region_migration::test_util::*;
1024    use crate::procedure::test_util::{
1025        new_downgrade_region_reply, new_flush_region_reply_for_region, new_open_region_reply,
1026        new_upgrade_region_reply,
1027    };
1028    use crate::service::mailbox::Channel;
1029
1030    fn new_persistent_context() -> PersistentContext {
1031        test_util::new_persistent_context(1, 2, RegionId::new(1024, 1))
1032    }
1033
1034    #[test]
1035    fn test_lock_key() {
1036        let persistent_context = new_persistent_context();
1037        let expected_keys = persistent_context.lock_key();
1038
1039        let env = TestingEnv::new();
1040        let context = env.context_factory();
1041
1042        let procedure = RegionMigrationProcedure::new(persistent_context, context, vec![]);
1043
1044        let key = procedure.lock_key();
1045        let keys = key.keys_to_lock().cloned().collect::<Vec<_>>();
1046
1047        for key in expected_keys {
1048            assert!(keys.contains(&key));
1049        }
1050    }
1051
1052    #[test]
1053    fn test_data_serialization() {
1054        let persistent_context = new_persistent_context();
1055
1056        let env = TestingEnv::new();
1057        let context = env.context_factory();
1058
1059        let procedure = RegionMigrationProcedure::new(persistent_context, context, vec![]);
1060
1061        let serialized = procedure.dump().unwrap();
1062        let expected = r#"{"persistent_ctx":{"catalog_and_schema":[["greptime","public"]],"from_peer":{"id":1,"addr":""},"to_peer":{"id":2,"addr":""},"region_ids":[4398046511105],"timeout":"10s","trigger_reason":"Unknown"},"state":{"region_migration_state":"RegionMigrationStart"}}"#;
1063        assert_eq!(expected, serialized);
1064    }
1065
1066    #[test]
1067    fn test_event_hook_uses_current_persistent_context() {
1068        let env = TestingEnv::new();
1069        let procedure =
1070            RegionMigrationProcedure::new(new_persistent_context(), env.context_factory(), vec![]);
1071        let state = common_procedure::ProcedureState::Running;
1072        let triggers = [
1073            common_procedure::EventTrigger::Recovered,
1074            common_procedure::EventTrigger::Retrying {
1075                phase: common_procedure::RetryPhase::Execute,
1076                attempt: 1,
1077            },
1078            common_procedure::EventTrigger::RollingBack,
1079            common_procedure::EventTrigger::Succeeded,
1080            common_procedure::EventTrigger::Failed,
1081            common_procedure::EventTrigger::Poisoned,
1082        ];
1083
1084        for trigger in triggers {
1085            let event = procedure
1086                .event(&EventContext {
1087                    procedure_id: common_procedure::ProcedureId::random(),
1088                    lifecycle_state: &state,
1089                    trigger,
1090                    event_type_filter: Arc::new(common_event_recorder::EventTypeFilter::All),
1091                    event_context: None,
1092                })
1093                .unwrap();
1094            assert_eq!(event.event_type(), "region_migration");
1095            assert_eq!(event.extra_rows().unwrap().len(), 1);
1096        }
1097    }
1098
1099    #[test]
1100    fn test_backward_compatibility() {
1101        let persistent_ctx = PersistentContext {
1102            #[allow(deprecated)]
1103            catalog: Some("greptime".into()),
1104            #[allow(deprecated)]
1105            schema: Some("public".into()),
1106            catalog_and_schema: vec![],
1107            from_peer: Peer::empty(1),
1108            to_peer: Peer::empty(2),
1109            region_ids: vec![RegionId::new(1024, 1)],
1110            timeout: Duration::from_secs(10),
1111            trigger_reason: RegionMigrationTriggerReason::Unknown,
1112        };
1113        // NOTES: Changes it will break backward compatibility.
1114        let serialized = r#"{"catalog":"greptime","schema":"public","from_peer":{"id":1,"addr":""},"to_peer":{"id":2,"addr":""},"region_id":4398046511105}"#;
1115        let deserialized: PersistentContext = serde_json::from_str(serialized).unwrap();
1116
1117        assert_eq!(persistent_ctx, deserialized);
1118    }
1119
1120    #[test]
1121    fn test_legacy_trigger_reason_survives_recovery_and_repersistence() {
1122        let serialized = r#"{"persistent_ctx":{"catalog":"greptime","schema":"public","from_peer":{"id":1,"addr":""},"to_peer":{"id":2,"addr":""},"region_id":4398046511105,"trigger_reason":"Failover"},"state":{"region_migration_state":"RegionMigrationStart"}}"#;
1123        let env = TestingEnv::new();
1124        let procedure = RegionMigrationProcedure::from_json(
1125            serialized,
1126            env.context_factory(),
1127            RegionMigrationProcedureTracker::default(),
1128        )
1129        .unwrap();
1130        assert_eq!(
1131            procedure.context.persistent_ctx.trigger_reason,
1132            RegionMigrationTriggerReason::Failover
1133        );
1134        let repersisted = procedure.dump().unwrap();
1135        assert_eq!(
1136            serde_json::from_str::<serde_json::Value>(&repersisted).unwrap()["persistent_ctx"]["trigger_reason"],
1137            "Failover"
1138        );
1139
1140        let recovered = RegionMigrationProcedure::from_json(
1141            &repersisted,
1142            env.context_factory(),
1143            RegionMigrationProcedureTracker::default(),
1144        )
1145        .unwrap();
1146        assert_eq!(
1147            recovered.context.trigger_reason(None),
1148            RegionMigrationTriggerReason::Failover
1149        );
1150        let event_context = PersistentEventContext::new(TriggerReason::AutoRebalance);
1151        assert_eq!(
1152            recovered.context.trigger_reason(Some(&event_context)),
1153            RegionMigrationTriggerReason::AutoRebalance
1154        );
1155
1156        let state = common_procedure::ProcedureState::Running;
1157        let event = recovered
1158            .event(&EventContext {
1159                procedure_id: common_procedure::ProcedureId::random(),
1160                lifecycle_state: &state,
1161                trigger: common_procedure::EventTrigger::Recovered,
1162                event_type_filter: Arc::new(common_event_recorder::EventTypeFilter::All),
1163                event_context: None,
1164            })
1165            .unwrap();
1166        assert_eq!(
1167            event.extra_rows().unwrap()[0].values[3].value_data,
1168            Some(api::v1::value::ValueData::StringValue(
1169                "Failover".to_string()
1170            ))
1171        );
1172    }
1173
1174    #[test]
1175    fn test_migration_reason_uses_event_context() {
1176        let env = TestingEnv::new();
1177        let context = env.context_factory().new_context(new_persistent_context());
1178        let event_context = PersistentEventContext::new(TriggerReason::RegionFailover);
1179
1180        assert_eq!(
1181            context.trigger_reason(Some(&event_context)),
1182            RegionMigrationTriggerReason::Failover
1183        );
1184
1185        let procedure =
1186            RegionMigrationProcedure::new(new_persistent_context(), env.context_factory(), vec![]);
1187        let state = common_procedure::ProcedureState::Running;
1188        let event = procedure
1189            .event(&EventContext {
1190                procedure_id: common_procedure::ProcedureId::random(),
1191                lifecycle_state: &state,
1192                trigger: common_procedure::EventTrigger::Submitted,
1193                event_type_filter: Arc::new(common_event_recorder::EventTypeFilter::All),
1194                event_context: Some(&event_context),
1195            })
1196            .unwrap();
1197        assert_eq!(
1198            event.extra_rows().unwrap()[0].values[3].value_data,
1199            Some(api::v1::value::ValueData::StringValue(
1200                "Failover".to_string()
1201            ))
1202        );
1203    }
1204
1205    #[derive(Debug, Serialize, Deserialize, Default)]
1206    pub struct MockState;
1207
1208    #[async_trait::async_trait]
1209    #[typetag::serde]
1210    impl State for MockState {
1211        async fn next(
1212            &mut self,
1213            _ctx: &mut Context,
1214            _procedure_ctx: &ProcedureContext,
1215        ) -> Result<(Box<dyn State>, Status)> {
1216            Ok((Box::new(MockState), Status::done()))
1217        }
1218
1219        fn as_any(&self) -> &dyn Any {
1220            self
1221        }
1222    }
1223
1224    #[tokio::test]
1225    async fn test_execution_after_deserialized() {
1226        let env = TestingEnv::new();
1227
1228        fn new_mock_procedure(env: &TestingEnv) -> RegionMigrationProcedure {
1229            let persistent_context = new_persistent_context();
1230            let context_factory = env.context_factory();
1231            let state = Box::<MockState>::default();
1232            RegionMigrationProcedure::new_inner(state, persistent_context, context_factory, vec![])
1233        }
1234
1235        let ctx = TestingEnv::procedure_context();
1236        let mut procedure = new_mock_procedure(&env);
1237        let mut status = None;
1238        for _ in 0..3 {
1239            status = Some(procedure.execute(&ctx).await.unwrap());
1240        }
1241        assert!(status.unwrap().is_done());
1242
1243        let ctx = TestingEnv::procedure_context();
1244        let mut procedure = new_mock_procedure(&env);
1245
1246        status = Some(procedure.execute(&ctx).await.unwrap());
1247
1248        let serialized = procedure.dump().unwrap();
1249
1250        let context_factory = env.context_factory();
1251        let tracker = env.tracker();
1252        let mut procedure =
1253            RegionMigrationProcedure::from_json(&serialized, context_factory, tracker.clone())
1254                .unwrap();
1255        for region_id in &procedure.context.persistent_ctx.region_ids {
1256            assert!(tracker.contains(*region_id));
1257        }
1258
1259        for _ in 1..3 {
1260            status = Some(procedure.execute(&ctx).await.unwrap());
1261        }
1262        assert!(status.unwrap().is_done());
1263    }
1264
1265    #[tokio::test]
1266    async fn test_broadcast_invalidate_table_cache() {
1267        let mut env = TestingEnv::new();
1268        let persistent_context = test_util::new_persistent_context(1, 2, RegionId::new(1024, 1));
1269        let ctx = env.context_factory().new_context(persistent_context);
1270        let mailbox_ctx = env.mailbox_context();
1271
1272        // No receivers.
1273        ctx.invalidate_table_cache().await.unwrap();
1274
1275        let (tx, mut rx) = tokio::sync::mpsc::channel(1);
1276
1277        mailbox_ctx
1278            .insert_heartbeat_response_receiver(Channel::Frontend(1), tx)
1279            .await;
1280
1281        ctx.invalidate_table_cache().await.unwrap();
1282
1283        let resp = rx.recv().await.unwrap().unwrap();
1284        let msg = resp.mailbox_message.unwrap();
1285
1286        let instruction = HeartbeatMailbox::json_instruction(&msg).unwrap();
1287        assert_eq!(
1288            instruction,
1289            Instruction::InvalidateCaches(vec![CacheIdent::TableId(1024)])
1290        );
1291    }
1292
1293    fn procedure_flow_steps(from_peer_id: u64, to_peer_id: u64) -> Vec<Step> {
1294        vec![
1295            // MigrationStart
1296            Step::next(
1297                "Should be the open candidate region",
1298                None,
1299                Assertion::simple(assert_open_candidate_region, assert_need_persist),
1300            ),
1301            // OpenCandidateRegion
1302            Step::next(
1303                "Should be the flush leader region",
1304                Some(mock_datanode_reply(
1305                    to_peer_id,
1306                    Arc::new(|id| Ok(new_open_region_reply(id, true, None))),
1307                )),
1308                Assertion::simple(assert_flush_leader_region, assert_no_persist),
1309            ),
1310            // Flush Leader Region
1311            Step::next(
1312                "Should be the flush leader region",
1313                Some(mock_datanode_reply(
1314                    from_peer_id,
1315                    Arc::new(move |id| {
1316                        Ok(new_flush_region_reply_for_region(
1317                            id,
1318                            RegionId::new(1024, 1),
1319                            true,
1320                            None,
1321                        ))
1322                    }),
1323                )),
1324                Assertion::simple(assert_update_metadata_downgrade, assert_no_persist),
1325            ),
1326            // UpdateMetadata::Downgrade
1327            Step::next(
1328                "Should be the downgrade leader region",
1329                None,
1330                Assertion::simple(assert_downgrade_leader_region, assert_no_persist),
1331            ),
1332            // Downgrade Candidate
1333            Step::next(
1334                "Should be the upgrade candidate region",
1335                Some(mock_datanode_reply(
1336                    from_peer_id,
1337                    Arc::new(|id| Ok(new_downgrade_region_reply(id, None, true, None))),
1338                )),
1339                Assertion::simple(assert_upgrade_candidate_region, assert_no_persist),
1340            ),
1341            // Upgrade Candidate
1342            Step::next(
1343                "Should be the update metadata for upgrading",
1344                Some(mock_datanode_reply(
1345                    to_peer_id,
1346                    Arc::new(|id| Ok(new_upgrade_region_reply(id, true, true, None))),
1347                )),
1348                Assertion::simple(assert_update_metadata_upgrade, assert_no_persist),
1349            ),
1350            // UpdateMetadata::Upgrade
1351            Step::next(
1352                "Should be the close downgraded region",
1353                None,
1354                Assertion::simple(assert_close_downgraded_region, assert_no_persist),
1355            ),
1356            // CloseDowngradedRegion
1357            Step::next(
1358                "Should be the region migration end",
1359                None,
1360                Assertion::simple(assert_region_migration_end, assert_done),
1361            ),
1362            // RegionMigrationEnd
1363            Step::next(
1364                "Should be the region migration end again",
1365                None,
1366                Assertion::simple(assert_region_migration_end, assert_done),
1367            ),
1368        ]
1369    }
1370
1371    #[tokio::test]
1372    async fn test_procedure_flow() {
1373        common_telemetry::init_default_ut_logging();
1374
1375        let persistent_context = test_util::new_persistent_context(1, 2, RegionId::new(1024, 1));
1376        let state = Box::new(RegionMigrationStart);
1377
1378        // The table metadata.
1379        let from_peer_id = persistent_context.from_peer.id;
1380        let to_peer_id = persistent_context.to_peer.id;
1381        let from_peer = persistent_context.from_peer.clone();
1382        let to_peer = persistent_context.to_peer.clone();
1383        let region_id = persistent_context.region_ids[0];
1384        let table_info = new_test_table_info(1024);
1385        let region_routes = vec![RegionRoute {
1386            region: Region::new_test(region_id),
1387            leader_peer: Some(from_peer),
1388            follower_peers: vec![to_peer],
1389            ..Default::default()
1390        }];
1391
1392        let suite = ProcedureMigrationTestSuite::new(persistent_context, state);
1393        suite.init_table_metadata(table_info, region_routes).await;
1394
1395        let steps = procedure_flow_steps(from_peer_id, to_peer_id);
1396        let timer = Instant::now();
1397
1398        // Run the table tests.
1399        let runner = ProcedureMigrationSuiteRunner::new(suite)
1400            .steps(steps)
1401            .run_once()
1402            .await;
1403
1404        let region_lease = default_distributed_time_constants().region_lease.as_secs();
1405
1406        // Ensure it didn't run into the slow path.
1407        assert!(timer.elapsed().as_secs() < region_lease / 2);
1408
1409        runner.suite.verify_table_metadata().await;
1410    }
1411
1412    #[tokio::test]
1413    async fn test_procedure_flow_open_candidate_region_retryable_error() {
1414        common_telemetry::init_default_ut_logging();
1415
1416        let persistent_context = test_util::new_persistent_context(1, 2, RegionId::new(1024, 1));
1417        let state = Box::new(RegionMigrationStart);
1418
1419        // The table metadata.
1420        let to_peer_id = persistent_context.to_peer.id;
1421        let from_peer = persistent_context.from_peer.clone();
1422        let region_id = persistent_context.region_ids[0];
1423        let table_info = new_test_table_info(1024);
1424        let region_routes = vec![RegionRoute {
1425            region: Region::new_test(region_id),
1426            leader_peer: Some(from_peer),
1427            follower_peers: vec![],
1428            ..Default::default()
1429        }];
1430
1431        let suite = ProcedureMigrationTestSuite::new(persistent_context, state);
1432        suite.init_table_metadata(table_info, region_routes).await;
1433
1434        let steps = vec![
1435            // Migration Start
1436            Step::next(
1437                "Should be the open candidate region",
1438                None,
1439                Assertion::simple(assert_open_candidate_region, assert_need_persist),
1440            ),
1441            // OpenCandidateRegion
1442            Step::next(
1443                "Should be throwing a non-retry error",
1444                Some(mock_datanode_reply(
1445                    to_peer_id,
1446                    Arc::new(|id| error::MailboxTimeoutSnafu { id }.fail()),
1447                )),
1448                Assertion::error(|error| assert!(error.is_retryable())),
1449            ),
1450            // OpenCandidateRegion
1451            Step::next(
1452                "Should be throwing a non-retry error again",
1453                Some(mock_datanode_reply(
1454                    to_peer_id,
1455                    Arc::new(|id| error::MailboxTimeoutSnafu { id }.fail()),
1456                )),
1457                Assertion::error(|error| assert!(error.is_retryable())),
1458            ),
1459        ];
1460
1461        let setup_to_latest_persisted_state = Step::setup(
1462            "Sets state to UpdateMetadata::Downgrade",
1463            merge_before_test_fn(vec![
1464                setup_state(Arc::new(|| Box::new(OpenCandidateRegion))),
1465                Arc::new(reset_volatile_ctx),
1466            ]),
1467        );
1468
1469        let steps = [
1470            steps.clone(),
1471            // Mocks the volatile ctx lost(i.g., Meta leader restarts).
1472            vec![setup_to_latest_persisted_state.clone()],
1473            steps.clone()[1..].to_vec(),
1474            vec![setup_to_latest_persisted_state],
1475            steps.clone()[1..].to_vec(),
1476        ]
1477        .concat();
1478
1479        // Run the table tests.
1480        let runner = ProcedureMigrationSuiteRunner::new(suite)
1481            .steps(steps.clone())
1482            .run_once()
1483            .await;
1484
1485        let table_routes_version = runner
1486            .env()
1487            .table_metadata_manager()
1488            .table_route_manager()
1489            .table_route_storage()
1490            .get(region_id.table_id())
1491            .await
1492            .unwrap()
1493            .unwrap()
1494            .version();
1495        // Should be unchanged.
1496        assert_eq!(table_routes_version.unwrap(), 0);
1497    }
1498
1499    #[tokio::test]
1500    async fn test_procedure_flow_upgrade_candidate_with_retry_and_failed() {
1501        common_telemetry::init_default_ut_logging();
1502
1503        let persistent_context = test_util::new_persistent_context(1, 2, RegionId::new(1024, 1));
1504        let state = Box::new(RegionMigrationStart);
1505
1506        // The table metadata.
1507        let from_peer_id = persistent_context.from_peer.id;
1508        let to_peer_id = persistent_context.to_peer.id;
1509        let from_peer = persistent_context.from_peer.clone();
1510        let region_id = persistent_context.region_ids[0];
1511        let table_info = new_test_table_info(1024);
1512        let region_routes = vec![RegionRoute {
1513            region: Region::new_test(region_id),
1514            leader_peer: Some(from_peer),
1515            follower_peers: vec![],
1516            ..Default::default()
1517        }];
1518
1519        let suite = ProcedureMigrationTestSuite::new(persistent_context, state);
1520        suite.init_table_metadata(table_info, region_routes).await;
1521
1522        let steps = vec![
1523            // MigrationStart
1524            Step::next(
1525                "Should be the open candidate region",
1526                None,
1527                Assertion::simple(assert_open_candidate_region, assert_need_persist),
1528            ),
1529            // OpenCandidateRegion
1530            Step::next(
1531                "Should be the flush leader region",
1532                Some(mock_datanode_reply(
1533                    to_peer_id,
1534                    Arc::new(|id| Ok(new_open_region_reply(id, true, None))),
1535                )),
1536                Assertion::simple(assert_flush_leader_region, assert_no_persist),
1537            ),
1538            // Flush Leader Region
1539            Step::next(
1540                "Should be the flush leader region",
1541                Some(mock_datanode_reply(
1542                    from_peer_id,
1543                    Arc::new(move |id| {
1544                        Ok(new_flush_region_reply_for_region(
1545                            id,
1546                            RegionId::new(1024, 1),
1547                            true,
1548                            None,
1549                        ))
1550                    }),
1551                )),
1552                Assertion::simple(assert_update_metadata_downgrade, assert_no_persist),
1553            ),
1554            // UpdateMetadata::Downgrade
1555            Step::next(
1556                "Should be the downgrade leader region",
1557                None,
1558                Assertion::simple(assert_downgrade_leader_region, assert_no_persist),
1559            ),
1560            // Downgrade Candidate
1561            Step::next(
1562                "Should be the upgrade candidate region",
1563                Some(mock_datanode_reply(
1564                    from_peer_id,
1565                    Arc::new(|id| Ok(new_downgrade_region_reply(id, None, true, None))),
1566                )),
1567                Assertion::simple(assert_upgrade_candidate_region, assert_no_persist),
1568            ),
1569            // Upgrade Candidate
1570            Step::next(
1571                "Should be the rollback metadata",
1572                Some(mock_datanode_reply(
1573                    to_peer_id,
1574                    Arc::new(|id| error::MailboxTimeoutSnafu { id }.fail()),
1575                )),
1576                Assertion::simple(assert_update_metadata_rollback, assert_no_persist),
1577            ),
1578            // UpdateMetadata::Rollback
1579            Step::next(
1580                "Should be the region migration abort",
1581                None,
1582                Assertion::simple(assert_region_migration_abort, assert_no_persist),
1583            ),
1584            // RegionMigrationAbort
1585            Step::next(
1586                "Should throw an error",
1587                None,
1588                Assertion::error(|error| {
1589                    assert!(!error.is_retryable());
1590                    assert_matches!(error, error::Error::MigrationAbort { .. });
1591                }),
1592            ),
1593        ];
1594
1595        let setup_to_latest_persisted_state = Step::setup(
1596            "Sets state to OpenCandidateRegion",
1597            merge_before_test_fn(vec![
1598                setup_state(Arc::new(|| Box::new(OpenCandidateRegion))),
1599                Arc::new(reset_volatile_ctx),
1600            ]),
1601        );
1602
1603        let steps = [
1604            steps.clone(),
1605            vec![setup_to_latest_persisted_state.clone()],
1606            steps.clone()[1..].to_vec(),
1607            vec![setup_to_latest_persisted_state],
1608            steps.clone()[1..].to_vec(),
1609        ]
1610        .concat();
1611
1612        // Run the table tests.
1613        ProcedureMigrationSuiteRunner::new(suite)
1614            .steps(steps.clone())
1615            .run_once()
1616            .await;
1617    }
1618
1619    #[tokio::test]
1620    async fn test_procedure_flow_upgrade_candidate_with_retry() {
1621        common_telemetry::init_default_ut_logging();
1622
1623        let persistent_context = test_util::new_persistent_context(1, 2, RegionId::new(1024, 1));
1624        let state = Box::new(RegionMigrationStart);
1625
1626        // The table metadata.
1627        let to_peer_id = persistent_context.to_peer.id;
1628        let from_peer_id = persistent_context.from_peer.id;
1629        let from_peer = persistent_context.from_peer.clone();
1630        let region_id = persistent_context.region_ids[0];
1631        let table_info = new_test_table_info(1024);
1632        let region_routes = vec![RegionRoute {
1633            region: Region::new_test(region_id),
1634            leader_peer: Some(from_peer),
1635            follower_peers: vec![],
1636            ..Default::default()
1637        }];
1638
1639        let suite = ProcedureMigrationTestSuite::new(persistent_context, state);
1640        suite.init_table_metadata(table_info, region_routes).await;
1641
1642        let steps = vec![
1643            // Migration Start
1644            Step::next(
1645                "Should be the open candidate region",
1646                None,
1647                Assertion::simple(assert_open_candidate_region, assert_need_persist),
1648            ),
1649            // OpenCandidateRegion
1650            Step::next(
1651                "Should be throwing a retryable error",
1652                Some(mock_datanode_reply(
1653                    to_peer_id,
1654                    Arc::new(|id| {
1655                        Ok(new_open_region_reply(
1656                            id,
1657                            false,
1658                            Some("mock retryable open region error".to_string()),
1659                        ))
1660                    }),
1661                )),
1662                Assertion::error(|error| assert!(error.is_retryable(), "err: {error:?}")),
1663            ),
1664            // OpenCandidateRegion
1665            Step::next(
1666                "Should be the update metadata for downgrading",
1667                Some(mock_datanode_reply(
1668                    to_peer_id,
1669                    Arc::new(|id| Ok(new_open_region_reply(id, true, None))),
1670                )),
1671                Assertion::simple(assert_flush_leader_region, assert_no_persist),
1672            ),
1673            // Flush Leader Region
1674            Step::next(
1675                "Should be the flush leader region",
1676                Some(mock_datanode_reply(
1677                    from_peer_id,
1678                    Arc::new(move |id| {
1679                        Ok(new_flush_region_reply_for_region(id, region_id, true, None))
1680                    }),
1681                )),
1682                Assertion::simple(assert_update_metadata_downgrade, assert_no_persist),
1683            ),
1684            // UpdateMetadata::Downgrade
1685            Step::next(
1686                "Should be the downgrade leader region",
1687                None,
1688                Assertion::simple(assert_downgrade_leader_region, assert_no_persist),
1689            ),
1690            // Downgrade Leader
1691            Step::next(
1692                "Should be the upgrade candidate region",
1693                Some(mock_datanode_reply(
1694                    from_peer_id,
1695                    merge_mailbox_messages(vec![
1696                        Arc::new(|id| error::MailboxTimeoutSnafu { id }.fail()),
1697                        Arc::new(|id| Ok(new_downgrade_region_reply(id, None, true, None))),
1698                    ]),
1699                )),
1700                Assertion::simple(assert_upgrade_candidate_region, assert_no_persist),
1701            ),
1702            // Upgrade Candidate
1703            Step::next(
1704                "Should be the update metadata for upgrading",
1705                Some(mock_datanode_reply(
1706                    to_peer_id,
1707                    merge_mailbox_messages(vec![
1708                        Arc::new(|id| error::MailboxTimeoutSnafu { id }.fail()),
1709                        Arc::new(|id| Ok(new_upgrade_region_reply(id, true, true, None))),
1710                    ]),
1711                )),
1712                Assertion::simple(assert_update_metadata_upgrade, assert_no_persist),
1713            ),
1714            // UpdateMetadata::Upgrade
1715            Step::next(
1716                "Should be the close downgraded region",
1717                None,
1718                Assertion::simple(assert_close_downgraded_region, assert_no_persist),
1719            ),
1720            // CloseDowngradedRegion
1721            Step::next(
1722                "Should be the region migration end",
1723                None,
1724                Assertion::simple(assert_region_migration_end, assert_done),
1725            ),
1726            // RegionMigrationEnd
1727            Step::next(
1728                "Should be the region migration end again",
1729                None,
1730                Assertion::simple(assert_region_migration_end, assert_done),
1731            ),
1732            // RegionMigrationStart
1733            Step::setup(
1734                "Sets state to RegionMigrationStart",
1735                merge_before_test_fn(vec![
1736                    setup_state(Arc::new(|| Box::new(RegionMigrationStart))),
1737                    Arc::new(reset_volatile_ctx),
1738                ]),
1739            ),
1740            // RegionMigrationEnd
1741            // Note: We can't run this test multiple times;
1742            // the `peer_id`'s `DatanodeTable` will be removed after first-time migration success.
1743            Step::next(
1744                "Should be the region migration end(has been migrated)",
1745                None,
1746                Assertion::simple(assert_region_migration_end, assert_done),
1747            ),
1748        ];
1749
1750        let steps = [steps.clone()].concat();
1751        let timer = Instant::now();
1752
1753        // Run the table tests.
1754        let runner = ProcedureMigrationSuiteRunner::new(suite)
1755            .steps(steps.clone())
1756            .run_once()
1757            .await;
1758
1759        let region_lease = default_distributed_time_constants().region_lease.as_secs();
1760        // Ensure it didn't run into the slow path.
1761        assert!(timer.elapsed().as_secs() < region_lease);
1762        runner.suite.verify_table_metadata().await;
1763    }
1764}