Skip to main content

rustfs_filemeta/
replication.rs

1// Copyright 2024 RustFS 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 bytes::Bytes;
16use core::fmt;
17use regex::Regex;
18use rustfs_utils::http::internal_key_rustfs;
19use serde::{Deserialize, Serialize};
20use std::any::Any;
21use std::collections::HashMap;
22use std::sync::LazyLock;
23use std::time::Duration;
24use time::OffsetDateTime;
25use uuid::Uuid;
26
27pub const REPLICATION_RESET: &str = "replication-reset";
28pub const REPLICATION_STATUS: &str = "replication-status";
29
30// ReplicateQueued - replication being queued trail
31pub const REPLICATE_QUEUED: &str = "replicate:queue";
32
33// ReplicateExisting - audit trail for existing objects replication
34pub const REPLICATE_EXISTING: &str = "replicate:existing";
35// ReplicateExistingDelete - audit trail for delete replication triggered for existing delete markers
36pub const REPLICATE_EXISTING_DELETE: &str = "replicate:existing:delete";
37
38// ReplicateMRF - audit trail for replication from Most Recent Failures (MRF) queue
39pub const REPLICATE_MRF: &str = "replicate:mrf";
40// ReplicateIncoming - audit trail of inline replication
41pub const REPLICATE_INCOMING: &str = "replicate:incoming";
42// ReplicateIncomingDelete - audit trail of inline replication of deletes.
43pub const REPLICATE_INCOMING_DELETE: &str = "replicate:incoming:delete";
44
45// ReplicateHeal - audit trail for healing of failed/pending replications
46pub const REPLICATE_HEAL: &str = "replicate:heal";
47// ReplicateHealDelete - audit trail of healing of failed/pending delete replications.
48pub const REPLICATE_HEAL_DELETE: &str = "replicate:heal:delete";
49
50/// StatusType of Replication for x-amz-replication-status header
51///
52/// NOTE: `rustfs-replication` owns a sibling copy of this enum (plus
53/// `VersionPurgeStatusType` and `ReplicationState`) bound to the MRF/resync
54/// persistence format, while this copy is bound to the xl.meta disk format.
55/// When adding or renaming a variant here, reconcile the sibling and the
56/// conversion layer — the reconciliation tests in
57/// `crates/ecstore/src/bucket/replication/replication_filemeta_boundary.rs`
58/// fail to compile until both sides agree.
59#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default, Hash)]
60pub enum ReplicationStatusType {
61    /// Pending - replication is pending.
62    Pending,
63    /// Completed - replication completed ok.
64    Completed,
65    /// CompletedLegacy was called "COMPLETE" incorrectly.
66    CompletedLegacy,
67    /// Failed - replication failed.
68    Failed,
69    /// Replica - this is a replica.
70    Replica,
71    #[default]
72    Empty,
73}
74
75impl ReplicationStatusType {
76    /// Returns string representation of status
77    pub fn as_str(&self) -> &'static str {
78        match self {
79            ReplicationStatusType::Pending => "PENDING",
80            ReplicationStatusType::Completed => "COMPLETED",
81            ReplicationStatusType::CompletedLegacy => "COMPLETE",
82            ReplicationStatusType::Failed => "FAILED",
83            ReplicationStatusType::Replica => "REPLICA",
84            ReplicationStatusType::Empty => "",
85        }
86    }
87    pub fn is_empty(&self) -> bool {
88        matches!(self, ReplicationStatusType::Empty)
89    }
90}
91
92impl fmt::Display for ReplicationStatusType {
93    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94        write!(f, "{}", self.as_str())
95    }
96}
97
98impl From<&str> for ReplicationStatusType {
99    fn from(s: &str) -> Self {
100        match s {
101            "PENDING" => ReplicationStatusType::Pending,
102            "COMPLETED" => ReplicationStatusType::Completed,
103            "COMPLETE" => ReplicationStatusType::CompletedLegacy,
104            "FAILED" => ReplicationStatusType::Failed,
105            "REPLICA" => ReplicationStatusType::Replica,
106            _ => ReplicationStatusType::Empty,
107        }
108    }
109}
110
111impl From<VersionPurgeStatusType> for ReplicationStatusType {
112    fn from(status: VersionPurgeStatusType) -> Self {
113        match status {
114            VersionPurgeStatusType::Pending => ReplicationStatusType::Pending,
115            VersionPurgeStatusType::Complete => ReplicationStatusType::Completed,
116            VersionPurgeStatusType::Failed => ReplicationStatusType::Failed,
117            VersionPurgeStatusType::Empty => ReplicationStatusType::Empty,
118        }
119    }
120}
121
122#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
123pub enum VersionPurgeStatusType {
124    Pending,
125    Complete,
126    Failed,
127    #[default]
128    Empty,
129}
130
131impl VersionPurgeStatusType {
132    /// Returns string representation of version purge status
133    pub fn as_str(&self) -> &'static str {
134        match self {
135            VersionPurgeStatusType::Pending => "PENDING",
136            VersionPurgeStatusType::Complete => "COMPLETE",
137            VersionPurgeStatusType::Failed => "FAILED",
138            VersionPurgeStatusType::Empty => "",
139        }
140    }
141
142    /// Returns true if the version is pending purge.
143    pub fn is_pending(&self) -> bool {
144        matches!(self, VersionPurgeStatusType::Pending | VersionPurgeStatusType::Failed)
145    }
146
147    pub fn is_empty(&self) -> bool {
148        matches!(self, VersionPurgeStatusType::Empty)
149    }
150}
151
152impl fmt::Display for VersionPurgeStatusType {
153    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154        write!(f, "{}", self.as_str())
155    }
156}
157
158impl From<&str> for VersionPurgeStatusType {
159    fn from(s: &str) -> Self {
160        match s {
161            "PENDING" => VersionPurgeStatusType::Pending,
162            "COMPLETE" => VersionPurgeStatusType::Complete,
163            "FAILED" => VersionPurgeStatusType::Failed,
164            _ => VersionPurgeStatusType::Empty,
165        }
166    }
167}
168
169/// Type - replication type enum
170#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
171pub enum ReplicationType {
172    #[default]
173    Unset,
174    Object,
175    Delete,
176    Metadata,
177    Heal,
178    ExistingObject,
179    Resync,
180    All,
181}
182
183impl ReplicationType {
184    pub fn as_str(&self) -> &'static str {
185        match self {
186            ReplicationType::Unset => "",
187            ReplicationType::Object => "OBJECT",
188            ReplicationType::Delete => "DELETE",
189            ReplicationType::Metadata => "METADATA",
190            ReplicationType::Heal => "HEAL",
191            ReplicationType::ExistingObject => "EXISTING_OBJECT",
192            ReplicationType::Resync => "RESYNC",
193            ReplicationType::All => "ALL",
194        }
195    }
196
197    pub fn is_valid(&self) -> bool {
198        matches!(
199            self,
200            ReplicationType::Object
201                | ReplicationType::Delete
202                | ReplicationType::Metadata
203                | ReplicationType::Heal
204                | ReplicationType::ExistingObject
205                | ReplicationType::Resync
206                | ReplicationType::All
207        )
208    }
209
210    pub fn is_data_replication(&self) -> bool {
211        matches!(self, ReplicationType::Object | ReplicationType::Delete | ReplicationType::Heal)
212    }
213}
214
215impl fmt::Display for ReplicationType {
216    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
217        write!(f, "{}", self.as_str())
218    }
219}
220
221impl From<&str> for ReplicationType {
222    fn from(s: &str) -> Self {
223        match s {
224            "UNSET" => ReplicationType::Unset,
225            "OBJECT" => ReplicationType::Object,
226            "DELETE" => ReplicationType::Delete,
227            "METADATA" => ReplicationType::Metadata,
228            "HEAL" => ReplicationType::Heal,
229            "EXISTING_OBJECT" => ReplicationType::ExistingObject,
230            "RESYNC" => ReplicationType::Resync,
231            "ALL" => ReplicationType::All,
232            _ => ReplicationType::Unset,
233        }
234    }
235}
236
237/// ReplicationState represents internal replication state
238/// Bounds on the per-target delete-marker version map. The map is rebuilt from
239/// attacker-influenced object metadata, so cap the entry count and both string
240/// lengths rather than trusting what was persisted.
241pub(crate) const MAX_REPLICATION_TARGET_VERSION_ENTRIES: usize = 1_000;
242pub(crate) const MAX_REPLICATION_TARGET_ARN_LEN: usize = 1_024;
243pub(crate) const MAX_REPLICATION_TARGET_VERSION_ID_LEN: usize = 1_024;
244
245#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
246pub struct ReplicationState {
247    pub replica_timestamp: Option<OffsetDateTime>,
248    pub replica_status: ReplicationStatusType,
249    pub delete_marker: bool,
250    pub replication_timestamp: Option<OffsetDateTime>,
251    pub replication_status_internal: Option<String>,
252    pub version_purge_status_internal: Option<String>,
253    pub replicate_decision_str: String,
254    pub targets: HashMap<String, ReplicationStatusType>,
255    pub purge_targets: HashMap<String, VersionPurgeStatusType>,
256    pub reset_statuses_map: HashMap<String, String>,
257    /// Exact version id the delete marker got on each replication target, keyed
258    /// by target ARN. Skipped by serde: `ReplicationState` has a positional wire
259    /// form, so this travels in the object's internal metadata instead and is
260    /// re-derived on read. See `persist_target_delete_marker_versions`.
261    #[serde(skip)]
262    pub target_delete_marker_version_ids: HashMap<String, String>,
263    /// Set when the persisted keys disagreed across the dual internal prefixes,
264    /// so callers fail closed instead of purging the wrong target version.
265    #[serde(skip)]
266    pub target_delete_marker_version_ids_corrupt: bool,
267}
268
269impl ReplicationState {
270    pub fn new() -> Self {
271        Self::default()
272    }
273
274    /// Returns true if replication state is identical for version purge statuses and replication statuses
275    pub fn equal(&self, other: &ReplicationState) -> bool {
276        self.replica_status == other.replica_status
277            && self.replication_status_internal == other.replication_status_internal
278            && self.version_purge_status_internal == other.version_purge_status_internal
279    }
280
281    /// Returns overall replication status for the object version being replicated
282    pub fn composite_replication_status(&self) -> ReplicationStatusType {
283        if let Some(replication_status_internal) = &self.replication_status_internal {
284            match ReplicationStatusType::from(replication_status_internal.as_str()) {
285                ReplicationStatusType::Pending
286                | ReplicationStatusType::Completed
287                | ReplicationStatusType::Failed
288                | ReplicationStatusType::Replica => {
289                    return ReplicationStatusType::from(replication_status_internal.as_str());
290                }
291                _ => {
292                    let repl_status = get_composite_replication_status(&self.targets);
293
294                    if self.replica_timestamp.is_none() {
295                        return repl_status;
296                    }
297
298                    if repl_status == ReplicationStatusType::Completed
299                        && let (Some(replica_timestamp), Some(replication_timestamp)) =
300                            (self.replica_timestamp, self.replication_timestamp)
301                        && replica_timestamp > replication_timestamp
302                    {
303                        return self.replica_status.clone();
304                    }
305
306                    return repl_status;
307                }
308            }
309        } else if !self.replica_status.is_empty() {
310            return self.replica_status.clone();
311        }
312
313        ReplicationStatusType::default()
314    }
315
316    /// Returns overall replication purge status for the permanent delete being replicated
317    pub fn composite_version_purge_status(&self) -> VersionPurgeStatusType {
318        match VersionPurgeStatusType::from(self.version_purge_status_internal.clone().unwrap_or_default().as_str()) {
319            VersionPurgeStatusType::Pending | VersionPurgeStatusType::Complete | VersionPurgeStatusType::Failed => {
320                VersionPurgeStatusType::from(self.version_purge_status_internal.clone().unwrap_or_default().as_str())
321            }
322            _ => get_composite_version_purge_status(&self.purge_targets),
323        }
324    }
325
326    /// Returns replicatedInfos struct initialized with the previous state of replication
327    pub fn target_state(&self, arn: &str) -> ReplicatedTargetInfo {
328        let resync_timestamp = self
329            .reset_statuses_map
330            .get(&target_reset_header(arn))
331            .or_else(|| self.reset_statuses_map.get(arn))
332            .cloned()
333            .unwrap_or_default();
334
335        ReplicatedTargetInfo {
336            arn: arn.to_string(),
337            prev_replication_status: self.targets.get(arn).cloned().unwrap_or_default(),
338            version_purge_status: self.purge_targets.get(arn).cloned().unwrap_or_default(),
339            target_delete_marker_version_id: self.target_delete_marker_version_ids.get(arn).cloned(),
340            resync_timestamp,
341            ..Default::default()
342        }
343    }
344}
345
346pub fn get_composite_replication_status(targets: &HashMap<String, ReplicationStatusType>) -> ReplicationStatusType {
347    if targets.is_empty() {
348        return ReplicationStatusType::Empty;
349    }
350
351    let mut completed = 0;
352    for status in targets.values() {
353        match status {
354            ReplicationStatusType::Failed => return ReplicationStatusType::Failed,
355            ReplicationStatusType::Completed => completed += 1,
356            _ => {}
357        }
358    }
359
360    if completed == targets.len() {
361        ReplicationStatusType::Completed
362    } else {
363        ReplicationStatusType::Pending
364    }
365}
366
367pub fn get_composite_version_purge_status(targets: &HashMap<String, VersionPurgeStatusType>) -> VersionPurgeStatusType {
368    if targets.is_empty() {
369        return VersionPurgeStatusType::default();
370    }
371
372    let mut completed = 0;
373    for status in targets.values() {
374        match status {
375            VersionPurgeStatusType::Failed => return VersionPurgeStatusType::Failed,
376            VersionPurgeStatusType::Complete => completed += 1,
377            _ => {}
378        }
379    }
380
381    if completed == targets.len() {
382        VersionPurgeStatusType::Complete
383    } else {
384        VersionPurgeStatusType::Pending
385    }
386}
387
388#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
389pub enum ReplicationAction {
390    /// Replicate all data
391    All,
392    /// Replicate only metadata
393    Metadata,
394    /// Do not replicate
395    #[default]
396    None,
397}
398
399impl ReplicationAction {
400    /// Returns string representation of replication action
401    pub fn as_str(&self) -> &'static str {
402        match self {
403            ReplicationAction::All => "all",
404            ReplicationAction::Metadata => "metadata",
405            ReplicationAction::None => "none",
406        }
407    }
408}
409
410impl fmt::Display for ReplicationAction {
411    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
412        write!(f, "{}", self.as_str())
413    }
414}
415
416impl From<&str> for ReplicationAction {
417    fn from(s: &str) -> Self {
418        match s {
419            "all" => ReplicationAction::All,
420            "metadata" => ReplicationAction::Metadata,
421            "none" => ReplicationAction::None,
422            _ => ReplicationAction::None,
423        }
424    }
425}
426
427/// ReplicatedTargetInfo struct represents replication info on a target
428#[derive(Debug, Clone, Serialize, Deserialize, Default)]
429pub struct ReplicatedTargetInfo {
430    pub arn: String,
431    pub size: i64,
432    pub duration: Duration,
433    pub replication_action: ReplicationAction,
434    pub op_type: ReplicationType,
435    pub replication_status: ReplicationStatusType,
436    pub prev_replication_status: ReplicationStatusType,
437    pub version_purge_status: VersionPurgeStatusType,
438    pub resync_timestamp: String,
439    pub replication_resynced: bool,
440    pub endpoint: String,
441    pub secure: bool,
442    pub error: Option<String>,
443    #[serde(default, skip_serializing_if = "Option::is_none")]
444    pub target_delete_marker_version_id: Option<String>,
445    /// Kept in step with the replication crate's copy: the id a target that
446    /// mints its own version ids assigned to this object version.
447    #[serde(default, skip_serializing_if = "Option::is_none")]
448    pub target_version_id: Option<String>,
449}
450
451impl ReplicatedTargetInfo {
452    /// Returns true for a target if arn is empty
453    pub fn is_empty(&self) -> bool {
454        self.arn.is_empty()
455    }
456}
457
458/// ReplicatedInfos struct contains replication information for multiple targets
459#[derive(Debug, Clone)]
460pub struct ReplicatedInfos {
461    pub replication_timestamp: Option<OffsetDateTime>,
462    pub targets: Vec<ReplicatedTargetInfo>,
463}
464
465impl ReplicatedInfos {
466    /// Returns the total size of completed replications
467    pub fn completed_size(&self) -> i64 {
468        let mut sz = 0i64;
469        for target in &self.targets {
470            if target.is_empty() {
471                continue;
472            }
473            if target.replication_status == ReplicationStatusType::Completed
474                && target.prev_replication_status != ReplicationStatusType::Completed
475            {
476                sz += target.size;
477            }
478        }
479        sz
480    }
481
482    /// Returns true if replication was attempted on any of the targets for the object version queued
483    pub fn replication_resynced(&self) -> bool {
484        for target in &self.targets {
485            if target.is_empty() || !target.replication_resynced {
486                continue;
487            }
488            return true;
489        }
490        false
491    }
492
493    /// Returns internal representation of replication status for all targets
494    pub fn replication_status_internal(&self) -> Option<String> {
495        let mut result = String::new();
496        for target in &self.targets {
497            if target.is_empty() {
498                continue;
499            }
500            result.push_str(&format!("{}={};", target.arn, target.replication_status));
501        }
502        if result.is_empty() { None } else { Some(result) }
503    }
504
505    /// Returns overall replication status across all targets
506    pub fn replication_status(&self) -> ReplicationStatusType {
507        if self.targets.is_empty() {
508            return ReplicationStatusType::Empty;
509        }
510
511        let mut completed = 0;
512        for target in &self.targets {
513            match target.replication_status {
514                ReplicationStatusType::Failed => return ReplicationStatusType::Failed,
515                ReplicationStatusType::Completed => completed += 1,
516                _ => {}
517            }
518        }
519
520        if completed == self.targets.len() {
521            ReplicationStatusType::Completed
522        } else {
523            ReplicationStatusType::Pending
524        }
525    }
526
527    /// Returns overall version purge status across all targets
528    pub fn version_purge_status(&self) -> VersionPurgeStatusType {
529        if self.targets.is_empty() {
530            return VersionPurgeStatusType::Empty;
531        }
532
533        let mut completed = 0;
534        for target in &self.targets {
535            match target.version_purge_status {
536                VersionPurgeStatusType::Failed => return VersionPurgeStatusType::Failed,
537                VersionPurgeStatusType::Complete => completed += 1,
538                _ => {}
539            }
540        }
541
542        if completed == self.targets.len() {
543            VersionPurgeStatusType::Complete
544        } else {
545            VersionPurgeStatusType::Pending
546        }
547    }
548
549    /// Returns internal representation of version purge status for all targets
550    pub fn version_purge_status_internal(&self) -> Option<String> {
551        let mut result = String::new();
552        for target in &self.targets {
553            if target.is_empty() || target.version_purge_status.is_empty() {
554                continue;
555            }
556            result.push_str(&format!("{}={};", target.arn, target.version_purge_status));
557        }
558        if result.is_empty() { None } else { Some(result) }
559    }
560
561    /// Returns replication action based on target that actually performed replication
562    pub fn action(&self) -> ReplicationAction {
563        for target in &self.targets {
564            if target.is_empty() {
565                continue;
566            }
567            // rely on replication action from target that actually performed replication now.
568            if target.prev_replication_status != ReplicationStatusType::Completed {
569                return target.replication_action;
570            }
571        }
572        ReplicationAction::None
573    }
574}
575
576/// Distinguishes the kind of operation stored in [`MrfReplicateEntry`].
577///
578/// Old serialized files lack the `op` key; `default` maps to `Object`, which preserves
579/// the pre-existing replay behaviour for entries written before this field existed.
580#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
581pub enum MrfOpKind {
582    #[default]
583    #[serde(rename = "object")]
584    Object,
585    #[serde(rename = "delete")]
586    Delete,
587}
588
589#[derive(Serialize, Deserialize, Debug, Clone)]
590pub struct MrfReplicateEntry {
591    #[serde(rename = "bucket")]
592    pub bucket: String,
593
594    #[serde(rename = "object")]
595    pub object: String,
596
597    // Persisted so recovery after restart can replay the exact version.
598    // Old serialized files lack this key; `default` fills in None safely.
599    #[serde(rename = "versionID", skip_serializing_if = "Option::is_none", default)]
600    pub version_id: Option<Uuid>,
601
602    #[serde(rename = "retryCount")]
603    pub retry_count: i32,
604
605    #[serde(rename = "size", default)]
606    pub size: i64,
607
608    // Operation kind. Old files lack this key; default=Object preserves existing behaviour.
609    #[serde(rename = "op", default)]
610    pub op: MrfOpKind,
611
612    // For delete entries: the delete-marker version id (distinct from version_id, which is
613    // the version being purged). Old files lack this; default=None is correct.
614    #[serde(rename = "deleteMarkerVersionID", skip_serializing_if = "Option::is_none", default)]
615    pub delete_marker_version_id: Option<Uuid>,
616
617    // For delete entries: whether this is a delete-marker vs a versioned-object delete.
618    // Old files lack this; default=false is correct.
619    #[serde(rename = "deleteMarker", default)]
620    pub delete_marker: bool,
621
622    // For delete entries: the original delete-marker mtime, persisted as Unix nanoseconds so
623    // replay stamps replicas with the source timestamp instead of the replay time. Old files
624    // lack this key; default=None means "unknown", and replay falls back to the current time
625    // to preserve pre-existing behaviour (backlog#867).
626    #[serde(rename = "deleteMarkerMtime", skip_serializing_if = "Option::is_none", default)]
627    pub delete_marker_mtime: Option<i64>,
628}
629
630pub trait ReplicationWorkerOperation: Any + Send + Sync {
631    fn to_mrf_entry(&self) -> MrfReplicateEntry;
632    fn as_any(&self) -> &dyn Any;
633    fn get_bucket(&self) -> &str;
634    fn get_object(&self) -> &str;
635    fn get_size(&self) -> i64;
636    fn is_delete_marker(&self) -> bool;
637    fn get_op_type(&self) -> ReplicationType;
638}
639
640#[derive(Debug, Clone, Serialize, Deserialize, Default)]
641pub struct ReplicateTargetDecision {
642    pub replicate: bool,
643    pub synchronous: bool,
644    pub arn: String,
645    pub id: String,
646}
647
648impl ReplicateTargetDecision {
649    pub fn new(arn: String, replicate: bool, sync: bool) -> Self {
650        Self {
651            replicate,
652            synchronous: sync,
653            arn,
654            id: String::new(),
655        }
656    }
657}
658
659impl fmt::Display for ReplicateTargetDecision {
660    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
661        write!(f, "{};{};{};{}", self.replicate, self.synchronous, self.arn, self.id)
662    }
663}
664
665/// ReplicateDecision represents replication decision for each target
666#[derive(Debug, Clone, Serialize, Deserialize)]
667pub struct ReplicateDecision {
668    pub targets_map: HashMap<String, ReplicateTargetDecision>,
669}
670
671impl ReplicateDecision {
672    pub fn new() -> Self {
673        Self {
674            targets_map: HashMap::new(),
675        }
676    }
677
678    /// Returns true if at least one target qualifies for replication
679    pub fn replicate_any(&self) -> bool {
680        self.targets_map.values().any(|t| t.replicate)
681    }
682
683    /// Returns true if at least one target qualifies for synchronous replication
684    pub fn is_synchronous(&self) -> bool {
685        self.targets_map.values().any(|t| t.synchronous)
686    }
687
688    /// Updates ReplicateDecision with target's replication decision
689    pub fn set(&mut self, target: ReplicateTargetDecision) {
690        self.targets_map.insert(target.arn.clone(), target);
691    }
692
693    /// Returns a stringified representation of internal replication status with all targets marked as `PENDING`
694    pub fn pending_status(&self) -> Option<String> {
695        let mut result = String::new();
696        for target in self.targets_map.values() {
697            if target.replicate {
698                result.push_str(&format!("{}={};", target.arn, ReplicationStatusType::Pending.as_str()));
699            }
700        }
701        if result.is_empty() { None } else { Some(result) }
702    }
703}
704
705impl fmt::Display for ReplicateDecision {
706    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
707        let mut result = String::new();
708        for (key, value) in &self.targets_map {
709            result.push_str(&format!("{key}={value},"));
710        }
711        write!(f, "{}", result.trim_end_matches(','))
712    }
713}
714
715impl Default for ReplicateDecision {
716    fn default() -> Self {
717        Self::new()
718    }
719}
720
721// parse k-v pairs of target ARN to stringified ReplicateTargetDecision delimited by ',' into a
722// ReplicateDecision struct
723pub fn parse_replicate_decision(_bucket: &str, s: &str) -> std::io::Result<ReplicateDecision> {
724    let mut decision = ReplicateDecision::new();
725
726    if s.is_empty() {
727        return Ok(decision);
728    }
729
730    for p in s.split(',') {
731        if p.is_empty() {
732            continue;
733        }
734
735        let slc = p.split('=').collect::<Vec<&str>>();
736        if slc.len() != 2 {
737            return Err(std::io::Error::new(
738                std::io::ErrorKind::InvalidInput,
739                format!("invalid replicate decision format: {s}"),
740            ));
741        }
742
743        let tgt_str = slc[1].trim_matches('"');
744        let tgt = tgt_str.split(';').collect::<Vec<&str>>();
745        if tgt.len() != 4 {
746            return Err(std::io::Error::new(
747                std::io::ErrorKind::InvalidInput,
748                format!("invalid replicate decision format: {s}"),
749            ));
750        }
751
752        let tgt = ReplicateTargetDecision {
753            replicate: tgt[0] == "true",
754            synchronous: tgt[1] == "true",
755            arn: tgt[2].to_string(),
756            id: tgt[3].to_string(),
757        };
758        decision.targets_map.insert(slc[0].to_string(), tgt);
759    }
760
761    Ok(decision)
762
763    // r = ReplicateDecision{
764    // 	targetsMap: make(map[string]replicateTargetDecision),
765    // }
766    // if len(s) == 0 {
767    // 	return
768    // }
769    // for _, p := range strings.Split(s, ",") {
770    // 	if p == "" {
771    // 		continue
772    // 	}
773    // 	slc := strings.Split(p, "=")
774    // 	if len(slc) != 2 {
775    // 		return r, errInvalidReplicateDecisionFormat
776    // 	}
777    // 	tgtStr := strings.TrimSuffix(strings.TrimPrefix(slc[1], `"`), `"`)
778    // 	tgt := strings.Split(tgtStr, ";")
779    // 	if len(tgt) != 4 {
780    // 		return r, errInvalidReplicateDecisionFormat
781    // 	}
782    // 	r.targetsMap[slc[0]] = replicateTargetDecision{Replicate: tgt[0] == "true", Synchronous: tgt[1] == "true", Arn: tgt[2], ID: tgt[3]}
783    // }
784}
785
786#[derive(Debug, Clone, Default, Serialize, Deserialize)]
787pub struct ReplicateObjectInfo {
788    pub name: String,
789    pub size: i64,
790    pub actual_size: i64,
791    pub bucket: String,
792    pub version_id: Option<Uuid>,
793    pub etag: Option<String>,
794    pub mod_time: Option<OffsetDateTime>,
795    pub replication_status: ReplicationStatusType,
796    pub replication_status_internal: Option<String>,
797    pub delete_marker: bool,
798    pub version_purge_status_internal: Option<String>,
799    pub version_purge_status: VersionPurgeStatusType,
800    pub replication_state: Option<ReplicationState>,
801    pub op_type: ReplicationType,
802    pub event_type: String,
803    pub dsc: ReplicateDecision,
804    pub existing_obj_resync: ResyncDecision,
805    pub target_statuses: HashMap<String, ReplicationStatusType>,
806    pub target_purge_statuses: HashMap<String, VersionPurgeStatusType>,
807    pub replication_timestamp: Option<OffsetDateTime>,
808    pub ssec: bool,
809    pub user_tags: String,
810    pub checksum: Option<Bytes>,
811    pub retry_count: u32,
812}
813
814impl ReplicationWorkerOperation for ReplicateObjectInfo {
815    fn as_any(&self) -> &dyn Any {
816        self
817    }
818
819    fn to_mrf_entry(&self) -> MrfReplicateEntry {
820        MrfReplicateEntry {
821            bucket: self.bucket.clone(),
822            object: self.name.clone(),
823            version_id: self.version_id,
824            retry_count: self.retry_count as i32,
825            size: self.size,
826            op: MrfOpKind::Object,
827            delete_marker_version_id: None,
828            delete_marker: false,
829            delete_marker_mtime: None,
830        }
831    }
832
833    fn get_bucket(&self) -> &str {
834        &self.bucket
835    }
836
837    fn get_object(&self) -> &str {
838        &self.name
839    }
840
841    fn get_size(&self) -> i64 {
842        self.size
843    }
844
845    fn is_delete_marker(&self) -> bool {
846        self.delete_marker
847    }
848
849    fn get_op_type(&self) -> ReplicationType {
850        self.op_type
851    }
852}
853
854static REPL_STATUS_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"([^=].*?)=([^,].*?);").unwrap());
855
856impl ReplicateObjectInfo {
857    /// Returns replication status of a target
858    pub fn target_replication_status(&self, arn: &str) -> ReplicationStatusType {
859        let binding = self.replication_status_internal.clone().unwrap_or_default();
860        let captures = REPL_STATUS_REGEX.captures_iter(&binding);
861        for cap in captures {
862            if cap.len() == 3 && &cap[1] == arn {
863                return ReplicationStatusType::from(&cap[2]);
864            }
865        }
866        ReplicationStatusType::default()
867    }
868
869    /// Returns the relevant info needed by MRF
870    pub fn to_mrf_entry(&self) -> MrfReplicateEntry {
871        MrfReplicateEntry {
872            bucket: self.bucket.clone(),
873            object: self.name.clone(),
874            version_id: self.version_id,
875            retry_count: self.retry_count as i32,
876            size: self.size,
877            op: MrfOpKind::Object,
878            delete_marker_version_id: None,
879            delete_marker: false,
880            delete_marker_mtime: None,
881        }
882    }
883}
884
885// constructs a replication status map from string representation
886pub fn replication_statuses_map(s: &str) -> HashMap<String, ReplicationStatusType> {
887    let mut targets = HashMap::new();
888    let rep_stat_matches = REPL_STATUS_REGEX.captures_iter(s).map(|c| c.extract());
889    for (_, [arn, status]) in rep_stat_matches {
890        if arn.is_empty() {
891            continue;
892        }
893        let status = ReplicationStatusType::from(status);
894        targets.insert(arn.to_string(), status);
895    }
896    targets
897}
898
899// constructs a version purge status map from string representation
900pub fn version_purge_statuses_map(s: &str) -> HashMap<String, VersionPurgeStatusType> {
901    let mut targets = HashMap::new();
902    let purge_status_matches = REPL_STATUS_REGEX.captures_iter(s).map(|c| c.extract());
903    for (_, [arn, status]) in purge_status_matches {
904        if arn.is_empty() {
905            continue;
906        }
907        let status = VersionPurgeStatusType::from(status);
908        targets.insert(arn.to_string(), status);
909    }
910    targets
911}
912
913fn replication_statuses_string(targets: &HashMap<String, ReplicationStatusType>) -> Option<String> {
914    let mut result = String::new();
915    for (arn, status) in targets {
916        if arn.is_empty() || status.is_empty() {
917            continue;
918        }
919        result.push_str(&format!("{arn}={status};"));
920    }
921    if result.is_empty() { None } else { Some(result) }
922}
923
924fn version_purge_statuses_string(targets: &HashMap<String, VersionPurgeStatusType>) -> Option<String> {
925    let mut result = String::new();
926    for (arn, status) in targets {
927        if arn.is_empty() || status.is_empty() {
928            continue;
929        }
930        result.push_str(&format!("{arn}={status};"));
931    }
932    if result.is_empty() { None } else { Some(result) }
933}
934
935pub fn get_replication_state(rinfos: &ReplicatedInfos, prev_state: &ReplicationState, _vid: Option<String>) -> ReplicationState {
936    let reset_status_map: Vec<(String, String)> = rinfos
937        .targets
938        .iter()
939        .filter(|v| !v.resync_timestamp.is_empty())
940        .map(|t| (target_reset_header(t.arn.as_str()), t.resync_timestamp.clone()))
941        .collect();
942
943    let mut targets = prev_state.targets.clone();
944    for (arn, status) in replication_statuses_map(&rinfos.replication_status_internal().unwrap_or_default()) {
945        targets.insert(arn, status);
946    }
947
948    let mut purge_targets = prev_state.purge_targets.clone();
949    for (arn, status) in version_purge_statuses_map(&rinfos.version_purge_status_internal().unwrap_or_default()) {
950        purge_targets.insert(arn, status);
951    }
952
953    let repl_statuses = replication_statuses_string(&targets);
954    let vpurge_statuses = version_purge_statuses_string(&purge_targets);
955
956    let mut reset_statuses_map = prev_state.reset_statuses_map.clone();
957    for (key, value) in reset_status_map {
958        reset_statuses_map.insert(key, value);
959    }
960
961    // Carry the previously recorded per-target delete-marker versions forward,
962    // dropping anything that no longer satisfies the bounds, then fold in the
963    // versions this round's targets reported. A map that has already grown past
964    // the cap is discarded rather than trusted.
965    let mut target_delete_marker_version_ids = prev_state.target_delete_marker_version_ids.clone();
966    target_delete_marker_version_ids.retain(|arn, version_id| {
967        !arn.is_empty()
968            && arn.len() <= MAX_REPLICATION_TARGET_ARN_LEN
969            && !version_id.is_empty()
970            && version_id.len() <= MAX_REPLICATION_TARGET_VERSION_ID_LEN
971    });
972    if target_delete_marker_version_ids.len() > MAX_REPLICATION_TARGET_VERSION_ENTRIES {
973        target_delete_marker_version_ids.clear();
974    }
975    for target in &rinfos.targets {
976        let Some(version_id) = target.target_delete_marker_version_id.as_ref() else {
977            continue;
978        };
979        if (!target_delete_marker_version_ids.contains_key(&target.arn)
980            && target_delete_marker_version_ids.len() >= MAX_REPLICATION_TARGET_VERSION_ENTRIES)
981            || target.arn.is_empty()
982            || target.arn.len() > MAX_REPLICATION_TARGET_ARN_LEN
983            || version_id.is_empty()
984            || version_id.len() > MAX_REPLICATION_TARGET_VERSION_ID_LEN
985        {
986            continue;
987        }
988        target_delete_marker_version_ids.insert(target.arn.clone(), version_id.clone());
989    }
990
991    ReplicationState {
992        replicate_decision_str: prev_state.replicate_decision_str.clone(),
993        reset_statuses_map,
994        replica_timestamp: prev_state.replica_timestamp,
995        replica_status: prev_state.replica_status.clone(),
996        targets,
997        replication_status_internal: repl_statuses,
998        replication_timestamp: rinfos.replication_timestamp,
999        purge_targets,
1000        version_purge_status_internal: vpurge_statuses,
1001        target_delete_marker_version_ids,
1002        target_delete_marker_version_ids_corrupt: prev_state.target_delete_marker_version_ids_corrupt,
1003
1004        ..Default::default()
1005    }
1006}
1007
1008pub fn target_reset_header(arn: &str) -> String {
1009    internal_key_rustfs(&format!("{REPLICATION_RESET}-{arn}"))
1010}
1011
1012#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1013pub struct ResyncTargetDecision {
1014    pub replicate: bool,
1015    pub reset_id: String,
1016    pub reset_before_date: Option<OffsetDateTime>,
1017}
1018
1019/// ResyncDecision is a struct representing a map with target's individual resync decisions
1020#[derive(Debug, Clone, Serialize, Deserialize)]
1021pub struct ResyncDecision {
1022    pub targets: HashMap<String, ResyncTargetDecision>,
1023}
1024
1025impl ResyncDecision {
1026    pub fn new() -> Self {
1027        Self { targets: HashMap::new() }
1028    }
1029
1030    /// Returns true if no targets with resync decision present
1031    pub fn is_empty(&self) -> bool {
1032        self.targets.is_empty()
1033    }
1034
1035    pub fn must_resync(&self) -> bool {
1036        self.targets.values().any(|v| v.replicate)
1037    }
1038
1039    pub fn must_resync_target(&self, tgt_arn: &str) -> bool {
1040        self.targets.get(tgt_arn).map(|v| v.replicate).unwrap_or(false)
1041    }
1042}
1043
1044impl Default for ResyncDecision {
1045    fn default() -> Self {
1046        Self::new()
1047    }
1048}
1049
1050#[cfg(test)]
1051mod tests {
1052    use super::*;
1053
1054    #[test]
1055    fn target_state_reads_resync_timestamp_from_target_reset_header_key() {
1056        let arn = "arn:rustfs:replication:us-east-1:target:bucket";
1057        let timestamp = "2026-06-30T00:00:00Z;reset-1".to_string();
1058        let mut state = ReplicationState::default();
1059        state.reset_statuses_map.insert(target_reset_header(arn), timestamp.clone());
1060
1061        let target_state = state.target_state(arn);
1062
1063        assert_eq!(target_state.resync_timestamp, timestamp);
1064    }
1065
1066    #[test]
1067    fn get_replication_state_preserves_untouched_target_statuses() {
1068        let target_a = "arn:target:a".to_string();
1069        let target_b = "arn:target:b".to_string();
1070        let mut prev_state = ReplicationState::default();
1071        prev_state.targets.insert(target_a.clone(), ReplicationStatusType::Failed);
1072        prev_state.targets.insert(target_b.clone(), ReplicationStatusType::Completed);
1073
1074        let rinfos = ReplicatedInfos {
1075            replication_timestamp: None,
1076            targets: vec![ReplicatedTargetInfo {
1077                arn: target_a.clone(),
1078                replication_status: ReplicationStatusType::Completed,
1079                ..Default::default()
1080            }],
1081        };
1082
1083        let state = get_replication_state(&rinfos, &prev_state, None);
1084
1085        assert_eq!(state.targets.get(&target_a), Some(&ReplicationStatusType::Completed));
1086        assert_eq!(state.targets.get(&target_b), Some(&ReplicationStatusType::Completed));
1087        assert_eq!(
1088            replication_statuses_map(&state.replication_status_internal.unwrap_or_default()).get(&target_b),
1089            Some(&ReplicationStatusType::Completed)
1090        );
1091    }
1092}