Skip to main content

meta_srv/procedure/
drain_datanode.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 check_drained;
16pub(crate) mod dispatch;
17pub(crate) mod drain_datanode_end;
18pub(crate) mod drain_datanode_start;
19pub(crate) mod manager;
20mod migration;
21pub(crate) mod utils;
22
23#[cfg(test)]
24mod test_util;
25
26use std::any::Any;
27use std::fmt::Debug;
28use std::time::Duration;
29
30use common_meta::DatanodeId;
31use common_meta::key::TableMetadataManagerRef;
32use common_procedure::error::{FromJsonSnafu, ToJsonSnafu};
33use common_procedure::{
34    Context as ProcedureContext, Error as ProcedureError, LockKey, Procedure, ProcedureId,
35    Result as ProcedureResult, Status,
36};
37use common_telemetry::{error, info};
38use serde::{Deserialize, Serialize};
39use snafu::ResultExt;
40use store_api::storage::RegionId;
41
42use crate::error::{self, Result};
43use crate::metasrv::SelectorContext;
44use crate::node_scheduling::NodeSchedulingManagerRef;
45use crate::procedure::drain_datanode::drain_datanode_start::DrainDatanodeStart;
46use crate::procedure::drain_datanode::manager::{
47    DrainDatanodeProcedureGuard, DrainDatanodeProcedureTracker,
48};
49use crate::procedure::region_migration::manager::RegionMigrationManagerRef;
50
51/// The default number of leader regions migrated concurrently.
52pub const DEFAULT_PARALLELISM: usize = 4;
53/// The maximum number of leader regions migrated concurrently.
54pub const MAX_PARALLELISM: usize = 32;
55/// The default budget of the whole drain.
56pub const DEFAULT_DRAIN_TIMEOUT: Duration = Duration::from_secs(20 * 60);
57/// The maximum number of plan build attempts, including the first scan and failed reads.
58pub const MAX_RESCAN_ROUNDS: u32 = 3;
59
60/// The strategy of a drain run when a region cannot be migrated.
61#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
62pub enum DrainDatanodeStrategy {
63    /// Aborts the procedure once every migration of the batch is settled and at least one
64    /// of them failed.
65    FailFast,
66    /// Drains every remaining region and records the failed ones.
67    #[default]
68    TryAll,
69}
70
71/// The regions placed on a datanode, split by role.
72#[derive(Debug, Clone, Default, PartialEq, Eq)]
73pub struct DrainDatanodePlan {
74    /// The leader regions still to migrate, sorted and deduplicated.
75    pub leader_regions: Vec<RegionId>,
76    /// The follower regions of the datanode, sorted and deduplicated.
77    pub follower_regions: Vec<RegionId>,
78}
79
80/// The persistent data of [`DrainDatanodeProcedure`].
81///
82/// Only what cannot be derived from the cluster metadata is persisted: the plan and the
83/// progress of a run are rebuilt from the authoritative table routes after a restart.
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct PersistentContext {
86    /// The datanode to drain.
87    pub node_id: DatanodeId,
88    /// The pinned destination of leader regions. Resolved by the selector when `None`.
89    pub target_node_id: Option<DatanodeId>,
90    /// The number of leader regions migrated concurrently.
91    pub parallelism: usize,
92    /// The error handling strategy.
93    pub strategy: DrainDatanodeStrategy,
94    /// The absolute deadline of the whole drain, in milliseconds since the Unix epoch.
95    pub deadline_ms: i64,
96}
97
98/// A dispatched migration sub-procedure that is not settled yet.
99#[derive(Debug, Clone, Copy)]
100pub struct SubprocedureMeta {
101    /// The region migrated by the sub-procedure.
102    pub region_id: RegionId,
103    /// The procedure id of the sub-procedure.
104    pub procedure_id: ProcedureId,
105}
106
107/// The in-memory data of [`DrainDatanodeProcedure`].
108///
109/// It is not persisted: a restart rebuilds the plan by rescanning the datanode, which is
110/// bounded by [`MAX_RESCAN_ROUNDS`], and re-dispatches the regions that are still led by
111/// the drained datanode and have no running migration. Recovered migrations own their
112/// lifecycle and may finish after this drain procedure ends.
113#[derive(Debug, Clone, Default)]
114pub struct VolatileContext {
115    /// The drain plan, `None` until it is built by the first state that runs.
116    ///
117    /// [`DrainDatanodePlan::leader_regions`] is the remaining work: a region leaves it when
118    /// its migration succeeded, when it was given up by [`DrainDatanodeStrategy::TryAll`],
119    /// when the drained datanode stopped leading it, or when another migration owns it.
120    pub plan: Option<DrainDatanodePlan>,
121    /// The migration sub-procedures of the current batch that are not settled yet.
122    pub inflight_subprocedures: Vec<SubprocedureMeta>,
123    /// The number of regions confirmed to have left the source while collecting batch results.
124    pub migrated: usize,
125    /// The regions given up by [`DrainDatanodeStrategy::TryAll`] in this run.
126    pub failed_regions: Vec<RegionId>,
127    /// The number of plan build attempts in this run, including failed reads.
128    pub rescan_round: u32,
129}
130
131/// The task of [`DrainDatanodeProcedure`].
132#[derive(Debug, Clone)]
133pub struct DrainDatanodeProcedureTask {
134    /// The datanode to drain.
135    pub node_id: DatanodeId,
136    /// The pinned destination of leader regions. Resolved by the selector when `None`.
137    pub target_node_id: Option<DatanodeId>,
138    /// The number of leader regions migrated concurrently.
139    pub parallelism: usize,
140    /// The error handling strategy.
141    pub strategy: DrainDatanodeStrategy,
142    /// The budget of the whole drain. `None` uses [`DEFAULT_DRAIN_TIMEOUT`].
143    pub timeout: Option<Duration>,
144}
145
146/// The context of [`DrainDatanodeProcedure`].
147#[derive(Clone)]
148pub struct Context {
149    /// The persistent data of the procedure.
150    pub persistent_ctx: PersistentContext,
151    /// The in-memory data of the procedure.
152    pub volatile_ctx: VolatileContext,
153    /// The table metadata manager.
154    pub table_metadata_manager: TableMetadataManagerRef,
155    /// The node scheduling manager, used to fence the drained datanode.
156    pub node_scheduling_manager: NodeSchedulingManagerRef,
157    /// The region migration manager, used to build migration sub-procedures.
158    pub region_migration_manager: RegionMigrationManagerRef,
159    /// The selector context, used to resolve the destination of a leader region.
160    pub selector_ctx: SelectorContext,
161}
162
163impl Context {
164    /// Builds the context of a run.
165    ///
166    /// The per-run state starts empty: the plan and the progress are rebuilt from the
167    /// table routes when the procedure needs them.
168    pub(crate) fn new(
169        table_metadata_manager: TableMetadataManagerRef,
170        node_scheduling_manager: NodeSchedulingManagerRef,
171        region_migration_manager: RegionMigrationManagerRef,
172        selector_ctx: SelectorContext,
173
174        persistent_ctx: PersistentContext,
175    ) -> Self {
176        Self {
177            persistent_ctx,
178            volatile_ctx: VolatileContext::default(),
179            table_metadata_manager,
180            node_scheduling_manager,
181            region_migration_manager,
182            selector_ctx,
183        }
184    }
185}
186
187impl Context {
188    /// Rebuilds the drain plan from the authoritative table routes.
189    ///
190    /// Builds are bounded by [`MAX_RESCAN_ROUNDS`]: a datanode whose leader regions keep
191    /// coming back must not be rescanned forever.
192    pub(crate) async fn rebuild_plan(&mut self) -> Result<()> {
193        let node_id = self.persistent_ctx.node_id;
194        if self.volatile_ctx.rescan_round >= MAX_RESCAN_ROUNDS {
195            return error::DrainDatanodeRescanExceededSnafu {
196                node_id,
197                max_rounds: MAX_RESCAN_ROUNDS,
198            }
199            .fail();
200        }
201
202        self.volatile_ctx.rescan_round += 1;
203        let plan = utils::build_datanode_plan(&self.table_metadata_manager, node_id).await?;
204        self.volatile_ctx.plan = Some(plan);
205
206        Ok(())
207    }
208
209    /// Removes `region_id` from the current plan. A later scan may add it back.
210    pub(crate) fn forget_leader_region(&mut self, region_id: RegionId) {
211        if let Some(plan) = self.volatile_ctx.plan.as_mut() {
212            plan.leader_regions.retain(|region| *region != region_id);
213        }
214    }
215}
216
217impl VolatileContext {
218    /// Returns the leader regions still to migrate.
219    pub(crate) fn leader_regions(&self) -> &[RegionId] {
220        self.plan
221            .as_ref()
222            .map_or(&[], |plan| plan.leader_regions.as_slice())
223    }
224
225    /// Returns the follower regions collected by the last plan build.
226    pub(crate) fn follower_regions(&self) -> &[RegionId] {
227        self.plan
228            .as_ref()
229            .map_or(&[], |plan| plan.follower_regions.as_slice())
230    }
231}
232
233/// The persistent data of [`DrainDatanodeProcedure`] as read by `from_json`.
234#[derive(Debug, Deserialize)]
235pub struct DrainDatanodeDataOwned {
236    persistent_ctx: PersistentContext,
237    state: Box<dyn State>,
238}
239
240/// The persistent data of [`DrainDatanodeProcedure`] as written by `dump`.
241#[derive(Debug, Serialize)]
242pub struct DrainDatanodeData<'a> {
243    persistent_ctx: &'a PersistentContext,
244    state: &'a dyn State,
245}
246
247/// A state of [`DrainDatanodeProcedure`].
248#[async_trait::async_trait]
249#[typetag::serde(tag = "drain_datanode_state")]
250pub(crate) trait State: Sync + Send + Debug {
251    /// Returns the short name of this state.
252    fn name(&self) -> &'static str {
253        let type_name = std::any::type_name::<Self>();
254        type_name.split("::").last().unwrap_or(type_name)
255    }
256
257    /// Yields the next [`State`] and [`Status`].
258    async fn next(
259        &mut self,
260        ctx: &mut Context,
261        procedure_ctx: &ProcedureContext,
262    ) -> Result<(Box<dyn State>, Status)>;
263
264    /// Returns as [`Any`].
265    fn as_any(&self) -> &dyn Any;
266}
267
268/// The procedure draining a datanode.
269///
270/// It fences the datanode, migrates every leader region off it through
271/// [`RegionMigrationProcedure`](crate::procedure::region_migration::RegionMigrationProcedure)
272/// sub-procedures, and rescans for regions that still need a new migration. Migrations
273/// already registered in the tracker continue independently and may outlive this
274/// procedure. Follower regions are collected but not removed yet.
275pub(crate) struct DrainDatanodeProcedure {
276    state: Box<dyn State>,
277    context: Context,
278    /// Keeps the drained datanode in the running set of the manager until the procedure ends,
279    /// so a second drain of the datanode is rejected at submission instead of being queued.
280    _guards: Vec<DrainDatanodeProcedureGuard>,
281}
282
283impl DrainDatanodeProcedure {
284    const TYPE_NAME: &'static str = "metasrv-procedure::DrainDatanode";
285
286    /// Returns a new [`DrainDatanodeProcedure`] in its initial state.
287    pub fn new(context: Context, guards: Vec<DrainDatanodeProcedureGuard>) -> Self {
288        Self {
289            state: Box::new(DrainDatanodeStart),
290            context,
291            _guards: guards,
292        }
293    }
294
295    /// Returns a new [`DrainDatanodeProcedure`] from the given serialized data.
296    ///
297    /// The procedure takes its place back in `tracker`, so a reloaded drain still rejects a
298    /// second drain of the same datanode.
299    pub fn from_json<F>(
300        json: &str,
301        tracker: &DrainDatanodeProcedureTracker,
302        ctx_factory: F,
303    ) -> ProcedureResult<Self>
304    where
305        F: FnOnce(PersistentContext) -> Context,
306    {
307        let DrainDatanodeDataOwned {
308            persistent_ctx,
309            state,
310        } = serde_json::from_str(json).context(FromJsonSnafu)?;
311        let guards = tracker
312            .insert_running_procedure(&DrainDatanodeProcedureTask {
313                node_id: persistent_ctx.node_id,
314                target_node_id: persistent_ctx.target_node_id,
315                parallelism: persistent_ctx.parallelism,
316                strategy: persistent_ctx.strategy,
317                timeout: None,
318            })
319            .into_iter()
320            .collect();
321
322        let context = ctx_factory(persistent_ctx);
323        Ok(Self {
324            state,
325            context,
326            _guards: guards,
327        })
328    }
329}
330
331#[async_trait::async_trait]
332impl Procedure for DrainDatanodeProcedure {
333    fn type_name(&self) -> &str {
334        Self::TYPE_NAME
335    }
336
337    #[tracing::instrument(skip_all, fields(
338        state = %self.state.name(),
339        node_id = %self.context.persistent_ctx.node_id
340    ))]
341    async fn execute(&mut self, ctx: &ProcedureContext) -> ProcedureResult<Status> {
342        let state_name = self.state.name();
343        info!(
344            "Drain datanode procedure executing state: {}, node_id: {}",
345            state_name, self.context.persistent_ctx.node_id
346        );
347        match self.state.next(&mut self.context, ctx).await {
348            Ok((next, status)) => {
349                self.state = next;
350                Ok(status)
351            }
352            Err(e) => {
353                if e.is_retryable() {
354                    Err(ProcedureError::retry_later(e))
355                } else {
356                    error!(
357                        e;
358                        "Drain datanode procedure failed, node_id: {}",
359                        self.context.persistent_ctx.node_id
360                    );
361                    Err(ProcedureError::external(e))
362                }
363            }
364        }
365    }
366
367    fn dump(&self) -> ProcedureResult<String> {
368        let data = DrainDatanodeData {
369            persistent_ctx: &self.context.persistent_ctx,
370            state: self.state.as_ref(),
371        };
372        serde_json::to_string(&data).context(ToJsonSnafu)
373    }
374
375    fn lock_key(&self) -> LockKey {
376        LockKey::single_exclusive(format!(
377            "__drain_datanode/{}",
378            self.context.persistent_ctx.node_id
379        ))
380    }
381}
382
383#[cfg(test)]
384mod tests {
385    use std::time::Duration;
386
387    use common_error::ext::ErrorExt;
388    use common_error::status_code::StatusCode;
389    use common_meta::state_store::KvStateStore;
390    use common_procedure::store::ProcedureMessage;
391    use common_procedure::store::state_store::StateStore;
392    use common_procedure::{Procedure, ProcedureId, ProcedureState, Status};
393    use futures::TryStreamExt;
394    use store_api::storage::RegionId;
395
396    use crate::error;
397    use crate::key::NodeSchedulingRole;
398    use crate::procedure::drain_datanode::drain_datanode_start::DrainDatanodeStart;
399    use crate::procedure::drain_datanode::manager::DrainDatanodeProcedureTracker;
400    use crate::procedure::drain_datanode::test_util::{
401        TestEnv, follower_region, leader_region, new_persistent_context, run_until_done,
402        spawn_mock_datanode, wait_for_terminal_state,
403    };
404    use crate::procedure::drain_datanode::{
405        DEFAULT_PARALLELISM, DrainDatanodeProcedure, DrainDatanodeProcedureTask,
406        DrainDatanodeStrategy, State,
407    };
408    use crate::procedure::region_migration::test_util::TestingEnv;
409    use crate::service::mailbox::Channel;
410
411    #[tokio::test]
412    async fn test_drain_datanode_fences_and_completes_without_leaders() {
413        let env = TestEnv::new().await;
414        env.create_table(vec![follower_region(RegionId::new(1024, 1), 2, 1)])
415            .await;
416
417        let mut ctx = env.context(new_persistent_context(
418            None,
419            DrainDatanodeStrategy::FailFast,
420        ));
421        let procedure_ctx = TestingEnv::procedure_context();
422        let mut state: Box<dyn State> = Box::new(DrainDatanodeStart);
423
424        let status = run_until_done(&mut state, &mut ctx, &procedure_ctx)
425            .await
426            .unwrap();
427
428        assert!(matches!(status, Status::Done { .. }));
429        assert_eq!(0, ctx.volatile_ctx.migrated);
430        assert_eq!(
431            vec![RegionId::new(1024, 1)],
432            ctx.volatile_ctx.follower_regions()
433        );
434        assert!(
435            !env.node_scheduling_manager
436                .is_schedulable(NodeSchedulingRole::Datanode, 1)
437                .await
438                .unwrap()
439        );
440    }
441
442    #[tokio::test]
443    async fn test_drain_datanode_dump_roundtrip() {
444        let env = TestEnv::new().await;
445        let persistent_ctx = new_persistent_context(Some(2), DrainDatanodeStrategy::TryAll);
446
447        let procedure = DrainDatanodeProcedure::new(env.context(persistent_ctx), vec![]);
448        let json = procedure.dump().unwrap();
449        let tracker = DrainDatanodeProcedureTracker::default();
450        let reloaded = DrainDatanodeProcedure::from_json(&json, &tracker, |persistent_ctx| {
451            env.context(persistent_ctx)
452        })
453        .unwrap();
454
455        assert_eq!("DrainDatanodeStart", reloaded.state.name());
456        let ctx = &reloaded.context.persistent_ctx;
457        assert_eq!(1, ctx.node_id);
458        assert_eq!(Some(2), ctx.target_node_id);
459        assert_eq!(DEFAULT_PARALLELISM, ctx.parallelism);
460        assert_eq!(DrainDatanodeStrategy::TryAll, ctx.strategy);
461        // The plan and the progress of a run are volatile, so they are not dumped.
462        assert!(!json.contains("leader_regions"));
463        assert!(!json.contains("rescan_round"));
464    }
465
466    #[tokio::test]
467    async fn test_drain_datanode_default_strategy_continues_after_batch_failures() {
468        let env = TestEnv::new().await;
469        let regions: Vec<_> = (1..=5).map(|number| RegionId::new(1024, number)).collect();
470        env.create_table(
471            regions
472                .iter()
473                .copied()
474                .map(|region_id| leader_region(region_id, 1))
475                .collect(),
476        )
477        .await;
478        // The pinned target is unschedulable, so every region is given up.
479        env.node_scheduling_manager
480            .mark_unschedulable(NodeSchedulingRole::Datanode, 3)
481            .await
482            .unwrap();
483
484        let mut persistent_ctx = new_persistent_context(Some(3), DrainDatanodeStrategy::default());
485        persistent_ctx.parallelism = 2;
486        let mut ctx = env.context(persistent_ctx);
487        let procedure_ctx = TestingEnv::procedure_context();
488        let mut state: Box<dyn State> = Box::new(DrainDatanodeStart);
489
490        let error = run_until_done(&mut state, &mut ctx, &procedure_ctx)
491            .await
492            .unwrap_err();
493
494        let error::Error::DrainDatanodeAbort { failed_regions, .. } = error else {
495            panic!("expected the drain procedure to abort");
496        };
497        assert_eq!(regions, failed_regions);
498        assert_eq!(regions, ctx.volatile_ctx.failed_regions);
499        assert_eq!(0, ctx.volatile_ctx.migrated);
500    }
501
502    #[tokio::test]
503    async fn test_drain_datanode_recovery_obeys_deadline() {
504        for expired in [true, false] {
505            let mut env = TestEnv::new().await;
506            let first_id = RegionId::new(1024, 1);
507            let second_id = RegionId::new(1024, 2);
508            env.create_table(vec![
509                leader_region(first_id, 1),
510                leader_region(second_id, 1),
511            ])
512            .await;
513            env.mock_datanodes().await;
514            let mut persistent_ctx =
515                new_persistent_context(Some(2), DrainDatanodeStrategy::FailFast);
516            if expired {
517                persistent_ctx.deadline_ms = 0;
518            }
519            let mut procedure =
520                DrainDatanodeProcedure::new(env.context(persistent_ctx.clone()), vec![]);
521            assert!(
522                procedure
523                    .execute(&TestingEnv::procedure_context())
524                    .await
525                    .unwrap()
526                    .need_persist()
527            );
528            let checkpoint = procedure.dump().unwrap();
529            let json: serde_json::Value = serde_json::from_str(&checkpoint).unwrap();
530            assert_eq!(
531                serde_json::json!({
532                    "persistent_ctx": {
533                        "node_id": 1, "target_node_id": 2,
534                        "parallelism": DEFAULT_PARALLELISM, "strategy": "FailFast",
535                        "deadline_ms": persistent_ctx.deadline_ms,
536                    },
537                    "state": {"drain_datanode_state": "CheckDrained"},
538                }),
539                json
540            );
541            drop(procedure);
542            let recovered = DrainDatanodeProcedure::from_json(
543                &checkpoint,
544                &DrainDatanodeProcedureTracker::default(),
545                |persistent_ctx| env.context(persistent_ctx),
546            )
547            .unwrap();
548            assert!(recovered.context.volatile_ctx.plan.is_none());
549            drop(recovered);
550            let id = ProcedureId::random();
551            let message = common_procedure::store::ProcedureMessage {
552                type_name: DrainDatanodeProcedure::TYPE_NAME.to_string(),
553                data: checkpoint,
554                parent_id: None,
555                subprocedure_recovery: Default::default(),
556                step: 0,
557                error: None,
558                context: Default::default(),
559            };
560            let store = common_meta::state_store::KvStateStore::new(env.env.kv_backend());
561            common_procedure::store::state_store::StateStore::put(
562                &store,
563                &format!("procedure/{id}/0000000000.step"),
564                serde_json::to_vec(&message).unwrap(),
565            )
566            .await
567            .unwrap();
568            let manager = env.drain_manager();
569            manager.try_start().unwrap();
570            env.procedure_manager().start().await.unwrap();
571            let state = wait_for_terminal_state(env.procedure_manager(), id).await;
572            if expired {
573                let ProcedureState::Failed { error } = state else {
574                    panic!("expected expired drain: {state:?}");
575                };
576                assert_eq!(StatusCode::Cancelled, error.status_code());
577            } else {
578                assert!(state.is_done(), "{state:?}");
579            }
580            let table_route = env
581                .table_metadata_manager
582                .table_route_manager()
583                .table_route_storage()
584                .get(1024)
585                .await
586                .unwrap()
587                .unwrap();
588            let routes = table_route.region_routes().unwrap();
589            assert_eq!(2, routes.len());
590            for route in routes {
591                assert_eq!(
592                    Some(if expired { 1 } else { 2 }),
593                    route.leader_peer.as_ref().map(|peer| peer.id)
594                );
595            }
596        }
597    }
598
599    #[tokio::test]
600    async fn test_drain_datanode_recovered_migration_outlives_parent() {
601        let mut env = TestEnv::new().await;
602        let region_id = RegionId::new(1024, 1);
603        env.create_table(vec![leader_region(region_id, 1)]).await;
604        let procedure_ctx = TestingEnv::procedure_context();
605        let mut parent = DrainDatanodeProcedure::new(
606            env.context(new_persistent_context(
607                Some(2),
608                DrainDatanodeStrategy::FailFast,
609            )),
610            vec![],
611        );
612        assert!(parent.execute(&procedure_ctx).await.unwrap().need_persist());
613        let parent_checkpoint = parent.dump().unwrap();
614        let Status::Suspended {
615            subprocedures: mut children,
616            recovery,
617            ..
618        } = run_until_done(&mut parent.state, &mut parent.context, &procedure_ctx)
619            .await
620            .unwrap()
621        else {
622            panic!("expected a migration batch");
623        };
624        assert_eq!(1, children.len());
625        let mut child = children.pop().unwrap();
626        // Persist the first migration checkpoint, before it opens the candidate region.
627        assert!(
628            child
629                .procedure
630                .execute(&procedure_ctx)
631                .await
632                .unwrap()
633                .need_persist()
634        );
635        let parent_id = ProcedureId::random();
636        let child_id = child.id;
637        let store = KvStateStore::new(env.env.kv_backend());
638        for (id, message) in [
639            (
640                parent_id,
641                ProcedureMessage {
642                    type_name: parent.type_name().to_string(),
643                    data: parent_checkpoint,
644                    parent_id: None,
645                    subprocedure_recovery: Default::default(),
646                    step: 0,
647                    error: None,
648                    context: Default::default(),
649                },
650            ),
651            (
652                child_id,
653                ProcedureMessage {
654                    type_name: child.procedure.type_name().to_string(),
655                    data: child.procedure.dump().unwrap(),
656                    parent_id: Some(parent_id),
657                    subprocedure_recovery: recovery,
658                    step: 0,
659                    error: None,
660                    context: child.context.clone(),
661                },
662            ),
663        ] {
664            store
665                .put(
666                    &format!("procedure/{id}/0000000000.step"),
667                    serde_json::to_vec(&message).unwrap(),
668                )
669                .await
670                .unwrap();
671        }
672        drop(child);
673        drop(parent);
674        assert!(!env.region_migration_manager.tracker().contains(region_id));
675
676        // Queue mailbox requests without replying until the recovered parent has ended.
677        let mailbox_ctx = env.env.mailbox_context();
678        let mut receivers = Vec::new();
679        for node_id in [1, 2] {
680            let (tx, rx) = tokio::sync::mpsc::channel(8);
681            mailbox_ctx
682                .insert_heartbeat_response_receiver(Channel::Datanode(node_id), tx)
683                .await;
684            receivers.push(rx);
685        }
686        let mailbox = mailbox_ctx.mailbox().clone();
687        env.region_migration_manager.try_start().unwrap();
688        let drain_manager = env.drain_manager();
689        drain_manager.try_start().unwrap();
690        env.procedure_manager().start().await.unwrap();
691        assert!(
692            wait_for_terminal_state(env.procedure_manager(), parent_id)
693                .await
694                .is_done()
695        );
696        assert!(env.region_migration_manager.tracker().contains(region_id));
697        assert!(matches!(
698            env.procedure_manager()
699                .procedure_state(child_id)
700                .await
701                .unwrap(),
702            Some(ProcedureState::Running)
703        ));
704        // No replacement migration was submitted by the recovering drain.
705        assert_eq!(
706            2,
707            env.procedure_manager()
708                .list_procedures()
709                .await
710                .unwrap()
711                .len()
712        );
713
714        for rx in receivers {
715            spawn_mock_datanode(mailbox.clone(), rx);
716        }
717        assert!(
718            wait_for_terminal_state(env.procedure_manager(), child_id)
719                .await
720                .is_done()
721        );
722        assert!(
723            !crate::procedure::drain_datanode::utils::leads_region(
724                &env.table_metadata_manager,
725                1,
726                region_id,
727            )
728            .await
729            .unwrap()
730        );
731        // Completion must release the recovered guard and clean its own checkpoints.
732        tokio::time::timeout(Duration::from_secs(5), async {
733            loop {
734                let mut records = store.walk_top_down("procedure/").await.unwrap();
735                if records.try_next().await.unwrap().is_none()
736                    && !env.region_migration_manager.tracker().contains(region_id)
737                {
738                    break;
739                }
740                tokio::time::sleep(Duration::from_millis(10)).await;
741            }
742        })
743        .await
744        .expect("recovered migration did not clean up");
745        env.procedure_manager().stop().await.unwrap();
746    }
747
748    #[tokio::test]
749    async fn test_drain_datanode_migrates_leader_region() {
750        assert_drain_migrates_leader_regions(1).await;
751    }
752
753    #[tokio::test]
754    async fn test_drain_datanode_migrates_multiple_batches() {
755        assert_drain_migrates_leader_regions(3).await;
756    }
757
758    async fn assert_drain_migrates_leader_regions(region_count: u32) {
759        let mut env = TestEnv::new().await;
760        let regions: Vec<_> = (1..=region_count)
761            .map(|number| RegionId::new(1024, number))
762            .collect();
763        env.create_table(
764            regions
765                .iter()
766                .copied()
767                .map(|region_id| leader_region(region_id, 1))
768                .collect(),
769        )
770        .await;
771
772        env.mock_datanodes().await;
773
774        env.procedure_manager().start().await.unwrap();
775        let manager = env.drain_manager();
776        let procedure_id = manager
777            .submit_procedure(
778                common_procedure::ProcedureContext::default(),
779                DrainDatanodeProcedureTask {
780                    node_id: 1,
781                    target_node_id: Some(2),
782                    parallelism: 1,
783                    strategy: DrainDatanodeStrategy::FailFast,
784                    timeout: None,
785                },
786            )
787            .await
788            .unwrap();
789
790        let state = wait_for_terminal_state(env.procedure_manager(), procedure_id).await;
791        assert!(
792            matches!(state, ProcedureState::Done { .. }),
793            "the drain procedure did not complete successfully: {state:?}"
794        );
795
796        let table_route = env
797            .table_metadata_manager
798            .table_route_manager()
799            .table_route_storage()
800            .get(1024)
801            .await
802            .unwrap()
803            .unwrap();
804        assert_eq!(
805            region_count as usize,
806            table_route.region_routes().unwrap().len()
807        );
808        assert!(
809            !env.node_scheduling_manager
810                .is_schedulable(NodeSchedulingRole::Datanode, 1)
811                .await
812                .unwrap()
813        );
814        for region_route in table_route.region_routes().unwrap() {
815            assert_eq!(
816                Some(2),
817                region_route.leader_peer.as_ref().map(|peer| peer.id)
818            );
819        }
820    }
821}