1use 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
30pub const REPLICATE_QUEUED: &str = "replicate:queue";
32
33pub const REPLICATE_EXISTING: &str = "replicate:existing";
35pub const REPLICATE_EXISTING_DELETE: &str = "replicate:existing:delete";
37
38pub const REPLICATE_MRF: &str = "replicate:mrf";
40pub const REPLICATE_INCOMING: &str = "replicate:incoming";
42pub const REPLICATE_INCOMING_DELETE: &str = "replicate:incoming:delete";
44
45pub const REPLICATE_HEAL: &str = "replicate:heal";
47pub const REPLICATE_HEAL_DELETE: &str = "replicate:heal:delete";
49
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default, Hash)]
60pub enum ReplicationStatusType {
61 Pending,
63 Completed,
65 CompletedLegacy,
67 Failed,
69 Replica,
71 #[default]
72 Empty,
73}
74
75impl ReplicationStatusType {
76 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 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 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#[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
237pub(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 #[serde(skip)]
262 pub target_delete_marker_version_ids: HashMap<String, String>,
263 #[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 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 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 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 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 All,
392 Metadata,
394 #[default]
396 None,
397}
398
399impl ReplicationAction {
400 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#[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 #[serde(default, skip_serializing_if = "Option::is_none")]
448 pub target_version_id: Option<String>,
449}
450
451impl ReplicatedTargetInfo {
452 pub fn is_empty(&self) -> bool {
454 self.arn.is_empty()
455 }
456}
457
458#[derive(Debug, Clone)]
460pub struct ReplicatedInfos {
461 pub replication_timestamp: Option<OffsetDateTime>,
462 pub targets: Vec<ReplicatedTargetInfo>,
463}
464
465impl ReplicatedInfos {
466 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 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 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 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 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 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 pub fn action(&self) -> ReplicationAction {
563 for target in &self.targets {
564 if target.is_empty() {
565 continue;
566 }
567 if target.prev_replication_status != ReplicationStatusType::Completed {
569 return target.replication_action;
570 }
571 }
572 ReplicationAction::None
573 }
574}
575
576#[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 #[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 #[serde(rename = "op", default)]
610 pub op: MrfOpKind,
611
612 #[serde(rename = "deleteMarkerVersionID", skip_serializing_if = "Option::is_none", default)]
615 pub delete_marker_version_id: Option<Uuid>,
616
617 #[serde(rename = "deleteMarker", default)]
620 pub delete_marker: bool,
621
622 #[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#[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 pub fn replicate_any(&self) -> bool {
680 self.targets_map.values().any(|t| t.replicate)
681 }
682
683 pub fn is_synchronous(&self) -> bool {
685 self.targets_map.values().any(|t| t.synchronous)
686 }
687
688 pub fn set(&mut self, target: ReplicateTargetDecision) {
690 self.targets_map.insert(target.arn.clone(), target);
691 }
692
693 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
721pub 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 }
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 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 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
885pub 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
899pub 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 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#[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 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}