Skip to main content

meta_srv/procedure/region_migration/
upgrade_candidate_region.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
15use std::any::Any;
16use std::collections::HashSet;
17use std::time::Duration;
18
19use api::v1::meta::MailboxMessage;
20use common_meta::instruction::{
21    Instruction, InstructionReply, UpgradeRegion, UpgradeRegionReply, UpgradeRegionsReply,
22};
23use common_meta::lock_key::RemoteWalLock;
24use common_meta::wal_provider::extract_topic_from_wal_options;
25use common_procedure::{Context as ProcedureContext, Status};
26use common_telemetry::tracing_context::TracingContext;
27use common_telemetry::{error, info};
28use common_wal::options::WalOptions;
29use serde::{Deserialize, Serialize};
30use snafu::{OptionExt, ResultExt, ensure};
31use store_api::metric_engine_consts::METRIC_ENGINE_NAME;
32use tokio::time::{Instant, sleep};
33
34use crate::error::{self, Result};
35use crate::handler::HeartbeatMailbox;
36use crate::procedure::region_migration::update_metadata::UpdateMetadata;
37use crate::procedure::region_migration::{Context, State};
38use crate::procedure::utils::instruction_error_result;
39use crate::service::mailbox::Channel;
40
41#[derive(Debug, Serialize, Deserialize)]
42pub struct UpgradeCandidateRegion {
43    // The optimistic retry times.
44    pub(crate) optimistic_retry: usize,
45    // The retry initial interval.
46    pub(crate) retry_initial_interval: Duration,
47    // If it's true it requires the candidate region MUST replay the WAL to the latest entry id.
48    // Otherwise, it will rollback to the old leader region.
49    pub(crate) require_ready: bool,
50}
51
52impl Default for UpgradeCandidateRegion {
53    fn default() -> Self {
54        Self {
55            optimistic_retry: 3,
56            retry_initial_interval: Duration::from_millis(500),
57            require_ready: true,
58        }
59    }
60}
61
62#[async_trait::async_trait]
63#[typetag::serde]
64impl State for UpgradeCandidateRegion {
65    async fn next(
66        &mut self,
67        ctx: &mut Context,
68        procedure_ctx: &ProcedureContext,
69    ) -> Result<(Box<dyn State>, Status)> {
70        let now = Instant::now();
71
72        let topics = self.get_kafka_topics(ctx).await?;
73        if self
74            .upgrade_region_with_retry(ctx, procedure_ctx, topics)
75            .await
76        {
77            ctx.update_upgrade_candidate_region_elapsed(now);
78            Ok((Box::new(UpdateMetadata::Upgrade), Status::executing(false)))
79        } else {
80            ctx.update_upgrade_candidate_region_elapsed(now);
81            Ok((Box::new(UpdateMetadata::Rollback), Status::executing(false)))
82        }
83    }
84
85    fn as_any(&self) -> &dyn Any {
86        self
87    }
88}
89
90impl UpgradeCandidateRegion {
91    async fn get_kafka_topics(&self, ctx: &mut Context) -> Result<HashSet<String>> {
92        let table_regions = ctx.persistent_ctx.table_regions();
93        let datanode_table_values = ctx.get_from_peer_datanode_table_values().await?;
94        let mut topics = HashSet::new();
95        for (table_id, regions) in table_regions {
96            let Some(datanode_table_value) = datanode_table_values.get(&table_id) else {
97                continue;
98            };
99
100            let region_wal_options = &datanode_table_value.region_info.region_wal_options;
101
102            for region_id in regions {
103                let Some(WalOptions::Kafka(kafka_wal_options)) =
104                    region_wal_options.get(&region_id.region_number())
105                else {
106                    continue;
107                };
108                if !topics.contains(&kafka_wal_options.topic) {
109                    topics.insert(kafka_wal_options.topic.clone());
110                }
111            }
112        }
113
114        Ok(topics)
115    }
116
117    /// Builds upgrade region instruction.
118    async fn build_upgrade_region_instruction(
119        &self,
120        ctx: &mut Context,
121        replay_timeout: Duration,
122    ) -> Result<Instruction> {
123        let region_ids = ctx.persistent_ctx.region_ids.clone();
124        let datanode_table_values = ctx.get_from_peer_datanode_table_values().await?;
125        let mut region_topic = Vec::with_capacity(region_ids.len());
126        for region_id in region_ids.iter() {
127            let table_id = region_id.table_id();
128            if let Some(datanode_table_value) = datanode_table_values.get(&table_id)
129                && let Some(topic) = extract_topic_from_wal_options(
130                    *region_id,
131                    &datanode_table_value.region_info.region_wal_options,
132                )
133            {
134                let is_metric_engine =
135                    datanode_table_value.region_info.engine == METRIC_ENGINE_NAME;
136                region_topic.push((*region_id, topic, is_metric_engine));
137            }
138        }
139
140        let replay_checkpoints = ctx
141            .get_replay_checkpoints_with_topic_pruned_entry_ids(&region_topic)
142            .await?;
143        // Build upgrade regions instruction.
144        let mut upgrade_regions = Vec::with_capacity(region_ids.len());
145        for region_id in region_ids {
146            let last_entry_id = ctx
147                .volatile_ctx
148                .leader_region_last_entry_ids
149                .get(&region_id)
150                .copied();
151            let metadata_last_entry_id = ctx
152                .volatile_ctx
153                .leader_region_metadata_last_entry_ids
154                .get(&region_id)
155                .copied();
156            let checkpoint = replay_checkpoints.get(&region_id).copied();
157            upgrade_regions.push(UpgradeRegion {
158                region_id,
159                last_entry_id,
160                metadata_last_entry_id,
161                replay_timeout,
162                location_id: Some(ctx.persistent_ctx.from_peer.id),
163                replay_entry_id: checkpoint.map(|c| c.entry_id),
164                metadata_replay_entry_id: checkpoint.and_then(|c| c.metadata_entry_id),
165            });
166        }
167
168        Ok(Instruction::UpgradeRegions(upgrade_regions))
169    }
170
171    fn handle_upgrade_region_reply(
172        &self,
173        ctx: &mut Context,
174        UpgradeRegionReply {
175            region_id,
176            ready,
177            exists,
178            error,
179        }: &UpgradeRegionReply,
180        now: &Instant,
181    ) -> Result<()> {
182        let candidate = &ctx.persistent_ctx.to_peer;
183        if let Some(error) = error {
184            return instruction_error_result(
185                error,
186                format!(
187                    "Failed to upgrade the region {} on datanode {:?}, error: {:?}, elapsed: {:?}",
188                    region_id,
189                    candidate,
190                    error,
191                    now.elapsed()
192                ),
193            );
194        }
195
196        ensure!(
197            exists,
198            error::UnexpectedSnafu {
199                violated: format!(
200                    "Candidate region {} doesn't exist on datanode {:?}",
201                    region_id, candidate
202                )
203            }
204        );
205
206        if self.require_ready && !ready {
207            return error::RetryLaterSnafu {
208                reason: format!(
209                    "Candidate region {} still replaying the wal on datanode {:?}, elapsed: {:?}",
210                    region_id,
211                    candidate,
212                    now.elapsed()
213                ),
214            }
215            .fail();
216        }
217
218        Ok(())
219    }
220
221    /// Tries to upgrade a candidate region.
222    ///
223    /// Retry:
224    /// - If `require_ready` is true, but the candidate region returns `ready` is false.
225    /// - [MailboxTimeout](error::Error::MailboxTimeout), Timeout.
226    ///
227    /// Abort:
228    /// - The candidate region doesn't exist.
229    /// - [PusherNotFound](error::Error::PusherNotFound), The datanode is unreachable.
230    /// - [PushMessage](error::Error::PushMessage), The receiver is dropped.
231    /// - [MailboxReceiver](error::Error::MailboxReceiver), The sender is dropped without sending (impossible).
232    /// - [UnexpectedInstructionReply](error::Error::UnexpectedInstructionReply) (impossible).
233    /// - [ExceededDeadline](error::Error::ExceededDeadline)
234    /// - Invalid JSON (impossible).
235    async fn upgrade_region(&self, ctx: &mut Context) -> Result<()> {
236        let operation_timeout =
237            ctx.next_operation_timeout()
238                .context(error::ExceededDeadlineSnafu {
239                    operation: "Upgrade region",
240                })?;
241        let upgrade_instruction = self
242            .build_upgrade_region_instruction(ctx, operation_timeout)
243            .await?;
244
245        let pc = &ctx.persistent_ctx;
246        let region_ids = &pc.region_ids;
247        let candidate = &pc.to_peer;
248
249        let tracing_ctx = TracingContext::from_current_span();
250        let msg = MailboxMessage::json_message(
251            &format!("Upgrade candidate regions: {:?}", region_ids),
252            &format!("Metasrv@{}", ctx.server_addr()),
253            &format!("Datanode-{}@{}", candidate.id, candidate.addr),
254            common_time::util::current_time_millis(),
255            &upgrade_instruction,
256            Some(tracing_ctx.to_w3c()),
257        )
258        .with_context(|_| error::SerializeToJsonSnafu {
259            input: upgrade_instruction.to_string(),
260        })?;
261
262        let ch = Channel::Datanode(candidate.id);
263        let receiver = ctx.mailbox.send(&ch, msg, operation_timeout).await?;
264
265        let now = Instant::now();
266        match receiver.await {
267            Ok(msg) => {
268                let reply = HeartbeatMailbox::json_reply(&msg)?;
269                info!(
270                    "Received upgrade region reply: {:?}, regions: {:?}, elapsed: {:?}",
271                    reply,
272                    region_ids,
273                    now.elapsed()
274                );
275                let InstructionReply::UpgradeRegions(UpgradeRegionsReply { replies }) = reply
276                else {
277                    return error::UnexpectedInstructionReplySnafu {
278                        mailbox_message: msg.to_string(),
279                        reason: "Unexpected reply of the upgrade region instruction",
280                    }
281                    .fail();
282                };
283                for reply in replies {
284                    self.handle_upgrade_region_reply(ctx, &reply, &now)?;
285                }
286                Ok(())
287            }
288            Err(error::Error::MailboxTimeout { .. }) => {
289                let reason = format!(
290                    "Mailbox received timeout for upgrade candidate regions {region_ids:?} on datanode {:?}, elapsed: {:?}",
291                    candidate,
292                    now.elapsed()
293                );
294                error::RetryLaterSnafu { reason }.fail()
295            }
296            Err(err) => Err(err),
297        }
298    }
299
300    /// Upgrades a candidate region.
301    ///
302    /// Returns true if the candidate region is upgraded successfully.
303    async fn upgrade_region_with_retry(
304        &self,
305        ctx: &mut Context,
306        procedure_ctx: &ProcedureContext,
307        topics: HashSet<String>,
308    ) -> bool {
309        let mut retry = 0;
310        let mut upgraded = false;
311
312        // If using Kafka WAL, hold a read lock on each topic for all attempts to prevent WAL
313        // pruning during the upgrade. Acquire once: re-acquiring a held read lock on retry
314        // queues behind a waiting `WalPruneProcedure` writer, which waits on the first guard.
315        let mut guards = Vec::with_capacity(topics.len());
316        for topic in &topics {
317            guards.push(
318                procedure_ctx
319                    .provider
320                    .acquire_lock(&(RemoteWalLock::Read(topic.clone()).into()))
321                    .await,
322            );
323        }
324
325        loop {
326            let timer = Instant::now();
327            if let Err(err) = self.upgrade_region(ctx).await {
328                retry += 1;
329                ctx.update_operations_elapsed(timer);
330                if matches!(err, error::Error::ExceededDeadline { .. }) {
331                    error!("Failed to upgrade region, exceeded deadline");
332                    break;
333                } else if err.is_retryable() && retry < self.optimistic_retry {
334                    error!("Failed to upgrade region, error: {err:?}, retry later");
335                    sleep(self.retry_initial_interval).await;
336                } else {
337                    error!("Failed to upgrade region, error: {err:?}");
338                    break;
339                }
340            } else {
341                ctx.update_operations_elapsed(timer);
342                upgraded = true;
343                break;
344            }
345        }
346
347        upgraded
348    }
349}
350
351#[cfg(test)]
352mod tests {
353    use std::assert_matches;
354    use std::collections::HashMap;
355
356    use common_meta::key::table_route::TableRouteValue;
357    use common_meta::key::test_utils::new_test_table_info;
358    use common_meta::key::topic_name::TopicNameKey;
359    use common_meta::key::topic_region::{ReplayCheckpoint, TopicRegionKey, TopicRegionValue};
360    use common_meta::peer::Peer;
361    use common_meta::rpc::router::{Region, RegionRoute};
362    use common_meta::wal_provider::RegionWalOptions;
363    use common_wal::options::KafkaWalOptions;
364    use store_api::storage::RegionId;
365
366    use super::*;
367    use crate::error::Error;
368    use crate::procedure::region_migration::test_util::{TestingEnv, new_procedure_context};
369    use crate::procedure::region_migration::{
370        ContextFactory, PersistentContext, RegionMigrationTriggerReason,
371    };
372    use crate::procedure::test_util::{
373        new_close_region_reply, new_upgrade_region_reply, send_mock_reply,
374    };
375
376    fn new_persistent_context() -> PersistentContext {
377        PersistentContext::new(
378            vec![("greptime".into(), "public".into())],
379            Peer::empty(1),
380            Peer::empty(2),
381            vec![RegionId::new(1024, 1)],
382            Duration::from_millis(1000),
383            RegionMigrationTriggerReason::Unknown,
384        )
385    }
386
387    fn kafka_wal_options(topic: &str) -> RegionWalOptions {
388        RegionWalOptions::from([(
389            1,
390            WalOptions::Kafka(KafkaWalOptions::new(topic.to_string())),
391        )])
392    }
393
394    async fn prepare_table_metadata(ctx: &Context, wal_options: RegionWalOptions) {
395        prepare_table_metadata_with_engine(ctx, wal_options, "engine").await;
396    }
397
398    async fn prepare_table_metadata_with_engine(
399        ctx: &Context,
400        wal_options: RegionWalOptions,
401        engine: &str,
402    ) {
403        let region_id = ctx.persistent_ctx.region_ids[0];
404        let mut table_info = new_test_table_info(region_id.table_id());
405        table_info.meta.engine = engine.to_string();
406        let region_routes = vec![RegionRoute {
407            region: Region::new_test(region_id),
408            leader_peer: Some(ctx.persistent_ctx.from_peer.clone()),
409            follower_peers: vec![ctx.persistent_ctx.to_peer.clone()],
410            ..Default::default()
411        }];
412        ctx.table_metadata_manager
413            .create_table_metadata(
414                table_info,
415                TableRouteValue::physical(region_routes),
416                wal_options,
417            )
418            .await
419            .unwrap();
420    }
421
422    #[tokio::test]
423    async fn test_build_upgrade_region_instruction_merges_topic_pruned_entry_id() {
424        let state = UpgradeCandidateRegion::default();
425        let persistent_context = new_persistent_context();
426        let env = TestingEnv::new();
427        let mut ctx = env.context_factory().new_context(persistent_context);
428        let region_id = ctx.persistent_ctx.region_ids[0];
429        let topic = "test_topic";
430        prepare_table_metadata(&ctx, kafka_wal_options(topic)).await;
431        ctx.table_metadata_manager
432            .topic_region_manager()
433            .batch_put(&[(
434                TopicRegionKey::new(region_id, topic),
435                Some(TopicRegionValue::new(Some(ReplayCheckpoint::new(10, None)))),
436            )])
437            .await
438            .unwrap();
439        ctx.table_metadata_manager
440            .topic_name_manager()
441            .batch_put(vec![TopicNameKey::new(topic)])
442            .await
443            .unwrap();
444        let prev = ctx
445            .table_metadata_manager
446            .topic_name_manager()
447            .get(topic)
448            .await
449            .unwrap();
450        ctx.table_metadata_manager
451            .topic_name_manager()
452            .update(topic, 20, prev)
453            .await
454            .unwrap();
455
456        let instruction = state
457            .build_upgrade_region_instruction(&mut ctx, Duration::from_secs(1))
458            .await
459            .unwrap();
460        let Instruction::UpgradeRegions(upgrade_regions) = instruction else {
461            unreachable!()
462        };
463
464        assert_eq!(upgrade_regions.len(), 1);
465        assert_eq!(upgrade_regions[0].replay_entry_id, Some(20));
466        assert_eq!(upgrade_regions[0].metadata_replay_entry_id, None);
467    }
468
469    #[tokio::test]
470    async fn test_build_upgrade_region_instruction_merges_metric_metadata_pruned_entry_id() {
471        let state = UpgradeCandidateRegion::default();
472        let persistent_context = new_persistent_context();
473        let env = TestingEnv::new();
474        let mut ctx = env.context_factory().new_context(persistent_context);
475        let region_id = ctx.persistent_ctx.region_ids[0];
476        let topic = "test_topic";
477        prepare_table_metadata_with_engine(&ctx, kafka_wal_options(topic), METRIC_ENGINE_NAME)
478            .await;
479        ctx.table_metadata_manager
480            .topic_region_manager()
481            .batch_put(&[(
482                TopicRegionKey::new(region_id, topic),
483                Some(TopicRegionValue::new(Some(ReplayCheckpoint::new(
484                    10,
485                    Some(5),
486                )))),
487            )])
488            .await
489            .unwrap();
490        ctx.table_metadata_manager
491            .topic_name_manager()
492            .batch_put(vec![TopicNameKey::new(topic)])
493            .await
494            .unwrap();
495        let prev = ctx
496            .table_metadata_manager
497            .topic_name_manager()
498            .get(topic)
499            .await
500            .unwrap();
501        ctx.table_metadata_manager
502            .topic_name_manager()
503            .update(topic, 20, prev)
504            .await
505            .unwrap();
506
507        let instruction = state
508            .build_upgrade_region_instruction(&mut ctx, Duration::from_secs(1))
509            .await
510            .unwrap();
511        let Instruction::UpgradeRegions(upgrade_regions) = instruction else {
512            unreachable!()
513        };
514
515        assert_eq!(upgrade_regions.len(), 1);
516        assert_eq!(upgrade_regions[0].replay_entry_id, Some(20));
517        assert_eq!(upgrade_regions[0].metadata_replay_entry_id, Some(20));
518    }
519
520    #[tokio::test]
521    async fn test_datanode_is_unreachable() {
522        let state = UpgradeCandidateRegion::default();
523        let persistent_context = new_persistent_context();
524        let env = TestingEnv::new();
525        let mut ctx = env.context_factory().new_context(persistent_context);
526        prepare_table_metadata(&ctx, HashMap::default()).await;
527        let err = state.upgrade_region(&mut ctx).await.unwrap_err();
528
529        assert_matches!(err, Error::PusherNotFound { .. });
530        assert!(!err.is_retryable());
531    }
532
533    #[tokio::test]
534    async fn test_pusher_dropped() {
535        let state = UpgradeCandidateRegion::default();
536        let persistent_context = new_persistent_context();
537        let to_peer_id = persistent_context.to_peer.id;
538
539        let mut env = TestingEnv::new();
540        let mut ctx = env.context_factory().new_context(persistent_context);
541        prepare_table_metadata(&ctx, HashMap::default()).await;
542        let mailbox_ctx = env.mailbox_context();
543
544        let (tx, rx) = tokio::sync::mpsc::channel(1);
545
546        mailbox_ctx
547            .insert_heartbeat_response_receiver(Channel::Datanode(to_peer_id), tx)
548            .await;
549
550        drop(rx);
551
552        let err = state.upgrade_region(&mut ctx).await.unwrap_err();
553
554        assert_matches!(err, Error::PushMessage { .. });
555        assert!(!err.is_retryable());
556    }
557
558    #[tokio::test]
559    async fn test_procedure_exceeded_deadline() {
560        let state = UpgradeCandidateRegion::default();
561        let persistent_context = new_persistent_context();
562        let env = TestingEnv::new();
563        let mut ctx = env.context_factory().new_context(persistent_context);
564        prepare_table_metadata(&ctx, HashMap::default()).await;
565        ctx.volatile_ctx.metrics.operations_elapsed =
566            ctx.persistent_ctx.timeout + Duration::from_secs(1);
567
568        let err = state.upgrade_region(&mut ctx).await.unwrap_err();
569
570        assert_matches!(err, Error::ExceededDeadline { .. });
571        assert!(!err.is_retryable());
572    }
573
574    #[tokio::test]
575    async fn test_unexpected_instruction_reply() {
576        let state = UpgradeCandidateRegion::default();
577        let persistent_context = new_persistent_context();
578        let to_peer_id = persistent_context.to_peer.id;
579
580        let mut env = TestingEnv::new();
581        let mut ctx = env.context_factory().new_context(persistent_context);
582        prepare_table_metadata(&ctx, HashMap::default()).await;
583        let mailbox_ctx = env.mailbox_context();
584        let mailbox = mailbox_ctx.mailbox().clone();
585
586        let (tx, rx) = tokio::sync::mpsc::channel(1);
587
588        mailbox_ctx
589            .insert_heartbeat_response_receiver(Channel::Datanode(to_peer_id), tx)
590            .await;
591
592        send_mock_reply(mailbox, rx, |id| Ok(new_close_region_reply(id)));
593
594        let err = state.upgrade_region(&mut ctx).await.unwrap_err();
595        assert_matches!(err, Error::UnexpectedInstructionReply { .. });
596        assert!(!err.is_retryable());
597    }
598
599    #[tokio::test]
600    async fn test_upgrade_region_failed() {
601        let state = UpgradeCandidateRegion::default();
602        let persistent_context = new_persistent_context();
603        let to_peer_id = persistent_context.to_peer.id;
604
605        let mut env = TestingEnv::new();
606        let mut ctx = env.context_factory().new_context(persistent_context);
607        prepare_table_metadata(&ctx, HashMap::default()).await;
608        let mailbox_ctx = env.mailbox_context();
609        let mailbox = mailbox_ctx.mailbox().clone();
610
611        let (tx, rx) = tokio::sync::mpsc::channel(1);
612
613        mailbox_ctx
614            .insert_heartbeat_response_receiver(Channel::Datanode(to_peer_id), tx)
615            .await;
616
617        // A reply contains an error.
618        send_mock_reply(mailbox, rx, |id| {
619            Ok(new_upgrade_region_reply(
620                id,
621                true,
622                true,
623                Some("test mocked".to_string()),
624            ))
625        });
626
627        let err = state.upgrade_region(&mut ctx).await.unwrap_err();
628
629        assert_matches!(err, Error::RetryLater { .. });
630        assert!(err.is_retryable());
631        assert!(format!("{err:?}").contains("test mocked"));
632    }
633
634    #[tokio::test]
635    async fn test_upgrade_region_not_found() {
636        let state = UpgradeCandidateRegion::default();
637        let persistent_context = new_persistent_context();
638        let to_peer_id = persistent_context.to_peer.id;
639
640        let mut env = TestingEnv::new();
641        let mut ctx = env.context_factory().new_context(persistent_context);
642        prepare_table_metadata(&ctx, HashMap::default()).await;
643        let mailbox_ctx = env.mailbox_context();
644        let mailbox = mailbox_ctx.mailbox().clone();
645
646        let (tx, rx) = tokio::sync::mpsc::channel(1);
647
648        mailbox_ctx
649            .insert_heartbeat_response_receiver(Channel::Datanode(to_peer_id), tx)
650            .await;
651
652        send_mock_reply(mailbox, rx, |id| {
653            Ok(new_upgrade_region_reply(id, true, false, None))
654        });
655
656        let err = state.upgrade_region(&mut ctx).await.unwrap_err();
657
658        assert_matches!(err, Error::Unexpected { .. });
659        assert!(!err.is_retryable());
660        assert!(err.to_string().contains("doesn't exist"));
661    }
662
663    #[tokio::test]
664    async fn test_upgrade_region_require_ready() {
665        let mut state = UpgradeCandidateRegion {
666            require_ready: true,
667            ..Default::default()
668        };
669
670        let persistent_context = new_persistent_context();
671        let to_peer_id = persistent_context.to_peer.id;
672
673        let mut env = TestingEnv::new();
674        let mut ctx = env.context_factory().new_context(persistent_context);
675        prepare_table_metadata(&ctx, HashMap::default()).await;
676        let mailbox_ctx = env.mailbox_context();
677        let mailbox = mailbox_ctx.mailbox().clone();
678
679        let (tx, rx) = tokio::sync::mpsc::channel(1);
680
681        mailbox_ctx
682            .insert_heartbeat_response_receiver(Channel::Datanode(to_peer_id), tx)
683            .await;
684
685        send_mock_reply(mailbox, rx, |id| {
686            Ok(new_upgrade_region_reply(id, false, true, None))
687        });
688
689        let err = state.upgrade_region(&mut ctx).await.unwrap_err();
690
691        assert_matches!(err, Error::RetryLater { .. });
692        assert!(err.is_retryable());
693        assert!(format!("{err:?}").contains("still replaying the wal"));
694
695        // Sets the `require_ready` to false.
696        state.require_ready = false;
697
698        let mailbox = mailbox_ctx.mailbox().clone();
699        let (tx, rx) = tokio::sync::mpsc::channel(1);
700
701        mailbox_ctx
702            .insert_heartbeat_response_receiver(Channel::Datanode(to_peer_id), tx)
703            .await;
704
705        send_mock_reply(mailbox, rx, |id| {
706            Ok(new_upgrade_region_reply(id, false, true, None))
707        });
708
709        state.upgrade_region(&mut ctx).await.unwrap();
710    }
711
712    #[tokio::test]
713    async fn test_upgrade_region_with_retry_ok() {
714        let mut state = Box::<UpgradeCandidateRegion>::default();
715        state.retry_initial_interval = Duration::from_millis(100);
716        let persistent_context = new_persistent_context();
717        let to_peer_id = persistent_context.to_peer.id;
718
719        let mut env = TestingEnv::new();
720        let mut ctx = env.context_factory().new_context(persistent_context);
721        prepare_table_metadata(&ctx, HashMap::default()).await;
722        let mailbox_ctx = env.mailbox_context();
723        let mailbox = mailbox_ctx.mailbox().clone();
724
725        let (tx, mut rx) = tokio::sync::mpsc::channel(1);
726
727        mailbox_ctx
728            .insert_heartbeat_response_receiver(Channel::Datanode(to_peer_id), tx)
729            .await;
730
731        common_runtime::spawn_global(async move {
732            let resp = rx.recv().await.unwrap().unwrap();
733            let reply_id = resp.mailbox_message.unwrap().id;
734            mailbox
735                .on_recv(
736                    reply_id,
737                    Err(error::MailboxTimeoutSnafu { id: reply_id }.build()),
738                )
739                .await
740                .unwrap();
741
742            // retry: 1
743            let resp = rx.recv().await.unwrap().unwrap();
744            let reply_id = resp.mailbox_message.unwrap().id;
745            mailbox
746                .on_recv(
747                    reply_id,
748                    Ok(new_upgrade_region_reply(reply_id, false, true, None)),
749                )
750                .await
751                .unwrap();
752
753            // retry: 2
754            let resp = rx.recv().await.unwrap().unwrap();
755            let reply_id = resp.mailbox_message.unwrap().id;
756            mailbox
757                .on_recv(
758                    reply_id,
759                    Ok(new_upgrade_region_reply(reply_id, true, true, None)),
760                )
761                .await
762                .unwrap();
763        });
764
765        let procedure_ctx = new_procedure_context();
766        let (next, _) = state.next(&mut ctx, &procedure_ctx).await.unwrap();
767
768        let update_metadata = next.as_any().downcast_ref::<UpdateMetadata>().unwrap();
769
770        assert_matches!(update_metadata, UpdateMetadata::Upgrade);
771    }
772
773    #[tokio::test]
774    async fn test_upgrade_region_with_retry_failed() {
775        let mut state = Box::<UpgradeCandidateRegion>::default();
776        state.retry_initial_interval = Duration::from_millis(100);
777        let persistent_context = new_persistent_context();
778        let to_peer_id = persistent_context.to_peer.id;
779
780        let mut env = TestingEnv::new();
781        let mut ctx = env.context_factory().new_context(persistent_context);
782        prepare_table_metadata(&ctx, HashMap::default()).await;
783        let mailbox_ctx = env.mailbox_context();
784        let mailbox = mailbox_ctx.mailbox().clone();
785
786        let (tx, mut rx) = tokio::sync::mpsc::channel(1);
787
788        mailbox_ctx
789            .insert_heartbeat_response_receiver(Channel::Datanode(to_peer_id), tx)
790            .await;
791
792        common_runtime::spawn_global(async move {
793            let resp = rx.recv().await.unwrap().unwrap();
794            let reply_id = resp.mailbox_message.unwrap().id;
795            mailbox
796                .on_recv(
797                    reply_id,
798                    Err(error::MailboxTimeoutSnafu { id: reply_id }.build()),
799                )
800                .await
801                .unwrap();
802
803            // retry: 1
804            let resp = rx.recv().await.unwrap().unwrap();
805            let reply_id = resp.mailbox_message.unwrap().id;
806            mailbox
807                .on_recv(
808                    reply_id,
809                    Ok(new_upgrade_region_reply(reply_id, false, true, None)),
810                )
811                .await
812                .unwrap();
813
814            // retry: 2
815            let resp = rx.recv().await.unwrap().unwrap();
816            let reply_id = resp.mailbox_message.unwrap().id;
817            mailbox
818                .on_recv(
819                    reply_id,
820                    Ok(new_upgrade_region_reply(reply_id, false, false, None)),
821                )
822                .await
823                .unwrap();
824        });
825        let procedure_ctx = new_procedure_context();
826        let (next, _) = state.next(&mut ctx, &procedure_ctx).await.unwrap();
827
828        let update_metadata = next.as_any().downcast_ref::<UpdateMetadata>().unwrap();
829        assert_matches!(update_metadata, UpdateMetadata::Rollback);
830    }
831
832    #[tokio::test]
833    async fn test_upgrade_region_procedure_exceeded_deadline() {
834        let mut state = Box::<UpgradeCandidateRegion>::default();
835        state.retry_initial_interval = Duration::from_millis(100);
836        let persistent_context = new_persistent_context();
837        let to_peer_id = persistent_context.to_peer.id;
838
839        let mut env = TestingEnv::new();
840        let mut ctx = env.context_factory().new_context(persistent_context);
841        prepare_table_metadata(&ctx, HashMap::default()).await;
842        let mailbox_ctx = env.mailbox_context();
843        let mailbox = mailbox_ctx.mailbox().clone();
844        ctx.volatile_ctx.metrics.operations_elapsed =
845            ctx.persistent_ctx.timeout + Duration::from_secs(1);
846
847        let (tx, rx) = tokio::sync::mpsc::channel(1);
848        mailbox_ctx
849            .insert_heartbeat_response_receiver(Channel::Datanode(to_peer_id), tx)
850            .await;
851
852        send_mock_reply(mailbox, rx, |id| {
853            Ok(new_upgrade_region_reply(id, false, true, None))
854        });
855        let procedure_ctx = new_procedure_context();
856        let (next, _) = state.next(&mut ctx, &procedure_ctx).await.unwrap();
857        let update_metadata = next.as_any().downcast_ref::<UpdateMetadata>().unwrap();
858        assert_matches!(update_metadata, UpdateMetadata::Rollback);
859    }
860}
861
862/// Regression tests for a deadlock between the upgrade step's per-retry `RemoteWalLock::Read`
863/// (now acquired once before the retry loop) and `WalPruneProcedure`'s `RemoteWalLock::Write`
864/// on the same topic.
865///
866/// Mirrors a datanode rolling restart: failover sends the upgrade instruction to a candidate
867/// datanode that never answers, the mailbox times out (retryable), and the periodic WAL prune
868/// for the region's topic starts while the upgrade is waiting. Both procedures share one
869/// `ContextProvider`, as they do inside metasrv's procedure manager.
870///
871/// Requires a Kafka broker: `GT_KAFKA_ENDPOINTS=127.0.0.1:9092`.
872#[cfg(test)]
873mod wal_prune_deadlock_tests {
874    use std::assert_matches;
875    use std::sync::Arc;
876    use std::sync::atomic::{AtomicUsize, Ordering};
877
878    use common_meta::key::table_route::TableRouteValue;
879    use common_meta::key::test_utils::new_test_table_info;
880    use common_meta::peer::Peer;
881    use common_meta::region_registry::LeaderRegionRegistry;
882    use common_meta::rpc::router::{Region, RegionRoute};
883    use common_meta::wal_provider::{RegionWalOptions, build_kafka_client};
884    use common_procedure::Procedure;
885    use common_wal::config::kafka::common::KafkaConnectionConfig;
886    use common_wal::maybe_skip_kafka_integration_test;
887    use common_wal::options::KafkaWalOptions;
888    use common_wal::test_util::get_kafka_endpoints;
889    use store_api::storage::RegionId;
890    use tokio::time::timeout;
891
892    use super::*;
893    use crate::procedure::region_migration::test_util::{TestingEnv, new_procedure_context};
894    use crate::procedure::region_migration::{
895        ContextFactory, PersistentContext, RegionMigrationTriggerReason,
896    };
897    use crate::procedure::wal_prune::{Context as WalPruneContext, WalPruneProcedure};
898
899    struct Scenario {
900        env: TestingEnv,
901        ctx: Context,
902        topic: String,
903        upgrade_instructions: Arc<AtomicUsize>,
904    }
905
906    /// One region on a Kafka topic, failing over to a datanode that receives the upgrade
907    /// instruction but never replies (it is restarting).
908    async fn setup(name: &str) -> Scenario {
909        let topic = format!("{name}_{}", uuid::Uuid::new_v4());
910        let region_id = RegionId::new(1024, 1);
911        let persistent_ctx = PersistentContext::new(
912            vec![("greptime".into(), "public".into())],
913            Peer::empty(1),
914            Peer::empty(2),
915            vec![region_id],
916            // The migration timeout; the first upgrade attempt consumes all of it.
917            Duration::from_millis(1000),
918            RegionMigrationTriggerReason::Failover,
919        );
920        let to_peer_id = persistent_ctx.to_peer.id;
921
922        let mut env = TestingEnv::new();
923        let ctx = env.context_factory().new_context(persistent_ctx);
924        let region_routes = vec![RegionRoute {
925            region: Region::new_test(region_id),
926            leader_peer: Some(ctx.persistent_ctx.from_peer.clone()),
927            follower_peers: vec![ctx.persistent_ctx.to_peer.clone()],
928            ..Default::default()
929        }];
930        let wal_options = RegionWalOptions::from([(
931            region_id.region_number(),
932            WalOptions::Kafka(KafkaWalOptions::new(topic.clone())),
933        )]);
934        ctx.table_metadata_manager
935            .create_table_metadata(
936                new_test_table_info(region_id.table_id()),
937                TableRouteValue::physical(region_routes),
938                wal_options,
939            )
940            .await
941            .unwrap();
942
943        let (tx, mut rx) = tokio::sync::mpsc::channel(8);
944        env.mailbox_context()
945            .insert_heartbeat_response_receiver(Channel::Datanode(to_peer_id), tx)
946            .await;
947        let upgrade_instructions = Arc::new(AtomicUsize::new(0));
948        let counter = upgrade_instructions.clone();
949        common_runtime::spawn_global(async move {
950            while let Some(Ok(_)) = rx.recv().await {
951                counter.fetch_add(1, Ordering::SeqCst);
952            }
953        });
954
955        Scenario {
956            env,
957            ctx,
958            topic,
959            upgrade_instructions,
960        }
961    }
962
963    async fn wait_for_first_instruction(counter: &AtomicUsize) {
964        timeout(Duration::from_secs(5), async {
965            while counter.load(Ordering::SeqCst) == 0 {
966                sleep(Duration::from_millis(10)).await;
967            }
968        })
969        .await
970        .unwrap();
971    }
972
973    #[tokio::test]
974    async fn test_upgrade_retry_does_not_deadlock_with_wal_prune() {
975        maybe_skip_kafka_integration_test!();
976        let Scenario {
977            env,
978            mut ctx,
979            topic,
980            upgrade_instructions,
981        } = setup("upgrade_wal_prune_deadlock").await;
982        // Metasrv shares one provider (and dynamic key lock) across all procedures.
983        let procedure_ctx = new_procedure_context();
984
985        let migration_procedure_ctx = procedure_ctx.clone();
986        let migration = tokio::spawn(async move {
987            let mut state = Box::<UpgradeCandidateRegion>::default();
988            state.next(&mut ctx, &migration_procedure_ctx).await
989        });
990        // The upgrade step now holds `RemoteWalLock::Read(topic)` and waits on the datanode.
991        wait_for_first_instruction(&upgrade_instructions).await;
992
993        // The periodic WAL prune for the region's topic starts.
994        let client = build_kafka_client(&KafkaConnectionConfig {
995            broker_endpoints: get_kafka_endpoints(),
996            ..Default::default()
997        })
998        .await
999        .unwrap();
1000        client
1001            .controller_client()
1002            .unwrap()
1003            .create_topic(&topic, 1, 1, 5000)
1004            .await
1005            .unwrap();
1006        let mut wal_prune = WalPruneProcedure::new(
1007            WalPruneContext {
1008                client: Arc::new(client),
1009                table_metadata_manager: env.table_metadata_manager().clone(),
1010                leader_region_registry: Arc::new(LeaderRegionRegistry::new()),
1011            },
1012            None,
1013            topic,
1014            0,
1015            false,
1016        );
1017        let wal_prune_procedure_ctx = procedure_ctx.clone();
1018        let wal_prune =
1019            tokio::spawn(async move { wal_prune.execute(&wal_prune_procedure_ctx).await });
1020
1021        // The mailbox times out after ~1s (retryable). The retry keeps the read lock it already
1022        // holds, fails fast with `ExceededDeadline`, and the migration rolls back. Releasing the
1023        // read lock then lets the queued WAL prune run.
1024        let (next, _) = timeout(Duration::from_secs(10), migration)
1025            .await
1026            .expect("upgrade step should not deadlock")
1027            .unwrap()
1028            .unwrap();
1029        let update_metadata = next.as_any().downcast_ref::<UpdateMetadata>().unwrap();
1030        assert_matches!(update_metadata, UpdateMetadata::Rollback);
1031        assert_eq!(1, upgrade_instructions.load(Ordering::SeqCst));
1032
1033        timeout(Duration::from_secs(10), wal_prune)
1034            .await
1035            .expect("WAL prune should not deadlock")
1036            .unwrap()
1037            .unwrap();
1038    }
1039
1040    #[tokio::test]
1041    async fn test_upgrade_retry_without_wal_prune_rolls_back() {
1042        maybe_skip_kafka_integration_test!();
1043        let Scenario {
1044            mut ctx,
1045            upgrade_instructions,
1046            ..
1047        } = setup("upgrade_wal_prune_control").await;
1048        let procedure_ctx = new_procedure_context();
1049
1050        let mut state = Box::<UpgradeCandidateRegion>::default();
1051        let (next, _) = timeout(
1052            Duration::from_secs(10),
1053            state.next(&mut ctx, &procedure_ctx),
1054        )
1055        .await
1056        .expect("upgrade step should finish without WAL prune")
1057        .unwrap();
1058        let update_metadata = next.as_any().downcast_ref::<UpdateMetadata>().unwrap();
1059        assert_matches!(update_metadata, UpdateMetadata::Rollback);
1060        assert_eq!(1, upgrade_instructions.load(Ordering::SeqCst));
1061    }
1062}