1pub(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
51pub const DEFAULT_PARALLELISM: usize = 4;
53pub const MAX_PARALLELISM: usize = 32;
55pub const DEFAULT_DRAIN_TIMEOUT: Duration = Duration::from_secs(20 * 60);
57pub const MAX_RESCAN_ROUNDS: u32 = 3;
59
60#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
62pub enum DrainDatanodeStrategy {
63 FailFast,
66 #[default]
68 TryAll,
69}
70
71#[derive(Debug, Clone, Default, PartialEq, Eq)]
73pub struct DrainDatanodePlan {
74 pub leader_regions: Vec<RegionId>,
76 pub follower_regions: Vec<RegionId>,
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct PersistentContext {
86 pub node_id: DatanodeId,
88 pub target_node_id: Option<DatanodeId>,
90 pub parallelism: usize,
92 pub strategy: DrainDatanodeStrategy,
94 pub deadline_ms: i64,
96}
97
98#[derive(Debug, Clone, Copy)]
100pub struct SubprocedureMeta {
101 pub region_id: RegionId,
103 pub procedure_id: ProcedureId,
105}
106
107#[derive(Debug, Clone, Default)]
114pub struct VolatileContext {
115 pub plan: Option<DrainDatanodePlan>,
121 pub inflight_subprocedures: Vec<SubprocedureMeta>,
123 pub migrated: usize,
125 pub failed_regions: Vec<RegionId>,
127 pub rescan_round: u32,
129}
130
131#[derive(Debug, Clone)]
133pub struct DrainDatanodeProcedureTask {
134 pub node_id: DatanodeId,
136 pub target_node_id: Option<DatanodeId>,
138 pub parallelism: usize,
140 pub strategy: DrainDatanodeStrategy,
142 pub timeout: Option<Duration>,
144}
145
146#[derive(Clone)]
148pub struct Context {
149 pub persistent_ctx: PersistentContext,
151 pub volatile_ctx: VolatileContext,
153 pub table_metadata_manager: TableMetadataManagerRef,
155 pub node_scheduling_manager: NodeSchedulingManagerRef,
157 pub region_migration_manager: RegionMigrationManagerRef,
159 pub selector_ctx: SelectorContext,
161}
162
163impl Context {
164 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 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 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 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 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#[derive(Debug, Deserialize)]
235pub struct DrainDatanodeDataOwned {
236 persistent_ctx: PersistentContext,
237 state: Box<dyn State>,
238}
239
240#[derive(Debug, Serialize)]
242pub struct DrainDatanodeData<'a> {
243 persistent_ctx: &'a PersistentContext,
244 state: &'a dyn State,
245}
246
247#[async_trait::async_trait]
249#[typetag::serde(tag = "drain_datanode_state")]
250pub(crate) trait State: Sync + Send + Debug {
251 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 async fn next(
259 &mut self,
260 ctx: &mut Context,
261 procedure_ctx: &ProcedureContext,
262 ) -> Result<(Box<dyn State>, Status)>;
263
264 fn as_any(&self) -> &dyn Any;
266}
267
268pub(crate) struct DrainDatanodeProcedure {
276 state: Box<dyn State>,
277 context: Context,
278 _guards: Vec<DrainDatanodeProcedureGuard>,
281}
282
283impl DrainDatanodeProcedure {
284 const TYPE_NAME: &'static str = "metasrv-procedure::DrainDatanode";
285
286 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 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 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 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 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 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 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 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}