1use std::any::Any;
16
17use common_procedure::{Context as ProcedureContext, Status, SubprocedureRecovery};
18use common_telemetry::{error, info, warn};
19use serde::{Deserialize, Serialize};
20use store_api::storage::RegionId;
21
22use crate::error::{self, Result};
23use crate::metrics::{METRIC_META_REGION_MIGRATION_DATANODES, METRIC_META_REGION_MIGRATION_FAIL};
24use crate::procedure::drain_datanode::check_drained::CheckDrained;
25use crate::procedure::drain_datanode::utils;
26use crate::procedure::drain_datanode::{Context, DrainDatanodeStrategy, State, SubprocedureMeta};
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
37pub(crate) struct Dispatch;
38
39#[async_trait::async_trait]
40#[typetag::serde]
41impl State for Dispatch {
42 async fn next(
43 &mut self,
44 ctx: &mut Context,
45 procedure_ctx: &ProcedureContext,
46 ) -> Result<(Box<dyn State>, Status)> {
47 let node_id = ctx.persistent_ctx.node_id;
48
49 if !ctx.volatile_ctx.inflight_subprocedures.is_empty() {
51 ctx.wait_for_inflight_subprocedures(procedure_ctx, ctx.persistent_ctx.strategy)
52 .await?;
53 return Ok((Box::new(Dispatch), Status::executing(false)));
54 }
55
56 utils::ensure_drain_deadline(ctx)?;
57
58 let parallelism = ctx.persistent_ctx.parallelism;
59 let regions: Vec<RegionId> = ctx
60 .volatile_ctx
61 .leader_regions()
62 .iter()
63 .take(parallelism)
64 .copied()
65 .collect();
66 if regions.is_empty() {
67 return Ok((Box::new(CheckDrained), Status::executing(false)));
68 }
69
70 let mut children = Vec::with_capacity(regions.len());
71 let mut inflight = Vec::with_capacity(regions.len());
72 let mut targets = Vec::with_capacity(regions.len());
73 for region_id in regions {
74 match ctx
75 .build_migration_procedure(procedure_ctx, region_id)
76 .await
77 {
78 Ok(Some((child, target_node_id))) => {
79 info!(
80 "Dispatching migration of region {} off datanode {}, procedure id: {}",
81 region_id, node_id, child.id
82 );
83 inflight.push(SubprocedureMeta {
84 region_id,
85 procedure_id: child.id,
86 });
87 targets.push(target_node_id);
88 children.push(child);
89 }
90 Ok(None) => {
91 info!(
92 "Skipping region {}, it is no longer led by datanode {}",
93 region_id, node_id
94 );
95 ctx.forget_leader_region(region_id);
96 }
97 Err(error::Error::MigrationRunning { .. }) => {
98 warn!(
102 "A migration of region {} off datanode {} is already running, \
103 skipping the region",
104 region_id, node_id
105 );
106 ctx.forget_leader_region(region_id);
107 }
108 Err(
109 e @ (error::Error::TableRouteNotFound { .. }
110 | error::Error::TableInfoNotFound { .. }),
111 ) => {
112 warn!(
113 e;
114 "Skipping region {} while draining datanode {}: table metadata is missing",
115 region_id, node_id
116 );
117 ctx.forget_leader_region(region_id);
118 }
119 Err(e) => {
120 if matches!(
123 e,
124 error::Error::RegionMigrated { .. }
125 | error::Error::LeaderPeerChanged { .. }
126 ) && !utils::leads_region(&ctx.table_metadata_manager, node_id, region_id)
127 .await?
128 {
129 info!(
130 "Skipping region {}, it is no longer led by datanode {}",
131 region_id, node_id
132 );
133 ctx.forget_leader_region(region_id);
134 continue;
135 }
136
137 match ctx.persistent_ctx.strategy {
138 DrainDatanodeStrategy::FailFast => return Err(e),
140 DrainDatanodeStrategy::TryAll => {
141 METRIC_META_REGION_MIGRATION_FAIL.inc();
142 error!(
143 e;
144 "Gave up draining region {} off datanode {}",
145 region_id,
146 node_id
147 );
148 ctx.forget_leader_region(region_id);
149 ctx.volatile_ctx.failed_regions.push(region_id);
150 }
151 }
152 }
153 }
154 }
155 ctx.volatile_ctx.inflight_subprocedures = inflight;
156
157 if children.is_empty() {
158 return Ok((Box::new(Dispatch), Status::executing(false)));
161 }
162
163 for target_node_id in targets {
164 METRIC_META_REGION_MIGRATION_DATANODES
165 .with_label_values(&["src", &node_id.to_string()])
166 .inc();
167 METRIC_META_REGION_MIGRATION_DATANODES
168 .with_label_values(&["desc", &target_node_id.to_string()])
169 .inc();
170 }
171
172 Ok((
175 Box::new(Dispatch),
176 Status::Suspended {
177 subprocedures: children,
178 recovery: SubprocedureRecovery::Detach,
179 persist: false,
180 },
181 ))
182 }
183
184 fn as_any(&self) -> &dyn Any {
185 self
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use std::collections::HashMap;
192 use std::sync::Arc;
193 use std::time::Duration;
194
195 use crate::procedure::test_util::{procedure_context_with_receivers, procedure_state_receiver};
196 use api::v1::meta::heartbeat_request::NodeWorkloads;
197 use common_meta::cluster::NodeInfo;
198 use common_meta::error::Result as MetaResult;
199 use common_meta::key::table_route::TableRouteKey;
200 use common_meta::key::{MetadataKey, TableMetadataManager, TableMetadataManagerRef};
201 use common_meta::kv_backend::test_util::MockKvBackendBuilder;
202 use common_meta::peer::{Peer, PeerDiscovery, PeerDiscoveryRef};
203 use common_meta::rpc::store::BatchGetResponse;
204 use common_procedure::{ProcedureId, ProcedureState, Status};
205 use store_api::storage::RegionId;
206
207 use crate::error;
208 use crate::key::NodeSchedulingRole;
209 use crate::procedure::drain_datanode::check_drained::CheckDrained;
210 use crate::procedure::drain_datanode::dispatch::Dispatch;
211 use crate::procedure::drain_datanode::drain_datanode_start::DrainDatanodeStart;
212 use crate::procedure::drain_datanode::test_util::{
213 TestEnv, follower_region, leader_region, leader_regions, new_persistent_context,
214 run_until_done, seed_plan,
215 };
216 use crate::procedure::drain_datanode::{DrainDatanodeStrategy, State, SubprocedureMeta};
217 use crate::procedure::region_migration::manager::{
218 RegionMigrationManager, RegionMigrationProcedureTask, RegionMigrationTriggerReason,
219 };
220 use crate::procedure::region_migration::test_util::TestingEnv;
221
222 #[tokio::test]
223 async fn test_drain_datanode_dispatch_transitions_to_check_drained() {
224 let env = TestEnv::new().await;
225 env.create_table(vec![follower_region(RegionId::new(1024, 1), 2, 1)])
227 .await;
228
229 let mut ctx = env.context(new_persistent_context(
230 Some(2),
231 DrainDatanodeStrategy::FailFast,
232 ));
233 let procedure_ctx = TestingEnv::procedure_context();
234 seed_plan(&mut ctx, vec![]);
235 let (next, status) = Dispatch.next(&mut ctx, &procedure_ctx).await.unwrap();
236
237 assert!(next.as_any().downcast_ref::<CheckDrained>().is_some());
238 assert!(matches!(status, Status::Executing { persist: false, .. }));
239 }
240
241 #[tokio::test]
242 async fn test_drain_datanode_dispatches_migration() {
243 let env = TestEnv::new().await;
244 let region_id = RegionId::new(1024, 1);
245 env.create_table(vec![leader_region(region_id, 1)]).await;
246
247 let mut ctx = env.context(new_persistent_context(
248 Some(2),
249 DrainDatanodeStrategy::FailFast,
250 ));
251 let procedure_ctx = TestingEnv::procedure_context();
252
253 let (mut next, _) = DrainDatanodeStart
254 .next(&mut ctx, &procedure_ctx)
255 .await
256 .unwrap();
257 let status = run_until_done(&mut next, &mut ctx, &procedure_ctx)
258 .await
259 .unwrap();
260
261 let Status::Suspended {
262 subprocedures,
263 recovery,
264 persist,
265 } = status
266 else {
267 panic!("expected the dispatch state to suspend the procedure");
268 };
269 assert!(!persist);
271 assert_eq!(common_procedure::SubprocedureRecovery::Detach, recovery);
272 assert_eq!(1, subprocedures.len());
273 assert_eq!(1, ctx.volatile_ctx.inflight_subprocedures.len());
274 assert_eq!(
275 region_id,
276 ctx.volatile_ctx.inflight_subprocedures[0].region_id
277 );
278 assert_eq!(vec![region_id], leader_regions(&ctx));
280 assert!(next.as_any().downcast_ref::<Dispatch>().is_some());
281 }
282
283 #[tokio::test]
284 async fn test_drain_datanode_dispatch_skips_region_moved_away() {
285 let env = TestEnv::new().await;
286 let region_id = RegionId::new(1024, 1);
287 env.create_table(vec![leader_region(region_id, 2)]).await;
289
290 let mut ctx = env.context(new_persistent_context(
291 Some(3),
292 DrainDatanodeStrategy::FailFast,
293 ));
294 seed_plan(&mut ctx, vec![region_id]);
295 let procedure_ctx = TestingEnv::procedure_context();
296
297 let (next, status) = Dispatch.next(&mut ctx, &procedure_ctx).await.unwrap();
298
299 assert!(matches!(status, Status::Executing { .. }));
300 assert!(next.as_any().downcast_ref::<Dispatch>().is_some());
301 assert!(leader_regions(&ctx).is_empty());
302 assert!(ctx.volatile_ctx.inflight_subprocedures.is_empty());
303 }
304
305 struct ChangeRouteDuringDiscovery {
307 inner: PeerDiscoveryRef,
308 metadata: TableMetadataManagerRef,
309 region_id: RegionId,
310 leader_id: Option<u64>,
311 }
312
313 #[async_trait::async_trait]
314 impl PeerDiscovery for ChangeRouteDuringDiscovery {
315 async fn active_frontends(&self) -> MetaResult<Vec<NodeInfo>> {
316 self.inner.active_frontends().await
317 }
318
319 async fn active_datanodes(
320 &self,
321 filter: Option<for<'a> fn(&'a NodeWorkloads) -> bool>,
322 ) -> MetaResult<Vec<NodeInfo>> {
323 let storage = self.metadata.table_route_manager().table_route_storage();
324 let table_id = self.region_id.table_id();
325 let current = storage.get_with_raw_bytes(table_id).await?.unwrap();
326 if let Some(leader_id) = self.leader_id {
327 let updated = current.update(vec![leader_region(self.region_id, leader_id)])?;
328 let (txn, _) = storage.build_update_txn(table_id, ¤t, &updated)?;
329 assert!(self.metadata.kv_backend().txn(txn).await?.succeeded);
330 } else {
331 let table_info = self
332 .metadata
333 .table_info_manager()
334 .get(table_id)
335 .await?
336 .unwrap();
337 self.metadata
338 .delete_table_metadata(
339 table_id,
340 &table_info.table_name(),
341 ¤t,
342 &HashMap::new(),
343 None,
344 )
345 .await?;
346 }
347 self.inner.active_datanodes(filter).await
348 }
349
350 async fn active_flownodes(
351 &self,
352 filter: Option<for<'a> fn(&'a NodeWorkloads) -> bool>,
353 ) -> MetaResult<Vec<NodeInfo>> {
354 self.inner.active_flownodes(filter).await
355 }
356 }
357
358 #[tokio::test]
359 async fn test_drop_after_route_read_fail_fast() {
360 assert_drop_after_route_read_is_skipped(DrainDatanodeStrategy::FailFast).await;
361 }
362
363 #[tokio::test]
364 async fn test_drop_after_route_read_try_all() {
365 assert_drop_after_route_read_is_skipped(DrainDatanodeStrategy::TryAll).await;
366 }
367
368 async fn assert_drop_after_route_read_is_skipped(strategy: DrainDatanodeStrategy) {
369 let env = TestEnv::new().await;
370 let region_id = RegionId::new(1024, 1);
371 env.create_table(vec![leader_region(region_id, 1)]).await;
372 let metadata = env.table_metadata_manager.clone();
373 let backend = MockKvBackendBuilder::default()
376 .batch_get_fn(Arc::new(|_| Ok(BatchGetResponse { kvs: vec![] })))
378 .range_async_fn(Arc::new(move |request| {
379 let metadata = metadata.clone();
380 Box::pin(async move {
381 let response = metadata.kv_backend().range(request.clone()).await?;
382 if request.key == TableRouteKey::new(region_id.table_id()).to_bytes() {
383 let route = metadata
384 .table_route_manager()
385 .table_route_storage()
386 .get_with_raw_bytes(region_id.table_id())
387 .await?
388 .unwrap();
389 let info = metadata
390 .table_info_manager()
391 .get(region_id.table_id())
392 .await?
393 .unwrap();
394 metadata
395 .delete_table_metadata(
396 region_id.table_id(),
397 &info.table_name(),
398 &route,
399 &HashMap::new(),
400 None,
401 )
402 .await?;
403 }
404 Ok(response)
405 })
406 }))
407 .build()
408 .unwrap();
409 let factory = env
410 .env
411 .context_factory_with_metadata(Arc::new(TableMetadataManager::new(Arc::new(backend))));
412 let manager = Arc::new(RegionMigrationManager::new(
413 env.procedure_manager().clone(),
414 factory,
415 ));
416 let mut ctx = env.context(new_persistent_context(Some(2), strategy));
417 ctx.region_migration_manager = manager.clone();
418 seed_plan(&mut ctx, vec![region_id]);
419
420 let (next, status) = Dispatch
421 .next(&mut ctx, &TestingEnv::procedure_context())
422 .await
423 .unwrap();
424
425 assert!(next.as_any().is::<Dispatch>());
426 assert!(matches!(status, Status::Executing { persist: false, .. }));
427 assert!(leader_regions(&ctx).is_empty());
428 assert!(ctx.volatile_ctx.failed_regions.is_empty());
429 assert!(ctx.volatile_ctx.inflight_subprocedures.is_empty());
430 assert_eq!(0, ctx.volatile_ctx.migrated);
431 assert!(!manager.tracker().contains(region_id));
432 assert!(
433 env.table_metadata_manager
434 .table_info_manager()
435 .get(region_id.table_id())
436 .await
437 .unwrap()
438 .is_none()
439 );
440 }
441
442 #[tokio::test]
443 async fn test_drain_datanode_dispatch_skips_region_moved_or_dropped_during_discovery() {
444 for strategy in [
445 DrainDatanodeStrategy::FailFast,
446 DrainDatanodeStrategy::TryAll,
447 ] {
448 for leader_id in [Some(2), Some(3), None] {
451 let env = TestEnv::new().await;
452 let region_id = RegionId::new(1024, 1);
453 env.create_table(vec![leader_region(region_id, 1)]).await;
454 let mut ctx = env.context(new_persistent_context(Some(2), strategy));
455 ctx.selector_ctx.peer_discovery = Arc::new(ChangeRouteDuringDiscovery {
456 inner: ctx.selector_ctx.peer_discovery.clone(),
457 metadata: env.table_metadata_manager.clone(),
458 region_id,
459 leader_id,
460 });
461 seed_plan(&mut ctx, vec![region_id]);
462
463 let (next, status) = Dispatch
464 .next(&mut ctx, &TestingEnv::procedure_context())
465 .await
466 .unwrap();
467
468 assert!(next.as_any().is::<Dispatch>());
469 assert!(matches!(status, Status::Executing { persist: false, .. }));
470 assert!(leader_regions(&ctx).is_empty());
471 assert!(ctx.volatile_ctx.failed_regions.is_empty());
472 assert!(ctx.volatile_ctx.inflight_subprocedures.is_empty());
473 assert_eq!(0, ctx.volatile_ctx.migrated);
474 assert!(!env.region_migration_manager.tracker().contains(region_id));
475 }
476 }
477 }
478
479 #[tokio::test]
480 async fn test_drain_datanode_skips_region_whose_migration_is_running() {
481 let env = TestEnv::new().await;
482 let region_id = RegionId::new(1024, 1);
483 env.create_table(vec![leader_region(region_id, 1)]).await;
484
485 let mut ctx = env.context(new_persistent_context(
486 Some(2),
487 DrainDatanodeStrategy::FailFast,
488 ));
489 seed_plan(&mut ctx, vec![region_id]);
490 let procedure_ctx = TestingEnv::procedure_context();
491
492 let _child = env
495 .region_migration_manager
496 .build_subprocedure(
497 common_procedure::ProcedureContext::default(),
498 RegionMigrationProcedureTask::new(
499 region_id,
500 Peer::empty(1),
501 Peer::empty(2),
502 Duration::from_secs(10),
503 RegionMigrationTriggerReason::Manual,
504 ),
505 )
506 .await
507 .expect("the manual migration should be buildable");
508
509 let (next, status) = Dispatch.next(&mut ctx, &procedure_ctx).await.unwrap();
510
511 assert!(matches!(status, Status::Executing { .. }));
515 assert!(next.as_any().downcast_ref::<Dispatch>().is_some());
516 assert!(leader_regions(&ctx).is_empty());
517 assert!(ctx.volatile_ctx.failed_regions.is_empty());
518 assert!(ctx.volatile_ctx.inflight_subprocedures.is_empty());
519 }
520
521 #[tokio::test]
522 async fn test_drain_datanode_fail_fast_on_unschedulable_target() {
523 let env = TestEnv::new().await;
524 let region_id = RegionId::new(1024, 1);
525 env.create_table(vec![leader_region(region_id, 1)]).await;
526 env.node_scheduling_manager
527 .mark_unschedulable(NodeSchedulingRole::Datanode, 3)
528 .await
529 .unwrap();
530
531 let mut ctx = env.context(new_persistent_context(
532 Some(3),
533 DrainDatanodeStrategy::FailFast,
534 ));
535 let procedure_ctx = TestingEnv::procedure_context();
536
537 let (mut state, _) = DrainDatanodeStart
538 .next(&mut ctx, &procedure_ctx)
539 .await
540 .unwrap();
541 let error = run_until_done(&mut state, &mut ctx, &procedure_ctx)
542 .await
543 .unwrap_err();
544
545 assert!(matches!(error, error::Error::DrainDatanodeAbort { .. }));
546 assert!(!error.is_retryable());
547 assert_eq!(vec![region_id], leader_regions(&ctx));
548 }
549
550 #[tokio::test]
551 async fn test_drain_datanode_waits_for_migrated_batch() {
552 let env = TestEnv::new().await;
553 let region_id = RegionId::new(1024, 1);
554 env.create_table(vec![leader_region(region_id, 1)]).await;
555
556 let mut ctx = env.context(new_persistent_context(
557 Some(2),
558 DrainDatanodeStrategy::FailFast,
559 ));
560 seed_plan(&mut ctx, vec![region_id]);
561 ctx.volatile_ctx.inflight_subprocedures = vec![SubprocedureMeta {
562 region_id,
563 procedure_id: ProcedureId::random(),
564 }];
565 let procedure_ctx = procedure_context_with_receivers(HashMap::from([(
566 ctx.volatile_ctx.inflight_subprocedures[0].procedure_id,
567 procedure_state_receiver(ProcedureState::Done { output: None }),
568 )]));
569
570 let (mut next, status) = Dispatch.next(&mut ctx, &procedure_ctx).await.unwrap();
571
572 assert!(matches!(status, Status::Executing { .. }));
573 assert!(next.as_any().downcast_ref::<Dispatch>().is_some());
574 assert_eq!(1, ctx.volatile_ctx.migrated);
575 assert!(leader_regions(&ctx).is_empty());
576 assert!(ctx.volatile_ctx.inflight_subprocedures.is_empty());
577 let (next, _) = next.next(&mut ctx, &procedure_ctx).await.unwrap();
579 assert!(next.as_any().downcast_ref::<CheckDrained>().is_some());
580 }
581
582 #[tokio::test]
583 async fn test_drain_datanode_exceeded_deadline() {
584 let env = TestEnv::new().await;
585 let region_id = RegionId::new(1024, 1);
586 env.create_table(vec![leader_region(region_id, 1)]).await;
587
588 let mut persistent_ctx = new_persistent_context(Some(2), DrainDatanodeStrategy::FailFast);
589 persistent_ctx.deadline_ms = 0;
590 let mut ctx = env.context(persistent_ctx);
591 seed_plan(&mut ctx, vec![region_id]);
592 let procedure_ctx = TestingEnv::procedure_context();
593
594 let error = Dispatch.next(&mut ctx, &procedure_ctx).await.unwrap_err();
595
596 assert!(matches!(error, error::Error::ExceededDeadline { .. }));
597 assert!(!error.is_retryable());
598 assert_eq!(vec![region_id], leader_regions(&ctx));
599 }
600
601 #[tokio::test]
602 async fn test_drain_datanode_deadline_does_not_interrupt_batch() {
603 let env = TestEnv::new().await;
604 let region_id = RegionId::new(1024, 1);
605 env.create_table(vec![leader_region(region_id, 1)]).await;
606
607 let mut persistent_ctx = new_persistent_context(Some(2), DrainDatanodeStrategy::FailFast);
608 persistent_ctx.deadline_ms = 0;
609 let mut ctx = env.context(persistent_ctx);
610 seed_plan(&mut ctx, vec![region_id]);
611 ctx.volatile_ctx.inflight_subprocedures = vec![SubprocedureMeta {
612 region_id,
613 procedure_id: ProcedureId::random(),
614 }];
615 let procedure_ctx = procedure_context_with_receivers(HashMap::from([(
616 ctx.volatile_ctx.inflight_subprocedures[0].procedure_id,
617 procedure_state_receiver(ProcedureState::Done { output: None }),
618 )]));
619
620 let (_, status) = Dispatch.next(&mut ctx, &procedure_ctx).await.unwrap();
622 assert!(matches!(status, Status::Executing { .. }));
623 assert_eq!(1, ctx.volatile_ctx.migrated);
624
625 let error = Dispatch.next(&mut ctx, &procedure_ctx).await.unwrap_err();
627 assert!(matches!(error, error::Error::ExceededDeadline { .. }));
628 }
629}