1use common_meta::instruction::{
16 DowngradeRegion, DowngradeRegionReply, DowngradeRegionsReply, InstructionError,
17 InstructionReply,
18};
19use common_telemetry::tracing::info;
20use common_telemetry::{error, warn};
21use futures::future::join_all;
22use store_api::region_engine::{SetRegionRoleStateResponse, SettableRegionRoleState};
23use store_api::region_request::{RegionFlushRequest, RegionRequest};
24use store_api::storage::RegionId;
25
26use crate::heartbeat::handler::{HandlerContext, InstructionHandler};
27use crate::heartbeat::task_tracker::WaitResult;
28
29#[derive(Debug, Clone, Copy, Default)]
30pub struct DowngradeRegionsHandler;
31
32impl DowngradeRegionsHandler {
33 async fn handle_downgrade_region(
34 ctx: &HandlerContext,
35 DowngradeRegion {
36 region_id,
37 flush_timeout,
38 }: DowngradeRegion,
39 ) -> DowngradeRegionReply {
40 let Some(writable) = ctx.region_server.is_region_leader(region_id) else {
41 warn!("Region: {region_id} is not found");
42 return DowngradeRegionReply {
43 region_id,
44 last_entry_id: None,
45 metadata_last_entry_id: None,
46 exists: false,
47 error: None,
48 };
49 };
50
51 let region_server_moved = ctx.region_server.clone();
52
53 if !writable {
55 warn!(
56 "Region: {region_id} is not writable, flush_timeout: {:?}",
57 flush_timeout
58 );
59 return ctx.downgrade_to_follower_gracefully(region_id).await;
60 }
61
62 let Some(flush_timeout) = flush_timeout else {
64 return ctx.downgrade_to_follower_gracefully(region_id).await;
65 };
66
67 match ctx
71 .region_server
72 .set_region_role_state_gracefully(region_id, SettableRegionRoleState::DowngradingLeader)
73 .await
74 {
75 Ok(SetRegionRoleStateResponse::Success { .. }) => {}
76 Ok(SetRegionRoleStateResponse::NotFound) => {
77 warn!("Region: {region_id} is not found");
78 return DowngradeRegionReply {
79 region_id,
80 last_entry_id: None,
81 metadata_last_entry_id: None,
82 exists: false,
83 error: None,
84 };
85 }
86 Ok(SetRegionRoleStateResponse::InvalidTransition(err)) => {
87 error!(err; "Failed to convert region to downgrading leader - invalid transition");
88 return DowngradeRegionReply {
89 region_id,
90 last_entry_id: None,
91 metadata_last_entry_id: None,
92 exists: true,
93 error: Some(InstructionError::from_error(&err)),
94 };
95 }
96 Err(err) => {
97 error!(err; "Failed to convert region to downgrading leader");
98 return DowngradeRegionReply {
99 region_id,
100 last_entry_id: None,
101 metadata_last_entry_id: None,
102 exists: true,
103 error: Some(InstructionError::from_error(&err)),
104 };
105 }
106 }
107
108 let register_result = ctx
109 .downgrade_tasks
110 .try_register(
111 region_id,
112 Box::pin(async move {
113 info!("Flush region: {region_id} before converting region to follower");
114 region_server_moved
115 .handle_request(
116 region_id,
117 RegionRequest::Flush(RegionFlushRequest::default()),
118 )
119 .await?;
120
121 Ok(())
122 }),
123 )
124 .await;
125
126 if register_result.is_busy() {
127 warn!("Another flush task is running for the region: {region_id}");
128 }
129
130 let mut watcher = register_result.into_watcher();
131 let result = ctx.downgrade_tasks.wait(&mut watcher, flush_timeout).await;
132
133 match result {
134 WaitResult::Timeout => DowngradeRegionReply {
135 region_id,
136 last_entry_id: None,
137 metadata_last_entry_id: None,
138 exists: true,
139 error: Some(InstructionError::legacy_internal_retryable(format!(
140 "Flush region timeout, region: {region_id}, timeout: {:?}",
141 flush_timeout
142 ))),
143 },
144 WaitResult::Finish(Ok(_)) => ctx.downgrade_to_follower_gracefully(region_id).await,
145 WaitResult::Finish(Err(err)) => DowngradeRegionReply {
146 region_id,
147 last_entry_id: None,
148 metadata_last_entry_id: None,
149 exists: true,
150 error: Some(InstructionError::from_error(&err)),
151 },
152 }
153 }
154}
155
156#[async_trait::async_trait]
157impl InstructionHandler for DowngradeRegionsHandler {
158 type Instruction = Vec<DowngradeRegion>;
159
160 async fn handle(
161 &self,
162 ctx: &HandlerContext,
163 downgrade_regions: Self::Instruction,
164 ) -> Option<InstructionReply> {
165 let futures = downgrade_regions
166 .into_iter()
167 .map(|downgrade_region| Self::handle_downgrade_region(ctx, downgrade_region));
168 let results = join_all(futures).await;
170
171 Some(InstructionReply::DowngradeRegions(
172 DowngradeRegionsReply::new(results),
173 ))
174 }
175}
176
177impl HandlerContext {
178 async fn downgrade_to_follower_gracefully(&self, region_id: RegionId) -> DowngradeRegionReply {
179 match self
180 .region_server
181 .set_region_role_state_gracefully(region_id, SettableRegionRoleState::Follower)
182 .await
183 {
184 Ok(SetRegionRoleStateResponse::Success(success)) => DowngradeRegionReply {
185 region_id,
186 last_entry_id: success.last_entry_id(),
187 metadata_last_entry_id: success.metadata_last_entry_id(),
188 exists: true,
189 error: None,
190 },
191 Ok(SetRegionRoleStateResponse::NotFound) => {
192 warn!("Region: {region_id} is not found");
193 DowngradeRegionReply {
194 region_id,
195 last_entry_id: None,
196 metadata_last_entry_id: None,
197 exists: false,
198 error: None,
199 }
200 }
201 Ok(SetRegionRoleStateResponse::InvalidTransition(err)) => {
202 error!(err; "Failed to convert region to follower - invalid transition");
203 DowngradeRegionReply {
204 region_id,
205 last_entry_id: None,
206 metadata_last_entry_id: None,
207 exists: true,
208 error: Some(InstructionError::from_error(&err)),
209 }
210 }
211 Err(err) => {
212 error!(err; "Failed to convert region to {}", SettableRegionRoleState::Follower);
213 DowngradeRegionReply {
214 region_id,
215 last_entry_id: None,
216 metadata_last_entry_id: None,
217 exists: true,
218 error: Some(InstructionError::from_error(&err)),
219 }
220 }
221 }
222 }
223}
224
225#[cfg(test)]
226mod tests {
227 use std::assert_matches;
228 use std::sync::Arc;
229 use std::time::Duration;
230
231 use common_meta::heartbeat::handler::{HandleControl, HeartbeatResponseHandler};
232 use common_meta::heartbeat::mailbox::MessageMeta;
233 use common_meta::instruction::{DowngradeRegion, Instruction};
234 use common_meta::kv_backend::memory::MemoryKvBackend;
235 use mito2::config::MitoConfig;
236 use mito2::engine::MITO_ENGINE_NAME;
237 use mito2::test_util::{CreateRequestBuilder, TestEnv};
238 use store_api::region_engine::{
239 RegionEngine, RegionRole, SetRegionRoleStateResponse, SetRegionRoleStateSuccess,
240 };
241 use store_api::region_request::RegionRequest;
242 use store_api::storage::RegionId;
243 use tokio::time::Instant;
244
245 use crate::error;
246 use crate::heartbeat::handler::downgrade_region::DowngradeRegionsHandler;
247 use crate::heartbeat::handler::tests::HeartbeatResponseTestEnv;
248 use crate::heartbeat::handler::{
249 HandlerContext, InstructionHandler, RegionHeartbeatResponseHandler,
250 };
251 use crate::tests::{MockRegionEngine, mock_region_server};
252
253 #[tokio::test]
254 async fn test_region_not_exist() {
255 let mut mock_region_server = mock_region_server();
256 let (mock_engine, _) = MockRegionEngine::new(MITO_ENGINE_NAME);
257 mock_region_server.register_engine(mock_engine);
258 let kv_backend = Arc::new(MemoryKvBackend::new());
259 let handler_context = HandlerContext::new_for_test(mock_region_server, kv_backend);
260 let region_id = RegionId::new(1024, 1);
261 let waits = vec![None, Some(Duration::from_millis(100u64))];
262
263 for flush_timeout in waits {
264 let reply = DowngradeRegionsHandler
265 .handle(
266 &handler_context,
267 vec![DowngradeRegion {
268 region_id,
269 flush_timeout,
270 }],
271 )
272 .await;
273
274 let reply = &reply.unwrap().expect_downgrade_regions_reply()[0];
275 assert!(!reply.exists);
276 assert!(reply.error.is_none());
277 assert!(reply.last_entry_id.is_none());
278 }
279 }
280
281 #[tokio::test]
282 async fn test_region_readonly() {
283 let mock_region_server = mock_region_server();
284 let region_id = RegionId::new(1024, 1);
285 let (mock_engine, _) =
286 MockRegionEngine::with_custom_apply_fn(MITO_ENGINE_NAME, |region_engine| {
287 region_engine.mock_role = Some(Some(RegionRole::Follower));
288 region_engine.handle_request_mock_fn = Some(Box::new(|_, req| {
289 if let RegionRequest::Flush(_) = req {
290 unreachable!();
292 };
293
294 Ok(0)
295 }));
296 region_engine.handle_set_readonly_gracefully_mock_fn = Some(Box::new(|_| {
297 Ok(SetRegionRoleStateResponse::success(
298 SetRegionRoleStateSuccess::mito(1024),
299 ))
300 }))
301 });
302 mock_region_server.register_test_region(region_id, mock_engine);
303 let kv_backend = Arc::new(MemoryKvBackend::new());
304 let handler_context = HandlerContext::new_for_test(mock_region_server, kv_backend);
305
306 let waits = vec![None, Some(Duration::from_millis(100u64))];
307 for flush_timeout in waits {
308 let reply = DowngradeRegionsHandler
309 .handle(
310 &handler_context,
311 vec![DowngradeRegion {
312 region_id,
313 flush_timeout,
314 }],
315 )
316 .await;
317
318 let reply = &reply.unwrap().expect_downgrade_regions_reply()[0];
319 assert!(reply.exists);
320 assert!(reply.error.is_none());
321 assert_eq!(reply.last_entry_id.unwrap(), 1024);
322 }
323 }
324
325 #[tokio::test]
326 async fn test_region_flush_timeout() {
327 let mock_region_server = mock_region_server();
328 let region_id = RegionId::new(1024, 1);
329 let (mock_engine, _) =
330 MockRegionEngine::with_custom_apply_fn(MITO_ENGINE_NAME, |region_engine| {
331 region_engine.mock_role = Some(Some(RegionRole::Leader));
332 region_engine.handle_request_delay = Some(Duration::from_secs(100));
333 region_engine.handle_set_readonly_gracefully_mock_fn = Some(Box::new(|_| {
334 Ok(SetRegionRoleStateResponse::success(
335 SetRegionRoleStateSuccess::mito(1024),
336 ))
337 }))
338 });
339 mock_region_server.register_test_region(region_id, mock_engine);
340 let kv_backend = Arc::new(MemoryKvBackend::new());
341 let handler_context = HandlerContext::new_for_test(mock_region_server, kv_backend);
342
343 let flush_timeout = Duration::from_millis(100);
344 let reply = DowngradeRegionsHandler
345 .handle(
346 &handler_context,
347 vec![DowngradeRegion {
348 region_id,
349 flush_timeout: Some(flush_timeout),
350 }],
351 )
352 .await;
353
354 let reply = &reply.unwrap().expect_downgrade_regions_reply()[0];
355 assert!(reply.exists);
356 assert!(reply.error.as_ref().unwrap().message.contains("timeout"));
357 assert!(reply.last_entry_id.is_none());
358 }
359
360 #[tokio::test]
361 async fn test_region_flush_timeout_and_retry() {
362 let mock_region_server = mock_region_server();
363 let region_id = RegionId::new(1024, 1);
364 let (mock_engine, _) =
365 MockRegionEngine::with_custom_apply_fn(MITO_ENGINE_NAME, |region_engine| {
366 region_engine.mock_role = Some(Some(RegionRole::Leader));
367 region_engine.handle_request_delay = Some(Duration::from_millis(300));
368 region_engine.handle_set_readonly_gracefully_mock_fn = Some(Box::new(|_| {
369 Ok(SetRegionRoleStateResponse::success(
370 SetRegionRoleStateSuccess::mito(1024),
371 ))
372 }))
373 });
374 mock_region_server.register_test_region(region_id, mock_engine);
375 let kv_backend = Arc::new(MemoryKvBackend::new());
376 let handler_context = HandlerContext::new_for_test(mock_region_server, kv_backend);
377
378 let waits = vec![
379 Some(Duration::from_millis(100u64)),
380 Some(Duration::from_millis(100u64)),
381 ];
382
383 for flush_timeout in waits {
384 let reply = DowngradeRegionsHandler
385 .handle(
386 &handler_context,
387 vec![DowngradeRegion {
388 region_id,
389 flush_timeout,
390 }],
391 )
392 .await;
393
394 let reply = &reply.unwrap().expect_downgrade_regions_reply()[0];
395 assert!(reply.exists);
396 assert!(reply.error.as_ref().unwrap().message.contains("timeout"));
397 assert!(reply.last_entry_id.is_none());
398 }
399 let timer = Instant::now();
400 let reply = DowngradeRegionsHandler
401 .handle(
402 &handler_context,
403 vec![DowngradeRegion {
404 region_id,
405 flush_timeout: Some(Duration::from_millis(500)),
406 }],
407 )
408 .await;
409 assert!(timer.elapsed().as_millis() < 300);
411
412 let reply = &reply.unwrap().expect_downgrade_regions_reply()[0];
413 assert!(reply.exists);
414 assert!(reply.error.is_none());
415 assert_eq!(reply.last_entry_id.unwrap(), 1024);
416 }
417
418 #[tokio::test]
419 async fn test_region_flush_timeout_and_retry_error() {
420 let mock_region_server = mock_region_server();
421 let region_id = RegionId::new(1024, 1);
422 let (mock_engine, _) =
423 MockRegionEngine::with_custom_apply_fn(MITO_ENGINE_NAME, |region_engine| {
424 region_engine.mock_role = Some(Some(RegionRole::Leader));
425 region_engine.handle_request_delay = Some(Duration::from_millis(300));
426 region_engine.handle_request_mock_fn = Some(Box::new(|_, _| {
427 error::UnexpectedSnafu {
428 violated: "mock flush failed",
429 }
430 .fail()
431 }));
432 region_engine.handle_set_readonly_gracefully_mock_fn = Some(Box::new(|_| {
433 Ok(SetRegionRoleStateResponse::success(
434 SetRegionRoleStateSuccess::mito(1024),
435 ))
436 }))
437 });
438 mock_region_server.register_test_region(region_id, mock_engine);
439 let kv_backend = Arc::new(MemoryKvBackend::new());
440 let handler_context = HandlerContext::new_for_test(mock_region_server, kv_backend);
441
442 let waits = vec![
443 Some(Duration::from_millis(100u64)),
444 Some(Duration::from_millis(100u64)),
445 ];
446
447 for flush_timeout in waits {
448 let reply = DowngradeRegionsHandler
449 .handle(
450 &handler_context,
451 vec![DowngradeRegion {
452 region_id,
453 flush_timeout,
454 }],
455 )
456 .await;
457 let reply = &reply.unwrap().expect_downgrade_regions_reply()[0];
458 assert!(reply.exists);
459 assert!(reply.error.as_ref().unwrap().message.contains("timeout"));
460 assert!(reply.last_entry_id.is_none());
461 }
462 let timer = Instant::now();
463 let reply = DowngradeRegionsHandler
464 .handle(
465 &handler_context,
466 vec![DowngradeRegion {
467 region_id,
468 flush_timeout: Some(Duration::from_millis(500)),
469 }],
470 )
471 .await;
472 assert!(timer.elapsed().as_millis() < 300);
474 let reply = &reply.unwrap().expect_downgrade_regions_reply()[0];
475 assert!(reply.exists);
476 assert!(
477 reply
478 .error
479 .as_ref()
480 .unwrap()
481 .message
482 .contains("flush failed")
483 );
484 assert!(reply.last_entry_id.is_none());
485 }
486
487 #[tokio::test]
488 async fn test_set_region_readonly_not_found() {
489 let mock_region_server = mock_region_server();
490 let region_id = RegionId::new(1024, 1);
491 let (mock_engine, _) =
492 MockRegionEngine::with_custom_apply_fn(MITO_ENGINE_NAME, |region_engine| {
493 region_engine.mock_role = Some(Some(RegionRole::Leader));
494 region_engine.handle_set_readonly_gracefully_mock_fn =
495 Some(Box::new(|_| Ok(SetRegionRoleStateResponse::NotFound)));
496 });
497 mock_region_server.register_test_region(region_id, mock_engine);
498 let kv_backend = Arc::new(MemoryKvBackend::new());
499 let handler_context = HandlerContext::new_for_test(mock_region_server, kv_backend);
500 let reply = DowngradeRegionsHandler
501 .handle(
502 &handler_context,
503 vec![DowngradeRegion {
504 region_id,
505 flush_timeout: None,
506 }],
507 )
508 .await;
509 let reply = &reply.unwrap().expect_downgrade_regions_reply()[0];
510 assert!(!reply.exists);
511 assert!(reply.error.is_none());
512 assert!(reply.last_entry_id.is_none());
513 }
514
515 #[tokio::test]
516 async fn test_set_region_readonly_error() {
517 let mock_region_server = mock_region_server();
518 let region_id = RegionId::new(1024, 1);
519 let (mock_engine, _) =
520 MockRegionEngine::with_custom_apply_fn(MITO_ENGINE_NAME, |region_engine| {
521 region_engine.mock_role = Some(Some(RegionRole::Leader));
522 region_engine.handle_set_readonly_gracefully_mock_fn = Some(Box::new(|_| {
523 error::UnexpectedSnafu {
524 violated: "Failed to set region to readonly",
525 }
526 .fail()
527 }));
528 });
529 mock_region_server.register_test_region(region_id, mock_engine);
530 let kv_backend = Arc::new(MemoryKvBackend::new());
531 let handler_context = HandlerContext::new_for_test(mock_region_server, kv_backend);
532 let reply = DowngradeRegionsHandler
533 .handle(
534 &handler_context,
535 vec![DowngradeRegion {
536 region_id,
537 flush_timeout: None,
538 }],
539 )
540 .await;
541 let reply = &reply.unwrap().expect_downgrade_regions_reply()[0];
542 assert!(reply.exists);
543 assert!(
544 reply
545 .error
546 .as_ref()
547 .unwrap()
548 .message
549 .contains("Failed to set region to readonly")
550 );
551 assert!(reply.last_entry_id.is_none());
552 }
553
554 #[tokio::test]
555 async fn test_downgrade_regions() {
556 common_telemetry::init_default_ut_logging();
557
558 let mut region_server = mock_region_server();
559 let kv_backend = Arc::new(MemoryKvBackend::new());
560 let heartbeat_handler =
561 RegionHeartbeatResponseHandler::new(region_server.clone(), kv_backend);
562 let mut engine_env = TestEnv::with_prefix("downgrade-regions").await;
563 let engine = engine_env.create_engine(MitoConfig::default()).await;
564 region_server.register_engine(Arc::new(engine.clone()));
565 let region_id = RegionId::new(1024, 1);
566 let region_id1 = RegionId::new(1024, 2);
567 let builder = CreateRequestBuilder::new();
568 let create_req = builder.build();
569 region_server
570 .handle_request(region_id, RegionRequest::Create(create_req))
571 .await
572 .unwrap();
573 let create_req1 = builder.build();
574 region_server
575 .handle_request(region_id1, RegionRequest::Create(create_req1))
576 .await
577 .unwrap();
578 let meta = MessageMeta::new_test(1, "test", "dn-1", "meta-0");
579 let instruction = Instruction::DowngradeRegions(vec![
580 DowngradeRegion {
581 region_id,
582 flush_timeout: Some(Duration::from_secs(1)),
583 },
584 DowngradeRegion {
585 region_id: region_id1,
586 flush_timeout: Some(Duration::from_secs(1)),
587 },
588 ]);
589 let mut heartbeat_env = HeartbeatResponseTestEnv::new();
590 let mut ctx = heartbeat_env.create_handler_ctx((meta, Default::default(), instruction));
591 let control = heartbeat_handler.handle(&mut ctx).await.unwrap();
592 assert_matches!(control, HandleControl::Continue);
593
594 let (_, reply) = heartbeat_env.receiver.recv().await.unwrap();
595 let reply = reply.expect_downgrade_regions_reply();
596 assert_eq!(reply[0].region_id, region_id);
597 assert!(reply[0].exists);
598 assert!(reply[0].error.is_none());
599 assert_eq!(reply[0].last_entry_id, Some(0));
600 assert_eq!(reply[1].region_id, region_id1);
601 assert!(reply[1].exists);
602 assert!(reply[1].error.is_none());
603 assert_eq!(reply[1].last_entry_id, Some(0));
604
605 assert_eq!(engine.role(region_id).unwrap(), RegionRole::Follower);
606 assert_eq!(engine.role(region_id1).unwrap(), RegionRole::Follower);
607 }
608}