Skip to main content

common_meta/
instruction.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::collections::{HashMap, HashSet};
16use std::fmt::{Display, Formatter};
17use std::time::Duration;
18
19use common_error::ext::{ErrorExt, RetryHint};
20use common_error::status_code::StatusCode;
21use serde::{Deserialize, Deserializer, Serialize, Serializer};
22use store_api::region_engine::SyncRegionFromRequest;
23use store_api::region_request::{RegionFlushReason, RegionRequirements};
24use store_api::storage::{FileRefsManifest, GcReport, RegionId, RegionNumber};
25use strum::Display;
26use table::metadata::TableId;
27use table::table_name::TableName;
28
29use crate::flow_name::FlowName;
30use crate::key::schema_name::SchemaName;
31use crate::key::{FlowId, FlowPartitionId};
32use crate::peer::Peer;
33use crate::wal_provider::{RegionWalOptions, region_wal_options_serde};
34use crate::{DatanodeId, FlownodeId};
35
36/// A structured error returned by instruction replies.
37#[derive(Debug, Serialize, PartialEq, Eq, Clone)]
38pub struct InstructionError {
39    /// Numeric status code aligned with [`StatusCode`].
40    #[serde(
41        serialize_with = "StatusCode::serialize_as_u32",
42        deserialize_with = "StatusCode::deserialize_from_u32"
43    )]
44    pub code: StatusCode,
45    /// User-facing error message aligned with [`ErrorExt::output_msg`].
46    pub message: String,
47    /// Retry hint serialized as a string.
48    #[serde(
49        serialize_with = "RetryHint::serialize_as_str",
50        deserialize_with = "RetryHint::deserialize_from_str"
51    )]
52    pub retry_hint: RetryHint,
53}
54
55impl<'de> Deserialize<'de> for InstructionError {
56    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
57    where
58        D: Deserializer<'de>,
59    {
60        #[derive(Deserialize)]
61        #[serde(untagged)]
62        enum Compat {
63            Structured {
64                #[serde(deserialize_with = "StatusCode::deserialize_from_u32")]
65                code: StatusCode,
66                message: String,
67                #[serde(deserialize_with = "RetryHint::deserialize_from_str")]
68                retry_hint: RetryHint,
69            },
70            Legacy(String),
71        }
72
73        match Compat::deserialize(deserializer)? {
74            Compat::Structured {
75                code,
76                message,
77                retry_hint,
78            } => Ok(Self {
79                code,
80                message,
81                retry_hint,
82            }),
83            Compat::Legacy(message) => Ok(Self::legacy_internal_retryable(message)),
84        }
85    }
86}
87
88impl InstructionError {
89    pub fn new(code: StatusCode, message: impl Into<String>, retry_hint: RetryHint) -> Self {
90        Self {
91            code,
92            message: message.into(),
93            retry_hint,
94        }
95    }
96
97    pub fn legacy_internal_retryable(message: impl Into<String>) -> Self {
98        Self::new(StatusCode::Internal, message, RetryHint::Retryable)
99    }
100
101    pub fn from_error<E: ErrorExt>(error: &E) -> Self {
102        Self {
103            code: error.status_code(),
104            message: error.output_msg(),
105            retry_hint: error.retry_hint(),
106        }
107    }
108}
109
110impl Display for InstructionError {
111    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
112        write!(
113            f,
114            "InstructionError(code={}, retry_hint={}, message={})",
115            self.code as u32,
116            self.retry_hint.as_str(),
117            self.message
118        )
119    }
120}
121
122pub type InstructionResult<T> = std::result::Result<T, InstructionError>;
123
124#[derive(Eq, Hash, PartialEq, Clone, Debug, Serialize, Deserialize)]
125pub struct RegionIdent {
126    pub datanode_id: DatanodeId,
127    pub table_id: TableId,
128    pub region_number: RegionNumber,
129    pub engine: String,
130}
131
132impl RegionIdent {
133    pub fn get_region_id(&self) -> RegionId {
134        RegionId::new(self.table_id, self.region_number)
135    }
136}
137
138impl Display for RegionIdent {
139    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
140        write!(
141            f,
142            "RegionIdent(datanode_id='{}', table_id={}, region_number={}, engine = {})",
143            self.datanode_id, self.table_id, self.region_number, self.engine
144        )
145    }
146}
147
148/// The result of downgrade leader region.
149#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
150pub struct DowngradeRegionReply {
151    /// The [RegionId].
152    /// For compatibility, it is defaulted to [RegionId::new(0, 0)].
153    #[serde(default)]
154    pub region_id: RegionId,
155    /// Returns the `last_entry_id` if available.
156    pub last_entry_id: Option<u64>,
157    /// Returns the `metadata_last_entry_id` if available (Only available for metric engine).
158    pub metadata_last_entry_id: Option<u64>,
159    /// Indicates whether the region exists.
160    pub exists: bool,
161    /// Return error if any during the operation.
162    pub error: Option<InstructionError>,
163}
164
165impl Display for DowngradeRegionReply {
166    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
167        write!(
168            f,
169            "(last_entry_id={:?}, exists={}, error={:?})",
170            self.last_entry_id, self.exists, self.error
171        )
172    }
173}
174
175#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
176pub struct SimpleReply {
177    pub result: bool,
178    pub error: Option<InstructionError>,
179}
180
181/// Reply for flush region operations with support for batch results.
182#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
183pub struct FlushRegionReply {
184    /// Results for each region that was attempted to be flushed.
185    /// For single region flushes, this will contain one result.
186    /// For batch flushes, this contains results for all attempted regions.
187    pub results: Vec<(RegionId, InstructionResult<()>)>,
188    /// Overall success: true if all regions were flushed successfully.
189    pub overall_success: bool,
190}
191
192impl FlushRegionReply {
193    /// Create a successful single region reply.
194    pub fn success_single(region_id: RegionId) -> Self {
195        Self {
196            results: vec![(region_id, Ok(()))],
197            overall_success: true,
198        }
199    }
200
201    /// Create a failed single region reply.
202    pub fn error_single(region_id: RegionId, error: InstructionError) -> Self {
203        Self {
204            results: vec![(region_id, Err(error))],
205            overall_success: false,
206        }
207    }
208
209    /// Create a batch reply from individual results.
210    pub fn from_results(results: Vec<(RegionId, InstructionResult<()>)>) -> Self {
211        let overall_success = results.iter().all(|(_, result)| result.is_ok());
212        Self {
213            results,
214            overall_success,
215        }
216    }
217
218    /// Convert to SimpleReply for backward compatibility.
219    pub fn to_simple_reply(&self) -> SimpleReply {
220        if self.overall_success {
221            SimpleReply {
222                result: true,
223                error: None,
224            }
225        } else {
226            let errors: Vec<String> = self
227                .results
228                .iter()
229                .filter_map(|(region_id, result)| {
230                    result
231                        .as_ref()
232                        .err()
233                        .map(|err| format!("{}: {}", region_id, err))
234                })
235                .collect();
236            SimpleReply {
237                result: false,
238                error: Some(InstructionError::legacy_internal_retryable(
239                    errors.join("; "),
240                )),
241            }
242        }
243    }
244}
245
246impl Display for SimpleReply {
247    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
248        write!(f, "(result={}, error={:?})", self.result, self.error)
249    }
250}
251
252impl Display for FlushRegionReply {
253    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
254        let results_str = self
255            .results
256            .iter()
257            .map(|(region_id, result)| match result {
258                Ok(()) => format!("{}:OK", region_id),
259                Err(err) => format!("{}:ERR({})", region_id, err),
260            })
261            .collect::<Vec<_>>()
262            .join(", ");
263        write!(
264            f,
265            "(overall_success={}, results=[{}])",
266            self.overall_success, results_str
267        )
268    }
269}
270
271impl Display for OpenRegion {
272    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
273        write!(
274            f,
275            "OpenRegion(region_ident={}, region_storage_path={}, reason={:?})",
276            self.region_ident, self.region_storage_path, self.reason
277        )
278    }
279}
280
281/// The reason why an open region instruction is triggered.
282#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
283pub enum OpenRegionReason {
284    /// Open triggered before region migration.
285    RegionMigration,
286    /// Open triggered by region failover.
287    RegionFailover,
288    /// Open triggered when adding a follower region.
289    #[cfg(feature = "enterprise")]
290    RegionFollower,
291}
292
293#[serde_with::serde_as]
294#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
295pub struct OpenRegion {
296    pub region_ident: RegionIdent,
297    pub region_storage_path: String,
298    pub region_options: HashMap<String, String>,
299    #[serde(default)]
300    #[serde(with = "region_wal_options_serde")]
301    pub region_wal_options: RegionWalOptions,
302    #[serde(default)]
303    pub skip_wal_replay: bool,
304    #[serde(default, skip_serializing_if = "Option::is_none")]
305    pub reason: Option<OpenRegionReason>,
306    #[serde(default)]
307    pub requirements: RegionRequirements,
308}
309
310impl OpenRegion {
311    pub fn new(
312        region_ident: RegionIdent,
313        path: &str,
314        region_options: HashMap<String, String>,
315        region_wal_options: RegionWalOptions,
316        skip_wal_replay: bool,
317        reason: Option<OpenRegionReason>,
318        requirements: RegionRequirements,
319    ) -> Self {
320        Self {
321            region_ident,
322            region_storage_path: path.to_string(),
323            region_options,
324            region_wal_options,
325            skip_wal_replay,
326            reason,
327            requirements,
328        }
329    }
330}
331
332/// The instruction of downgrading leader region.
333#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
334pub struct DowngradeRegion {
335    /// The [RegionId].
336    pub region_id: RegionId,
337    /// The timeout of waiting for flush the region.
338    ///
339    /// `None` stands for don't flush before downgrading the region.
340    #[serde(default)]
341    pub flush_timeout: Option<Duration>,
342}
343
344impl Display for DowngradeRegion {
345    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
346        write!(
347            f,
348            "DowngradeRegion(region_id={}, flush_timeout={:?})",
349            self.region_id, self.flush_timeout,
350        )
351    }
352}
353
354/// Upgrades a follower region to leader region.
355#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
356pub struct UpgradeRegion {
357    /// The [RegionId].
358    pub region_id: RegionId,
359    /// The `last_entry_id` of old leader region.
360    pub last_entry_id: Option<u64>,
361    /// The `last_entry_id` of old leader metadata region (Only used for metric engine).
362    pub metadata_last_entry_id: Option<u64>,
363    /// The timeout of waiting for a wal replay.
364    ///
365    /// `None` stands for no wait,
366    /// it's helpful to verify whether the leader region is ready.
367    #[serde(with = "humantime_serde")]
368    pub replay_timeout: Duration,
369    /// The hint for replaying memtable.
370    #[serde(default)]
371    pub location_id: Option<u64>,
372    #[serde(default, skip_serializing_if = "Option::is_none")]
373    pub replay_entry_id: Option<u64>,
374    #[serde(default, skip_serializing_if = "Option::is_none")]
375    pub metadata_replay_entry_id: Option<u64>,
376}
377
378impl UpgradeRegion {
379    /// Sets the replay entry id.
380    pub fn with_replay_entry_id(mut self, replay_entry_id: Option<u64>) -> Self {
381        self.replay_entry_id = replay_entry_id;
382        self
383    }
384
385    /// Sets the metadata replay entry id.
386    pub fn with_metadata_replay_entry_id(mut self, metadata_replay_entry_id: Option<u64>) -> Self {
387        self.metadata_replay_entry_id = metadata_replay_entry_id;
388        self
389    }
390}
391
392#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
393/// The identifier of cache.
394pub enum CacheIdent {
395    FlowId(FlowId),
396    /// Indicate change of address of flownode.
397    FlowNodeAddressChange(u64),
398    FlowName(FlowName),
399    TableId(TableId),
400    TableName(TableName),
401    SchemaName(SchemaName),
402    CreateFlow(CreateFlow),
403    DropFlow(DropFlow),
404    /// Indicate change of user metadata.
405    User(UserCacheIdent),
406}
407
408#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
409pub struct UserCacheIdent {
410    pub catalog: String,
411    pub username: String,
412}
413
414#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
415pub struct CreateFlow {
416    /// The unique identifier for the flow.
417    pub flow_id: FlowId,
418    pub source_table_ids: Vec<TableId>,
419    /// Mapping of flow partition to peer information
420    pub partition_to_peer_mapping: Vec<(FlowPartitionId, Peer)>,
421}
422
423#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
424pub struct DropFlow {
425    pub flow_id: FlowId,
426    pub source_table_ids: Vec<TableId>,
427    /// Mapping of flow partition to flownode id
428    pub flow_part2node_id: Vec<(FlowPartitionId, FlownodeId)>,
429}
430
431/// Strategy for executing flush operations.
432#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
433pub enum FlushStrategy {
434    /// Synchronous operation that waits for completion and expects a reply
435    #[default]
436    Sync,
437    /// Asynchronous hint operation (fire-and-forget, no reply expected)
438    Async,
439}
440
441/// Error handling strategy for batch flush operations.
442#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
443pub enum FlushErrorStrategy {
444    /// Abort on first error (fail-fast)
445    #[default]
446    FailFast,
447    /// Attempt to flush all regions and collect all errors
448    TryAll,
449}
450
451/// Unified flush instruction supporting both single and batch operations
452/// with configurable execution strategies and error handling.
453#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
454pub struct FlushRegions {
455    /// List of region IDs to flush. Can contain a single region or multiple regions.
456    pub region_ids: Vec<RegionId>,
457    /// Execution strategy: Sync (expects reply) or Async (fire-and-forget hint).
458    #[serde(default)]
459    pub strategy: FlushStrategy,
460    /// Error handling strategy for batch operations (only applies when multiple regions and sync strategy).
461    #[serde(default)]
462    pub error_strategy: FlushErrorStrategy,
463    /// The source that triggered this flush.
464    #[serde(default, skip_serializing_if = "Option::is_none")]
465    pub reason: Option<RegionFlushReason>,
466}
467
468impl Display for FlushRegions {
469    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
470        write!(
471            f,
472            "FlushRegions(region_ids={:?}, strategy={:?}, error_strategy={:?}, reason={:?})",
473            self.region_ids, self.strategy, self.error_strategy, self.reason
474        )
475    }
476}
477
478impl FlushRegions {
479    /// Create synchronous single-region flush
480    pub fn sync_single(region_id: RegionId) -> Self {
481        Self {
482            region_ids: vec![region_id],
483            strategy: FlushStrategy::Sync,
484            error_strategy: FlushErrorStrategy::FailFast,
485            reason: None,
486        }
487    }
488
489    /// Create asynchronous batch flush (fire-and-forget)
490    pub fn async_batch(region_ids: Vec<RegionId>) -> Self {
491        Self {
492            region_ids,
493            strategy: FlushStrategy::Async,
494            error_strategy: FlushErrorStrategy::TryAll,
495            reason: None,
496        }
497    }
498
499    /// Create synchronous batch flush with error strategy
500    pub fn sync_batch(region_ids: Vec<RegionId>, error_strategy: FlushErrorStrategy) -> Self {
501        Self {
502            region_ids,
503            strategy: FlushStrategy::Sync,
504            error_strategy,
505            reason: None,
506        }
507    }
508
509    pub fn with_reason(mut self, reason: RegionFlushReason) -> Self {
510        self.reason = Some(reason);
511        self
512    }
513
514    /// Check if this is a single region flush.
515    pub fn is_single_region(&self) -> bool {
516        self.region_ids.len() == 1
517    }
518
519    /// Get the single region ID if this is a single region flush.
520    pub fn single_region_id(&self) -> Option<RegionId> {
521        if self.is_single_region() {
522            self.region_ids.first().copied()
523        } else {
524            None
525        }
526    }
527
528    /// Check if this is a hint (asynchronous) operation.
529    pub fn is_hint(&self) -> bool {
530        matches!(self.strategy, FlushStrategy::Async)
531    }
532
533    /// Check if this is a synchronous operation.
534    pub fn is_sync(&self) -> bool {
535        matches!(self.strategy, FlushStrategy::Sync)
536    }
537}
538
539impl From<RegionId> for FlushRegions {
540    fn from(region_id: RegionId) -> Self {
541        Self::sync_single(region_id)
542    }
543}
544
545#[derive(Debug, Deserialize)]
546#[serde(untagged)]
547enum SingleOrMultiple<T> {
548    Single(T),
549    Multiple(Vec<T>),
550}
551
552fn single_or_multiple_from<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
553where
554    D: Deserializer<'de>,
555    T: Deserialize<'de>,
556{
557    let helper = SingleOrMultiple::<T>::deserialize(deserializer)?;
558    Ok(match helper {
559        SingleOrMultiple::Single(x) => vec![x],
560        SingleOrMultiple::Multiple(xs) => xs,
561    })
562}
563
564/// Instruction to get file references for specified regions.
565#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
566pub struct GetFileRefs {
567    /// List of region IDs to get file references from active FileHandles (in-memory).
568    pub query_regions: Vec<RegionId>,
569    /// Mapping from the src region IDs (whose file references to look for) to
570    /// the dst region IDs (where to read the manifests).
571    /// Key: The source region IDs (where files originally came from).
572    /// Value: The set of destination region IDs (whose manifests need to be read).
573    pub related_regions: HashMap<RegionId, HashSet<RegionId>>,
574}
575
576impl Display for GetFileRefs {
577    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
578        write!(f, "GetFileRefs(region_ids={:?})", self.query_regions)
579    }
580}
581
582/// Instruction to trigger garbage collection for a region.
583#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
584pub struct GcRegions {
585    /// The region ID to perform GC on, only regions that are currently on the given datanode can be garbage collected, regions not on the datanode will report errors.
586    pub regions: Vec<RegionId>,
587    /// The file references manifest containing temporary file references.
588    pub file_refs_manifest: FileRefsManifest,
589    /// Whether to perform a full file listing to find orphan files.
590    pub full_file_listing: bool,
591}
592
593impl Display for GcRegions {
594    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
595        write!(
596            f,
597            "GcRegion(regions={:?}, file_refs_count={}, full_file_listing={})",
598            self.regions,
599            self.file_refs_manifest.file_refs.len(),
600            self.full_file_listing
601        )
602    }
603}
604
605/// Reply for GetFileRefs instruction.
606#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
607pub struct GetFileRefsReply {
608    /// The file references manifest.
609    pub file_refs_manifest: FileRefsManifest,
610    /// Whether the operation was successful.
611    pub success: bool,
612    /// Error message if any.
613    pub error: Option<InstructionError>,
614}
615
616impl Display for GetFileRefsReply {
617    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
618        write!(
619            f,
620            "GetFileRefsReply(success={}, file_refs_count={}, error={:?})",
621            self.success,
622            self.file_refs_manifest.file_refs.len(),
623            self.error
624        )
625    }
626}
627
628/// Reply for GC instruction.
629#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
630pub struct GcRegionsReply {
631    pub result: InstructionResult<GcReport>,
632}
633
634impl Display for GcRegionsReply {
635    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
636        write!(
637            f,
638            "GcReply(result={})",
639            match &self.result {
640                Ok(report) => format!(
641                    "GcReport(deleted_files_count={}, need_retry_regions_count={})",
642                    report.deleted_files.len(),
643                    report.need_retry_regions.len()
644                ),
645                Err(err) => format!("Err({})", err),
646            }
647        )
648    }
649}
650
651#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
652pub struct EnterStagingRegion {
653    pub region_id: RegionId,
654    #[serde(
655        alias = "partition_expr",
656        deserialize_with = "deserialize_enter_staging_partition_directive",
657        serialize_with = "serialize_enter_staging_partition_directive"
658    )]
659    pub partition_directive: StagingPartitionDirective,
660}
661
662impl Display for EnterStagingRegion {
663    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
664        write!(
665            f,
666            "EnterStagingRegion(region_id={}, partition_directive={})",
667            self.region_id, self.partition_directive
668        )
669    }
670}
671
672#[derive(Debug, Clone, PartialEq, Eq)]
673pub enum StagingPartitionDirective {
674    UpdatePartitionExpr(String),
675    RejectAllWrites,
676}
677
678impl StagingPartitionDirective {
679    /// Returns the partition expression carried by this directive, if any.
680    pub fn as_partition_expr(&self) -> Option<&str> {
681        match self {
682            Self::UpdatePartitionExpr(expr) => Some(expr),
683            Self::RejectAllWrites => None,
684        }
685    }
686}
687
688impl Display for StagingPartitionDirective {
689    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
690        match self {
691            Self::UpdatePartitionExpr(expr) => write!(f, "UpdatePartitionExpr({})", expr),
692            Self::RejectAllWrites => write!(f, "RejectAllWrites"),
693        }
694    }
695}
696
697fn serialize_enter_staging_partition_directive<S>(
698    rule: &StagingPartitionDirective,
699    serializer: S,
700) -> std::result::Result<S::Ok, S::Error>
701where
702    S: Serializer,
703{
704    match rule {
705        StagingPartitionDirective::UpdatePartitionExpr(expr) => serializer.serialize_str(expr),
706        StagingPartitionDirective::RejectAllWrites => {
707            #[derive(Serialize)]
708            struct RejectAllWritesSer<'a> {
709                r#type: &'a str,
710            }
711
712            RejectAllWritesSer {
713                r#type: "reject_all_writes",
714            }
715            .serialize(serializer)
716        }
717    }
718}
719
720fn deserialize_enter_staging_partition_directive<'de, D>(
721    deserializer: D,
722) -> std::result::Result<StagingPartitionDirective, D::Error>
723where
724    D: Deserializer<'de>,
725{
726    #[derive(Deserialize)]
727    #[serde(untagged)]
728    enum Compat {
729        Legacy(String),
730        TypeTagged { r#type: String },
731    }
732
733    match Compat::deserialize(deserializer)? {
734        Compat::Legacy(expr) => Ok(StagingPartitionDirective::UpdatePartitionExpr(expr)),
735        Compat::TypeTagged { r#type } if r#type == "reject_all_writes" => {
736            Ok(StagingPartitionDirective::RejectAllWrites)
737        }
738        Compat::TypeTagged { r#type } => Err(serde::de::Error::custom(format!(
739            "Unknown enter staging partition directive type: {}",
740            r#type
741        ))),
742    }
743}
744
745/// Instruction payload for syncing a region from a manifest or another region.
746#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
747pub struct SyncRegion {
748    /// Region id to sync.
749    pub region_id: RegionId,
750    /// Request to sync the region.
751    pub request: SyncRegionFromRequest,
752}
753
754impl Display for SyncRegion {
755    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
756        write!(
757            f,
758            "SyncRegion(region_id={}, request={:?})",
759            self.region_id, self.request
760        )
761    }
762}
763
764#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
765pub struct RemapManifest {
766    pub region_id: RegionId,
767    /// Regions to remap manifests from.
768    pub input_regions: Vec<RegionId>,
769    /// For each old region, which new regions should receive its files
770    pub region_mapping: HashMap<RegionId, Vec<RegionId>>,
771    /// New partition expressions for the new regions.
772    pub new_partition_exprs: HashMap<RegionId, String>,
773}
774
775impl Display for RemapManifest {
776    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
777        write!(
778            f,
779            "RemapManifest(region_id={}, input_regions={:?}, region_mapping={:?}, new_partition_exprs={:?})",
780            self.region_id, self.input_regions, self.region_mapping, self.new_partition_exprs
781        )
782    }
783}
784
785#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
786pub struct ApplyStagingManifest {
787    /// The region ID to apply the staging manifest to.
788    pub region_id: RegionId,
789    /// The partition expression of the staging region.
790    pub partition_expr: String,
791    /// The region that stores the staging manifests in its staging blob storage.
792    pub central_region_id: RegionId,
793    /// The relative path to the staging manifest within the central region's staging blob storage.
794    pub manifest_path: String,
795}
796
797impl Display for ApplyStagingManifest {
798    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
799        write!(
800            f,
801            "ApplyStagingManifest(region_id={}, partition_expr={}, central_region_id={}, manifest_path={})",
802            self.region_id, self.partition_expr, self.central_region_id, self.manifest_path
803        )
804    }
805}
806
807#[derive(Debug, Clone, Serialize, Deserialize, Display, PartialEq)]
808pub enum Instruction {
809    /// Opens regions.
810    #[serde(deserialize_with = "single_or_multiple_from", alias = "OpenRegion")]
811    OpenRegions(Vec<OpenRegion>),
812    /// Closes regions.
813    #[serde(deserialize_with = "single_or_multiple_from", alias = "CloseRegion")]
814    CloseRegions(Vec<RegionIdent>),
815    /// Upgrades regions.
816    #[serde(deserialize_with = "single_or_multiple_from", alias = "UpgradeRegion")]
817    UpgradeRegions(Vec<UpgradeRegion>),
818    #[serde(
819        deserialize_with = "single_or_multiple_from",
820        alias = "DowngradeRegion"
821    )]
822    /// Downgrades regions.
823    DowngradeRegions(Vec<DowngradeRegion>),
824    /// Invalidates batch cache.
825    InvalidateCaches(Vec<CacheIdent>),
826    /// Flushes regions.
827    FlushRegions(FlushRegions),
828    /// Gets file references for regions.
829    GetFileRefs(GetFileRefs),
830    /// Triggers garbage collection for a region.
831    GcRegions(GcRegions),
832    /// Temporary suspend serving reads or writes
833    Suspend,
834    /// Makes regions enter staging state.
835    EnterStagingRegions(Vec<EnterStagingRegion>),
836    /// Syncs regions.
837    SyncRegions(Vec<SyncRegion>),
838    /// Remaps manifests for a region.
839    RemapManifest(RemapManifest),
840
841    /// Applies staging manifests for a region.
842    ApplyStagingManifests(Vec<ApplyStagingManifest>),
843}
844
845impl Instruction {
846    /// Converts the instruction into a vector of [OpenRegion].
847    pub fn into_open_regions(self) -> Option<Vec<OpenRegion>> {
848        match self {
849            Self::OpenRegions(open_regions) => Some(open_regions),
850            _ => None,
851        }
852    }
853
854    /// Converts the instruction into a vector of [RegionIdent].
855    pub fn into_close_regions(self) -> Option<Vec<RegionIdent>> {
856        match self {
857            Self::CloseRegions(close_regions) => Some(close_regions),
858            _ => None,
859        }
860    }
861
862    /// Converts the instruction into a [FlushRegions].
863    pub fn into_flush_regions(self) -> Option<FlushRegions> {
864        match self {
865            Self::FlushRegions(flush_regions) => Some(flush_regions),
866            _ => None,
867        }
868    }
869
870    /// Converts the instruction into a [DowngradeRegion].
871    pub fn into_downgrade_regions(self) -> Option<Vec<DowngradeRegion>> {
872        match self {
873            Self::DowngradeRegions(downgrade_region) => Some(downgrade_region),
874            _ => None,
875        }
876    }
877
878    /// Converts the instruction into a [UpgradeRegion].
879    pub fn into_upgrade_regions(self) -> Option<Vec<UpgradeRegion>> {
880        match self {
881            Self::UpgradeRegions(upgrade_region) => Some(upgrade_region),
882            _ => None,
883        }
884    }
885
886    pub fn into_get_file_refs(self) -> Option<GetFileRefs> {
887        match self {
888            Self::GetFileRefs(get_file_refs) => Some(get_file_refs),
889            _ => None,
890        }
891    }
892
893    pub fn into_gc_regions(self) -> Option<GcRegions> {
894        match self {
895            Self::GcRegions(gc_regions) => Some(gc_regions),
896            _ => None,
897        }
898    }
899
900    pub fn into_enter_staging_regions(self) -> Option<Vec<EnterStagingRegion>> {
901        match self {
902            Self::EnterStagingRegions(enter_staging) => Some(enter_staging),
903            _ => None,
904        }
905    }
906
907    pub fn into_sync_regions(self) -> Option<Vec<SyncRegion>> {
908        match self {
909            Self::SyncRegions(sync_regions) => Some(sync_regions),
910            _ => None,
911        }
912    }
913}
914
915/// The reply of [UpgradeRegion].
916#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
917pub struct UpgradeRegionReply {
918    /// The [RegionId].
919    /// For compatibility, it is defaulted to [RegionId::new(0, 0)].
920    #[serde(default)]
921    pub region_id: RegionId,
922    /// Returns true if `last_entry_id` has been replayed to the latest.
923    pub ready: bool,
924    /// Indicates whether the region exists.
925    pub exists: bool,
926    /// Returns error if any.
927    pub error: Option<InstructionError>,
928}
929
930impl Display for UpgradeRegionReply {
931    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
932        write!(
933            f,
934            "(ready={}, exists={}, error={:?})",
935            self.ready, self.exists, self.error
936        )
937    }
938}
939
940#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
941pub struct DowngradeRegionsReply {
942    pub replies: Vec<DowngradeRegionReply>,
943}
944
945impl DowngradeRegionsReply {
946    pub fn new(replies: Vec<DowngradeRegionReply>) -> Self {
947        Self { replies }
948    }
949
950    pub fn single(reply: DowngradeRegionReply) -> Self {
951        Self::new(vec![reply])
952    }
953}
954
955#[derive(Deserialize)]
956#[serde(untagged)]
957enum DowngradeRegionsCompat {
958    Single(DowngradeRegionReply),
959    Multiple(DowngradeRegionsReply),
960}
961
962fn downgrade_regions_compat_from<'de, D>(deserializer: D) -> Result<DowngradeRegionsReply, D::Error>
963where
964    D: Deserializer<'de>,
965{
966    let helper = DowngradeRegionsCompat::deserialize(deserializer)?;
967    Ok(match helper {
968        DowngradeRegionsCompat::Single(x) => DowngradeRegionsReply::new(vec![x]),
969        DowngradeRegionsCompat::Multiple(reply) => reply,
970    })
971}
972
973#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
974pub struct UpgradeRegionsReply {
975    pub replies: Vec<UpgradeRegionReply>,
976}
977
978impl UpgradeRegionsReply {
979    pub fn new(replies: Vec<UpgradeRegionReply>) -> Self {
980        Self { replies }
981    }
982
983    pub fn single(reply: UpgradeRegionReply) -> Self {
984        Self::new(vec![reply])
985    }
986}
987
988#[derive(Deserialize)]
989#[serde(untagged)]
990enum UpgradeRegionsCompat {
991    Single(UpgradeRegionReply),
992    Multiple(UpgradeRegionsReply),
993}
994
995fn upgrade_regions_compat_from<'de, D>(deserializer: D) -> Result<UpgradeRegionsReply, D::Error>
996where
997    D: Deserializer<'de>,
998{
999    let helper = UpgradeRegionsCompat::deserialize(deserializer)?;
1000    Ok(match helper {
1001        UpgradeRegionsCompat::Single(x) => UpgradeRegionsReply::new(vec![x]),
1002        UpgradeRegionsCompat::Multiple(reply) => reply,
1003    })
1004}
1005
1006#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
1007pub struct EnterStagingRegionReply {
1008    pub region_id: RegionId,
1009    /// Returns true if the region has entered staging with the target directive.
1010    pub ready: bool,
1011    /// Indicates whether the region exists.
1012    pub exists: bool,
1013    /// Return error if any during the operation.
1014    pub error: Option<InstructionError>,
1015}
1016
1017#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
1018pub struct EnterStagingRegionsReply {
1019    pub replies: Vec<EnterStagingRegionReply>,
1020}
1021
1022impl EnterStagingRegionsReply {
1023    pub fn new(replies: Vec<EnterStagingRegionReply>) -> Self {
1024        Self { replies }
1025    }
1026}
1027
1028/// Reply for a single region sync request.
1029#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
1030pub struct SyncRegionReply {
1031    /// Region id of the synced region.
1032    pub region_id: RegionId,
1033    /// Returns true if the region is successfully synced and ready.
1034    pub ready: bool,
1035    /// Indicates whether the region exists.
1036    pub exists: bool,
1037    /// Return error message if any during the operation.
1038    pub error: Option<InstructionError>,
1039}
1040
1041/// Reply for a batch of region sync requests.
1042#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
1043pub struct SyncRegionsReply {
1044    pub replies: Vec<SyncRegionReply>,
1045}
1046
1047impl SyncRegionsReply {
1048    pub fn new(replies: Vec<SyncRegionReply>) -> Self {
1049        Self { replies }
1050    }
1051}
1052
1053#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
1054pub struct RemapManifestReply {
1055    /// Returns false if the region does not exist.
1056    pub exists: bool,
1057    /// A map from region IDs to their corresponding remapped manifest paths.
1058    pub manifest_paths: HashMap<RegionId, String>,
1059    /// Return error if any during the operation.
1060    pub error: Option<InstructionError>,
1061}
1062
1063impl Display for RemapManifestReply {
1064    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1065        write!(
1066            f,
1067            "RemapManifestReply(manifest_paths={:?}, error={:?})",
1068            self.manifest_paths, self.error
1069        )
1070    }
1071}
1072
1073#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
1074pub struct ApplyStagingManifestsReply {
1075    pub replies: Vec<ApplyStagingManifestReply>,
1076}
1077
1078impl ApplyStagingManifestsReply {
1079    pub fn new(replies: Vec<ApplyStagingManifestReply>) -> Self {
1080        Self { replies }
1081    }
1082}
1083
1084#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
1085pub struct ApplyStagingManifestReply {
1086    pub region_id: RegionId,
1087    /// Returns true if the region is ready to serve reads and writes.
1088    pub ready: bool,
1089    /// Indicates whether the region exists.
1090    pub exists: bool,
1091    /// Return error if any during the operation.
1092    pub error: Option<InstructionError>,
1093}
1094
1095#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
1096#[serde(tag = "type", rename_all = "snake_case")]
1097pub enum InstructionReply {
1098    #[serde(alias = "open_region")]
1099    OpenRegions(SimpleReply),
1100    #[serde(alias = "close_region")]
1101    CloseRegions(SimpleReply),
1102    #[serde(
1103        deserialize_with = "upgrade_regions_compat_from",
1104        alias = "upgrade_region"
1105    )]
1106    UpgradeRegions(UpgradeRegionsReply),
1107    #[serde(
1108        alias = "downgrade_region",
1109        deserialize_with = "downgrade_regions_compat_from"
1110    )]
1111    DowngradeRegions(DowngradeRegionsReply),
1112    FlushRegions(FlushRegionReply),
1113    GetFileRefs(GetFileRefsReply),
1114    GcRegions(GcRegionsReply),
1115    EnterStagingRegions(EnterStagingRegionsReply),
1116    SyncRegions(SyncRegionsReply),
1117    RemapManifest(RemapManifestReply),
1118    ApplyStagingManifests(ApplyStagingManifestsReply),
1119}
1120
1121impl Display for InstructionReply {
1122    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1123        match self {
1124            Self::OpenRegions(reply) => write!(f, "InstructionReply::OpenRegions({})", reply),
1125            Self::CloseRegions(reply) => write!(f, "InstructionReply::CloseRegions({})", reply),
1126            Self::UpgradeRegions(reply) => {
1127                write!(f, "InstructionReply::UpgradeRegions({:?})", reply.replies)
1128            }
1129            Self::DowngradeRegions(reply) => {
1130                write!(f, "InstructionReply::DowngradeRegions({:?})", reply.replies)
1131            }
1132            Self::FlushRegions(reply) => write!(f, "InstructionReply::FlushRegions({})", reply),
1133            Self::GetFileRefs(reply) => write!(f, "InstructionReply::GetFileRefs({})", reply),
1134            Self::GcRegions(reply) => write!(f, "InstructionReply::GcRegion({})", reply),
1135            Self::EnterStagingRegions(reply) => {
1136                write!(
1137                    f,
1138                    "InstructionReply::EnterStagingRegions({:?})",
1139                    reply.replies
1140                )
1141            }
1142            Self::SyncRegions(reply) => {
1143                write!(f, "InstructionReply::SyncRegions({:?})", reply.replies)
1144            }
1145            Self::RemapManifest(reply) => write!(f, "InstructionReply::RemapManifest({})", reply),
1146            Self::ApplyStagingManifests(reply) => write!(
1147                f,
1148                "InstructionReply::ApplyStagingManifests({:?})",
1149                reply.replies
1150            ),
1151        }
1152    }
1153}
1154
1155#[cfg(any(test, feature = "testing"))]
1156impl InstructionReply {
1157    pub fn expect_close_regions_reply(self) -> SimpleReply {
1158        match self {
1159            Self::CloseRegions(reply) => reply,
1160            _ => panic!("Expected CloseRegions reply"),
1161        }
1162    }
1163
1164    pub fn expect_open_regions_reply(self) -> SimpleReply {
1165        match self {
1166            Self::OpenRegions(reply) => reply,
1167            _ => panic!("Expected OpenRegions reply"),
1168        }
1169    }
1170
1171    pub fn expect_upgrade_regions_reply(self) -> Vec<UpgradeRegionReply> {
1172        match self {
1173            Self::UpgradeRegions(reply) => reply.replies,
1174            _ => panic!("Expected UpgradeRegion reply"),
1175        }
1176    }
1177
1178    pub fn expect_downgrade_regions_reply(self) -> Vec<DowngradeRegionReply> {
1179        match self {
1180            Self::DowngradeRegions(reply) => reply.replies,
1181            _ => panic!("Expected DowngradeRegion reply"),
1182        }
1183    }
1184
1185    pub fn expect_flush_regions_reply(self) -> FlushRegionReply {
1186        match self {
1187            Self::FlushRegions(reply) => reply,
1188            _ => panic!("Expected FlushRegions reply"),
1189        }
1190    }
1191
1192    pub fn expect_enter_staging_regions_reply(self) -> Vec<EnterStagingRegionReply> {
1193        match self {
1194            Self::EnterStagingRegions(reply) => reply.replies,
1195            _ => panic!("Expected EnterStagingRegion reply"),
1196        }
1197    }
1198
1199    pub fn expect_sync_regions_reply(self) -> Vec<SyncRegionReply> {
1200        match self {
1201            Self::SyncRegions(reply) => reply.replies,
1202            _ => panic!("Expected SyncRegion reply"),
1203        }
1204    }
1205
1206    pub fn expect_remap_manifest_reply(self) -> RemapManifestReply {
1207        match self {
1208            Self::RemapManifest(reply) => reply,
1209            _ => panic!("Expected RemapManifest reply"),
1210        }
1211    }
1212
1213    pub fn expect_apply_staging_manifests_reply(self) -> Vec<ApplyStagingManifestReply> {
1214        match self {
1215            Self::ApplyStagingManifests(reply) => reply.replies,
1216            _ => panic!("Expected ApplyStagingManifest reply"),
1217        }
1218    }
1219}
1220
1221#[cfg(test)]
1222mod tests {
1223    use std::collections::HashSet;
1224
1225    use common_error::mock::MockError;
1226    use common_wal::options::WalOptions;
1227    use store_api::storage::{FileId, FileRef};
1228
1229    use super::*;
1230
1231    #[test]
1232    fn test_instruction_error_serde() {
1233        let error = InstructionError::new(
1234            StatusCode::RegionNotFound,
1235            "region not found",
1236            RetryHint::Retryable,
1237        );
1238
1239        let serialized = serde_json::to_string(&error).unwrap();
1240        assert_eq!(
1241            r#"{"code":4005,"message":"region not found","retry_hint":"retryable"}"#,
1242            serialized
1243        );
1244
1245        let deserialized: InstructionError = serde_json::from_str(&serialized).unwrap();
1246        assert_eq!(error, deserialized);
1247
1248        assert!(
1249            serde_json::from_str::<InstructionError>(
1250                r#"{"code":999999,"message":"unknown","retry_hint":"retryable"}"#
1251            )
1252            .is_err()
1253        );
1254        assert!(
1255            serde_json::from_str::<InstructionError>(
1256                r#"{"code":4005,"message":"unknown","retry_hint":"unknown"}"#
1257            )
1258            .is_err()
1259        );
1260    }
1261
1262    #[test]
1263    fn test_instruction_error_from_error() {
1264        let error = MockError::new(StatusCode::RegionNotFound);
1265
1266        let instruction_error = InstructionError::from_error(&error);
1267
1268        assert_eq!(StatusCode::RegionNotFound, instruction_error.code);
1269        assert_eq!("RegionNotFound", instruction_error.message);
1270        assert_eq!(RetryHint::NonRetryable, instruction_error.retry_hint);
1271    }
1272
1273    #[test]
1274    fn test_instruction_result_serde() {
1275        let success: InstructionResult<bool> = Ok(true);
1276        let serialized = serde_json::to_string(&success).unwrap();
1277        assert_eq!(r#"{"Ok":true}"#, serialized);
1278        let deserialized: InstructionResult<bool> = serde_json::from_str(&serialized).unwrap();
1279        assert_eq!(success, deserialized);
1280
1281        let failure: InstructionResult<bool> = Err(InstructionError::new(
1282            StatusCode::RegionBusy,
1283            "region busy",
1284            RetryHint::Retryable,
1285        ));
1286        let serialized = serde_json::to_string(&failure).unwrap();
1287        assert_eq!(
1288            r#"{"Err":{"code":4009,"message":"region busy","retry_hint":"retryable"}}"#,
1289            serialized
1290        );
1291        let deserialized: InstructionResult<bool> = serde_json::from_str(&serialized).unwrap();
1292        assert_eq!(failure, deserialized);
1293    }
1294
1295    #[test]
1296    fn test_serialize_instruction() {
1297        let open_region = Instruction::OpenRegions(vec![OpenRegion::new(
1298            RegionIdent {
1299                datanode_id: 2,
1300                table_id: 1024,
1301                region_number: 1,
1302                engine: "mito2".to_string(),
1303            },
1304            "test/foo",
1305            HashMap::new(),
1306            HashMap::new(),
1307            false,
1308            None,
1309            RegionRequirements::empty(),
1310        )]);
1311
1312        let serialized = serde_json::to_string(&open_region).unwrap();
1313        assert_eq!(
1314            r#"{"OpenRegions":[{"region_ident":{"datanode_id":2,"table_id":1024,"region_number":1,"engine":"mito2"},"region_storage_path":"test/foo","region_options":{},"region_wal_options":{},"skip_wal_replay":false,"requirements":{"object_storage":false}}]}"#,
1315            serialized
1316        );
1317
1318        let close_region = Instruction::CloseRegions(vec![RegionIdent {
1319            datanode_id: 2,
1320            table_id: 1024,
1321            region_number: 1,
1322            engine: "mito2".to_string(),
1323        }]);
1324
1325        let serialized = serde_json::to_string(&close_region).unwrap();
1326        assert_eq!(
1327            r#"{"CloseRegions":[{"datanode_id":2,"table_id":1024,"region_number":1,"engine":"mito2"}]}"#,
1328            serialized
1329        );
1330
1331        let upgrade_region = Instruction::UpgradeRegions(vec![UpgradeRegion {
1332            region_id: RegionId::new(1024, 1),
1333            last_entry_id: None,
1334            metadata_last_entry_id: None,
1335            replay_timeout: Duration::from_millis(1000),
1336            location_id: None,
1337            replay_entry_id: None,
1338            metadata_replay_entry_id: None,
1339        }]);
1340
1341        let serialized = serde_json::to_string(&upgrade_region).unwrap();
1342        assert_eq!(
1343            r#"{"UpgradeRegions":[{"region_id":4398046511105,"last_entry_id":null,"metadata_last_entry_id":null,"replay_timeout":"1s","location_id":null}]}"#,
1344            serialized
1345        );
1346    }
1347
1348    #[test]
1349    fn test_serialize_instruction_reply() {
1350        let downgrade_region_reply = InstructionReply::DowngradeRegions(
1351            DowngradeRegionsReply::single(DowngradeRegionReply {
1352                region_id: RegionId::new(1024, 1),
1353                last_entry_id: None,
1354                metadata_last_entry_id: None,
1355                exists: true,
1356                error: None,
1357            }),
1358        );
1359
1360        let serialized = serde_json::to_string(&downgrade_region_reply).unwrap();
1361        assert_eq!(
1362            r#"{"type":"downgrade_regions","replies":[{"region_id":4398046511105,"last_entry_id":null,"metadata_last_entry_id":null,"exists":true,"error":null}]}"#,
1363            serialized
1364        );
1365
1366        let upgrade_region_reply =
1367            InstructionReply::UpgradeRegions(UpgradeRegionsReply::single(UpgradeRegionReply {
1368                region_id: RegionId::new(1024, 1),
1369                ready: true,
1370                exists: true,
1371                error: None,
1372            }));
1373        let serialized = serde_json::to_string(&upgrade_region_reply).unwrap();
1374        assert_eq!(
1375            r#"{"type":"upgrade_regions","replies":[{"region_id":4398046511105,"ready":true,"exists":true,"error":null}]}"#,
1376            serialized
1377        );
1378    }
1379
1380    #[test]
1381    fn test_deserialize_instruction() {
1382        // legacy open region instruction
1383        let open_region_instruction = r#"{"OpenRegion":{"region_ident":{"datanode_id":2,"table_id":1024,"region_number":1,"engine":"mito2"},"region_storage_path":"test/foo","region_options":{},"region_wal_options":{},"skip_wal_replay":false}}"#;
1384        let open_region_instruction: Instruction =
1385            serde_json::from_str(open_region_instruction).unwrap();
1386        let open_region = Instruction::OpenRegions(vec![OpenRegion::new(
1387            RegionIdent {
1388                datanode_id: 2,
1389                table_id: 1024,
1390                region_number: 1,
1391                engine: "mito2".to_string(),
1392            },
1393            "test/foo",
1394            HashMap::new(),
1395            HashMap::new(),
1396            false,
1397            None,
1398            RegionRequirements::empty(),
1399        )]);
1400        assert_eq!(open_region_instruction, open_region);
1401
1402        // legacy close region instruction
1403        let close_region_instruction = r#"{"CloseRegion":{"datanode_id":2,"table_id":1024,"region_number":1,"engine":"mito2"}}"#;
1404        let close_region_instruction: Instruction =
1405            serde_json::from_str(close_region_instruction).unwrap();
1406        let close_region = Instruction::CloseRegions(vec![RegionIdent {
1407            datanode_id: 2,
1408            table_id: 1024,
1409            region_number: 1,
1410            engine: "mito2".to_string(),
1411        }]);
1412        assert_eq!(close_region_instruction, close_region);
1413
1414        // legacy downgrade region instruction
1415        let downgrade_region_instruction = r#"{"DowngradeRegions":{"region_id":4398046511105,"flush_timeout":{"secs":1,"nanos":0}}}"#;
1416        let downgrade_region_instruction: Instruction =
1417            serde_json::from_str(downgrade_region_instruction).unwrap();
1418        let downgrade_region = Instruction::DowngradeRegions(vec![DowngradeRegion {
1419            region_id: RegionId::new(1024, 1),
1420            flush_timeout: Some(Duration::from_millis(1000)),
1421        }]);
1422        assert_eq!(downgrade_region_instruction, downgrade_region);
1423
1424        // legacy upgrade region instruction
1425        let upgrade_region_instruction = r#"{"UpgradeRegion":{"region_id":4398046511105,"last_entry_id":null,"metadata_last_entry_id":null,"replay_timeout":"1s","location_id":null,"replay_entry_id":null,"metadata_replay_entry_id":null}}"#;
1426        let upgrade_region_instruction: Instruction =
1427            serde_json::from_str(upgrade_region_instruction).unwrap();
1428        let upgrade_region = Instruction::UpgradeRegions(vec![UpgradeRegion {
1429            region_id: RegionId::new(1024, 1),
1430            last_entry_id: None,
1431            metadata_last_entry_id: None,
1432            replay_timeout: Duration::from_millis(1000),
1433            location_id: None,
1434            replay_entry_id: None,
1435            metadata_replay_entry_id: None,
1436        }]);
1437        assert_eq!(upgrade_region_instruction, upgrade_region);
1438    }
1439
1440    #[test]
1441    fn test_deserialize_instruction_reply() {
1442        // legacy close region reply
1443        let close_region_instruction_reply =
1444            r#"{"result":true,"error":null,"type":"close_region"}"#;
1445        let close_region_instruction_reply: InstructionReply =
1446            serde_json::from_str(close_region_instruction_reply).unwrap();
1447        let close_region_reply = InstructionReply::CloseRegions(SimpleReply {
1448            result: true,
1449            error: None,
1450        });
1451        assert_eq!(close_region_instruction_reply, close_region_reply);
1452
1453        // legacy open region reply
1454        let open_region_instruction_reply = r#"{"result":true,"error":null,"type":"open_region"}"#;
1455        let open_region_instruction_reply: InstructionReply =
1456            serde_json::from_str(open_region_instruction_reply).unwrap();
1457        let open_region_reply = InstructionReply::OpenRegions(SimpleReply {
1458            result: true,
1459            error: None,
1460        });
1461        assert_eq!(open_region_instruction_reply, open_region_reply);
1462
1463        // legacy downgrade region reply
1464        let downgrade_region_instruction_reply = r#"{"region_id":4398046511105,"last_entry_id":null,"metadata_last_entry_id":null,"exists":true,"error":null,"type":"downgrade_region"}"#;
1465        let downgrade_region_instruction_reply: InstructionReply =
1466            serde_json::from_str(downgrade_region_instruction_reply).unwrap();
1467        let downgrade_region_reply = InstructionReply::DowngradeRegions(
1468            DowngradeRegionsReply::single(DowngradeRegionReply {
1469                region_id: RegionId::new(1024, 1),
1470                last_entry_id: None,
1471                metadata_last_entry_id: None,
1472                exists: true,
1473                error: None,
1474            }),
1475        );
1476        assert_eq!(downgrade_region_instruction_reply, downgrade_region_reply);
1477
1478        // legacy upgrade region reply
1479        let upgrade_region_instruction_reply = r#"{"region_id":4398046511105,"ready":true,"exists":true,"error":null,"type":"upgrade_region"}"#;
1480        let upgrade_region_instruction_reply: InstructionReply =
1481            serde_json::from_str(upgrade_region_instruction_reply).unwrap();
1482        let upgrade_region_reply =
1483            InstructionReply::UpgradeRegions(UpgradeRegionsReply::single(UpgradeRegionReply {
1484                region_id: RegionId::new(1024, 1),
1485                ready: true,
1486                exists: true,
1487                error: None,
1488            }));
1489        assert_eq!(upgrade_region_instruction_reply, upgrade_region_reply);
1490    }
1491
1492    #[test]
1493    fn test_enter_staging_partition_rule_compatibility() {
1494        let legacy = r#"{"region_id":4398046511105,"partition_expr":"{\"Expr\":{\"lhs\":{\"Column\":\"x\"},\"op\":\"GtEq\",\"rhs\":{\"Value\":{\"Int32\":0}}}}"}"#;
1495        let enter: EnterStagingRegion = serde_json::from_str(legacy).unwrap();
1496        assert_eq!(enter.region_id, RegionId::new(1024, 1));
1497        assert_eq!(
1498            enter.partition_directive,
1499            StagingPartitionDirective::UpdatePartitionExpr(
1500                "{\"Expr\":{\"lhs\":{\"Column\":\"x\"},\"op\":\"GtEq\",\"rhs\":{\"Value\":{\"Int32\":0}}}}"
1501                    .to_string()
1502            )
1503        );
1504
1505        let serialized = serde_json::to_string(&enter).unwrap();
1506        assert!(serialized.contains("\"partition_directive\":\""));
1507        assert!(!serialized.contains("partition_expr"));
1508
1509        let reject = r#"{"region_id":4398046511105,"partition_expr":{"type":"reject_all_writes"}}"#;
1510        let enter: EnterStagingRegion = serde_json::from_str(reject).unwrap();
1511        assert_eq!(
1512            enter.partition_directive,
1513            StagingPartitionDirective::RejectAllWrites
1514        );
1515    }
1516
1517    #[derive(Debug, Clone, Serialize, Deserialize)]
1518    struct LegacyOpenRegion {
1519        region_ident: RegionIdent,
1520        region_storage_path: String,
1521        region_options: HashMap<String, String>,
1522    }
1523
1524    #[test]
1525    fn test_compatible_serialize_open_region() {
1526        let region_ident = RegionIdent {
1527            datanode_id: 2,
1528            table_id: 1024,
1529            region_number: 1,
1530            engine: "mito2".to_string(),
1531        };
1532        let region_storage_path = "test/foo".to_string();
1533        let region_options = HashMap::from([
1534            ("a".to_string(), "aa".to_string()),
1535            ("b".to_string(), "bb".to_string()),
1536        ]);
1537
1538        // Serialize a legacy OpenRegion.
1539        let legacy_open_region = LegacyOpenRegion {
1540            region_ident: region_ident.clone(),
1541            region_storage_path: region_storage_path.clone(),
1542            region_options: region_options.clone(),
1543        };
1544        let serialized = serde_json::to_string(&legacy_open_region).unwrap();
1545
1546        // Deserialize to OpenRegion.
1547        let deserialized = serde_json::from_str(&serialized).unwrap();
1548        let expected = OpenRegion {
1549            region_ident,
1550            region_storage_path,
1551            region_options,
1552            region_wal_options: HashMap::new(),
1553            skip_wal_replay: false,
1554            reason: None,
1555            requirements: RegionRequirements::empty(),
1556        };
1557        assert_eq!(expected, deserialized);
1558    }
1559
1560    #[test]
1561    fn test_deserialize_open_region_with_legacy_region_wal_options() {
1562        let open_region = r#"{"region_ident":{"datanode_id":2,"table_id":1024,"region_number":1,"engine":"mito2"},"region_storage_path":"test/foo","region_options":{},"region_wal_options":{"1":"{\"wal.provider\":\"raft_engine\"}"},"skip_wal_replay":false}"#;
1563
1564        let open_region: OpenRegion = serde_json::from_str(open_region).unwrap();
1565
1566        assert_eq!(
1567            open_region.region_wal_options,
1568            HashMap::from([(1, WalOptions::RaftEngine)])
1569        );
1570    }
1571
1572    #[test]
1573    fn test_serialize_open_region_with_reason_and_requirements() {
1574        let open_region = OpenRegion::new(
1575            RegionIdent {
1576                datanode_id: 2,
1577                table_id: 1024,
1578                region_number: 1,
1579                engine: "mito2".to_string(),
1580            },
1581            "test/foo",
1582            HashMap::new(),
1583            HashMap::new(),
1584            false,
1585            Some(OpenRegionReason::RegionMigration),
1586            RegionRequirements::object_storage(),
1587        );
1588
1589        let serialized = serde_json::to_string(&open_region).unwrap();
1590        assert!(serialized.contains(r#""reason":"RegionMigration""#));
1591        assert!(serialized.contains(r#""object_storage":true"#));
1592
1593        let deserialized: OpenRegion = serde_json::from_str(&serialized).unwrap();
1594        assert_eq!(Some(OpenRegionReason::RegionMigration), deserialized.reason);
1595        assert_eq!(
1596            RegionRequirements::object_storage(),
1597            deserialized.requirements
1598        );
1599    }
1600
1601    #[test]
1602    fn test_flush_regions_creation() {
1603        let region_id = RegionId::new(1024, 1);
1604
1605        // Single region sync flush
1606        let single_sync = FlushRegions::sync_single(region_id);
1607        assert_eq!(single_sync.region_ids, vec![region_id]);
1608        assert_eq!(single_sync.strategy, FlushStrategy::Sync);
1609        assert!(!single_sync.is_hint());
1610        assert!(single_sync.is_sync());
1611        assert_eq!(single_sync.error_strategy, FlushErrorStrategy::FailFast);
1612        assert_eq!(single_sync.reason, None);
1613        assert!(single_sync.is_single_region());
1614        assert_eq!(single_sync.single_region_id(), Some(region_id));
1615
1616        // Batch async flush (hint)
1617        let region_ids = vec![RegionId::new(1024, 1), RegionId::new(1024, 2)];
1618        let batch_async = FlushRegions::async_batch(region_ids.clone());
1619        assert_eq!(batch_async.region_ids, region_ids);
1620        assert_eq!(batch_async.strategy, FlushStrategy::Async);
1621        assert!(batch_async.is_hint());
1622        assert!(!batch_async.is_sync());
1623        assert_eq!(batch_async.error_strategy, FlushErrorStrategy::TryAll);
1624        assert_eq!(batch_async.reason, None);
1625        assert!(!batch_async.is_single_region());
1626        assert_eq!(batch_async.single_region_id(), None);
1627
1628        // Batch sync flush
1629        let batch_sync = FlushRegions::sync_batch(region_ids.clone(), FlushErrorStrategy::FailFast);
1630        assert_eq!(batch_sync.region_ids, region_ids);
1631        assert_eq!(batch_sync.strategy, FlushStrategy::Sync);
1632        assert!(!batch_sync.is_hint());
1633        assert!(batch_sync.is_sync());
1634        assert_eq!(batch_sync.error_strategy, FlushErrorStrategy::FailFast);
1635        assert_eq!(batch_sync.reason, None);
1636
1637        let with_reason = batch_sync.with_reason(RegionFlushReason::RemoteWalPrune);
1638        assert_eq!(with_reason.reason, Some(RegionFlushReason::RemoteWalPrune));
1639    }
1640
1641    #[test]
1642    fn test_flush_regions_conversion() {
1643        let region_id = RegionId::new(1024, 1);
1644
1645        let from_region_id: FlushRegions = region_id.into();
1646        assert_eq!(from_region_id.region_ids, vec![region_id]);
1647        assert_eq!(from_region_id.strategy, FlushStrategy::Sync);
1648        assert!(!from_region_id.is_hint());
1649        assert!(from_region_id.is_sync());
1650
1651        // Test default construction
1652        let flush_regions = FlushRegions {
1653            region_ids: vec![region_id],
1654            strategy: FlushStrategy::Async,
1655            error_strategy: FlushErrorStrategy::TryAll,
1656            reason: None,
1657        };
1658        assert_eq!(flush_regions.region_ids, vec![region_id]);
1659        assert_eq!(flush_regions.strategy, FlushStrategy::Async);
1660        assert!(flush_regions.is_hint());
1661        assert!(!flush_regions.is_sync());
1662    }
1663
1664    #[test]
1665    fn test_flush_region_reply() {
1666        let region_id = RegionId::new(1024, 1);
1667
1668        // Successful single region reply
1669        let success_reply = FlushRegionReply::success_single(region_id);
1670        assert!(success_reply.overall_success);
1671        assert_eq!(success_reply.results.len(), 1);
1672        assert_eq!(success_reply.results[0].0, region_id);
1673        assert!(success_reply.results[0].1.is_ok());
1674
1675        // Failed single region reply
1676        let error_reply = FlushRegionReply::error_single(
1677            region_id,
1678            InstructionError::legacy_internal_retryable("test error"),
1679        );
1680        assert!(!error_reply.overall_success);
1681        assert_eq!(error_reply.results.len(), 1);
1682        assert_eq!(error_reply.results[0].0, region_id);
1683        assert!(error_reply.results[0].1.is_err());
1684
1685        // Batch reply
1686        let region_id2 = RegionId::new(1024, 2);
1687        let results = vec![
1688            (region_id, Ok(())),
1689            (
1690                region_id2,
1691                Err(InstructionError::legacy_internal_retryable("flush failed")),
1692            ),
1693        ];
1694        let batch_reply = FlushRegionReply::from_results(results);
1695        assert!(!batch_reply.overall_success);
1696        assert_eq!(batch_reply.results.len(), 2);
1697
1698        // Conversion to SimpleReply
1699        let simple_reply = batch_reply.to_simple_reply();
1700        assert!(!simple_reply.result);
1701        assert!(simple_reply.error.is_some());
1702        assert!(simple_reply.error.unwrap().message.contains("flush failed"));
1703    }
1704
1705    #[test]
1706    fn test_serialize_flush_regions_instruction() {
1707        let region_id = RegionId::new(1024, 1);
1708        let flush_regions = FlushRegions::sync_single(region_id);
1709        let instruction = Instruction::FlushRegions(flush_regions.clone());
1710
1711        let serialized = serde_json::to_string(&instruction).unwrap();
1712        assert!(!serialized.contains("reason"));
1713        let deserialized: Instruction = serde_json::from_str(&serialized).unwrap();
1714
1715        match deserialized {
1716            Instruction::FlushRegions(fr) => {
1717                assert_eq!(fr.region_ids, vec![region_id]);
1718                assert_eq!(fr.strategy, FlushStrategy::Sync);
1719                assert_eq!(fr.error_strategy, FlushErrorStrategy::FailFast);
1720                assert_eq!(fr.reason, None);
1721            }
1722            _ => panic!("Expected FlushRegions instruction"),
1723        }
1724
1725        let legacy = r#"{"FlushRegions":{"region_ids":[4398046511105],"strategy":"Sync","error_strategy":"FailFast"}}"#;
1726        let deserialized: Instruction = serde_json::from_str(legacy).unwrap();
1727        match deserialized {
1728            Instruction::FlushRegions(fr) => {
1729                assert_eq!(fr.region_ids, vec![region_id]);
1730                assert_eq!(fr.strategy, FlushStrategy::Sync);
1731                assert_eq!(fr.error_strategy, FlushErrorStrategy::FailFast);
1732                assert_eq!(fr.reason, None);
1733            }
1734            _ => panic!("Expected FlushRegions instruction"),
1735        }
1736
1737        let flush_regions = FlushRegions::async_batch(vec![region_id])
1738            .with_reason(RegionFlushReason::RemoteWalPrune);
1739        let instruction = Instruction::FlushRegions(flush_regions);
1740        let serialized = serde_json::to_string(&instruction).unwrap();
1741        assert!(serialized.contains(r#""reason":"RemoteWalPrune""#));
1742        let deserialized: Instruction = serde_json::from_str(&serialized).unwrap();
1743        match deserialized {
1744            Instruction::FlushRegions(fr) => {
1745                assert_eq!(fr.reason, Some(RegionFlushReason::RemoteWalPrune));
1746            }
1747            _ => panic!("Expected FlushRegions instruction"),
1748        }
1749    }
1750
1751    #[test]
1752    fn test_serialize_flush_regions_batch_instruction() {
1753        let region_ids = vec![RegionId::new(1024, 1), RegionId::new(1024, 2)];
1754        let flush_regions =
1755            FlushRegions::sync_batch(region_ids.clone(), FlushErrorStrategy::TryAll);
1756        let instruction = Instruction::FlushRegions(flush_regions);
1757
1758        let serialized = serde_json::to_string(&instruction).unwrap();
1759        let deserialized: Instruction = serde_json::from_str(&serialized).unwrap();
1760
1761        match deserialized {
1762            Instruction::FlushRegions(fr) => {
1763                assert_eq!(fr.region_ids, region_ids);
1764                assert_eq!(fr.strategy, FlushStrategy::Sync);
1765                assert!(!fr.is_hint());
1766                assert!(fr.is_sync());
1767                assert_eq!(fr.error_strategy, FlushErrorStrategy::TryAll);
1768                assert_eq!(fr.reason, None);
1769            }
1770            _ => panic!("Expected FlushRegions instruction"),
1771        }
1772    }
1773
1774    #[test]
1775    fn test_serialize_get_file_refs_instruction_reply() {
1776        let mut manifest = FileRefsManifest::default();
1777        let r0 = RegionId::new(1024, 1);
1778        let r1 = RegionId::new(1024, 2);
1779        manifest.file_refs.insert(
1780            r0,
1781            HashSet::from([FileRef::new(r0, FileId::random(), None)]),
1782        );
1783        manifest.file_refs.insert(
1784            r1,
1785            HashSet::from([FileRef::new(r1, FileId::random(), None)]),
1786        );
1787        manifest.manifest_version.insert(r0, 10);
1788        manifest.manifest_version.insert(r1, 20);
1789
1790        let instruction_reply = InstructionReply::GetFileRefs(GetFileRefsReply {
1791            file_refs_manifest: manifest,
1792            success: true,
1793            error: None,
1794        });
1795
1796        let serialized = serde_json::to_string(&instruction_reply).unwrap();
1797        let deserialized = serde_json::from_str(&serialized).unwrap();
1798
1799        assert_eq!(instruction_reply, deserialized);
1800    }
1801}