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