use bytes::Bytes;
use core::fmt;
use regex::Regex;
use rustfs_utils::http::internal_key_rustfs;
use serde::{Deserialize, Serialize};
use std::any::Any;
use std::collections::HashMap;
use std::sync::LazyLock;
use std::time::Duration;
use time::OffsetDateTime;
use uuid::Uuid;
pub const REPLICATION_RESET: &str = "replication-reset";
pub const REPLICATION_STATUS: &str = "replication-status";
pub const REPLICATE_QUEUED: &str = "replicate:queue";
pub const REPLICATE_EXISTING: &str = "replicate:existing";
pub const REPLICATE_EXISTING_DELETE: &str = "replicate:existing:delete";
pub const REPLICATE_MRF: &str = "replicate:mrf";
pub const REPLICATE_INCOMING: &str = "replicate:incoming";
pub const REPLICATE_INCOMING_DELETE: &str = "replicate:incoming:delete";
pub const REPLICATE_HEAL: &str = "replicate:heal";
pub const REPLICATE_HEAL_DELETE: &str = "replicate:heal:delete";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default, Hash)]
pub enum ReplicationStatusType {
Pending,
Completed,
CompletedLegacy,
Failed,
Replica,
#[default]
Empty,
}
impl ReplicationStatusType {
pub fn as_str(&self) -> &'static str {
match self {
ReplicationStatusType::Pending => "PENDING",
ReplicationStatusType::Completed => "COMPLETED",
ReplicationStatusType::CompletedLegacy => "COMPLETE",
ReplicationStatusType::Failed => "FAILED",
ReplicationStatusType::Replica => "REPLICA",
ReplicationStatusType::Empty => "",
}
}
pub fn is_empty(&self) -> bool {
matches!(self, ReplicationStatusType::Empty)
}
}
impl fmt::Display for ReplicationStatusType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl From<&str> for ReplicationStatusType {
fn from(s: &str) -> Self {
match s {
"PENDING" => ReplicationStatusType::Pending,
"COMPLETED" => ReplicationStatusType::Completed,
"COMPLETE" => ReplicationStatusType::CompletedLegacy,
"FAILED" => ReplicationStatusType::Failed,
"REPLICA" => ReplicationStatusType::Replica,
_ => ReplicationStatusType::Empty,
}
}
}
impl From<VersionPurgeStatusType> for ReplicationStatusType {
fn from(status: VersionPurgeStatusType) -> Self {
match status {
VersionPurgeStatusType::Pending => ReplicationStatusType::Pending,
VersionPurgeStatusType::Complete => ReplicationStatusType::Completed,
VersionPurgeStatusType::Failed => ReplicationStatusType::Failed,
VersionPurgeStatusType::Empty => ReplicationStatusType::Empty,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum VersionPurgeStatusType {
Pending,
Complete,
Failed,
#[default]
Empty,
}
impl VersionPurgeStatusType {
pub fn as_str(&self) -> &'static str {
match self {
VersionPurgeStatusType::Pending => "PENDING",
VersionPurgeStatusType::Complete => "COMPLETE",
VersionPurgeStatusType::Failed => "FAILED",
VersionPurgeStatusType::Empty => "",
}
}
pub fn is_pending(&self) -> bool {
matches!(self, VersionPurgeStatusType::Pending | VersionPurgeStatusType::Failed)
}
pub fn is_empty(&self) -> bool {
matches!(self, VersionPurgeStatusType::Empty)
}
}
impl fmt::Display for VersionPurgeStatusType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl From<&str> for VersionPurgeStatusType {
fn from(s: &str) -> Self {
match s {
"PENDING" => VersionPurgeStatusType::Pending,
"COMPLETE" => VersionPurgeStatusType::Complete,
"FAILED" => VersionPurgeStatusType::Failed,
_ => VersionPurgeStatusType::Empty,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum ReplicationType {
#[default]
Unset,
Object,
Delete,
Metadata,
Heal,
ExistingObject,
Resync,
All,
}
impl ReplicationType {
pub fn as_str(&self) -> &'static str {
match self {
ReplicationType::Unset => "",
ReplicationType::Object => "OBJECT",
ReplicationType::Delete => "DELETE",
ReplicationType::Metadata => "METADATA",
ReplicationType::Heal => "HEAL",
ReplicationType::ExistingObject => "EXISTING_OBJECT",
ReplicationType::Resync => "RESYNC",
ReplicationType::All => "ALL",
}
}
pub fn is_valid(&self) -> bool {
matches!(
self,
ReplicationType::Object
| ReplicationType::Delete
| ReplicationType::Metadata
| ReplicationType::Heal
| ReplicationType::ExistingObject
| ReplicationType::Resync
| ReplicationType::All
)
}
pub fn is_data_replication(&self) -> bool {
matches!(self, ReplicationType::Object | ReplicationType::Delete | ReplicationType::Heal)
}
}
impl fmt::Display for ReplicationType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl From<&str> for ReplicationType {
fn from(s: &str) -> Self {
match s {
"UNSET" => ReplicationType::Unset,
"OBJECT" => ReplicationType::Object,
"DELETE" => ReplicationType::Delete,
"METADATA" => ReplicationType::Metadata,
"HEAL" => ReplicationType::Heal,
"EXISTING_OBJECT" => ReplicationType::ExistingObject,
"RESYNC" => ReplicationType::Resync,
"ALL" => ReplicationType::All,
_ => ReplicationType::Unset,
}
}
}
pub(crate) const MAX_REPLICATION_TARGET_VERSION_ENTRIES: usize = 1_000;
pub(crate) const MAX_REPLICATION_TARGET_ARN_LEN: usize = 1_024;
pub(crate) const MAX_REPLICATION_TARGET_VERSION_ID_LEN: usize = 1_024;
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct ReplicationState {
pub replica_timestamp: Option<OffsetDateTime>,
pub replica_status: ReplicationStatusType,
pub delete_marker: bool,
pub replication_timestamp: Option<OffsetDateTime>,
pub replication_status_internal: Option<String>,
pub version_purge_status_internal: Option<String>,
pub replicate_decision_str: String,
pub targets: HashMap<String, ReplicationStatusType>,
pub purge_targets: HashMap<String, VersionPurgeStatusType>,
pub reset_statuses_map: HashMap<String, String>,
#[serde(skip)]
pub target_delete_marker_version_ids: HashMap<String, String>,
#[serde(skip)]
pub target_delete_marker_version_ids_corrupt: bool,
}
impl ReplicationState {
pub fn new() -> Self {
Self::default()
}
pub fn equal(&self, other: &ReplicationState) -> bool {
self.replica_status == other.replica_status
&& self.replication_status_internal == other.replication_status_internal
&& self.version_purge_status_internal == other.version_purge_status_internal
}
pub fn composite_replication_status(&self) -> ReplicationStatusType {
if let Some(replication_status_internal) = &self.replication_status_internal {
match ReplicationStatusType::from(replication_status_internal.as_str()) {
ReplicationStatusType::Pending
| ReplicationStatusType::Completed
| ReplicationStatusType::Failed
| ReplicationStatusType::Replica => {
return ReplicationStatusType::from(replication_status_internal.as_str());
}
_ => {
let repl_status = get_composite_replication_status(&self.targets);
if self.replica_timestamp.is_none() {
return repl_status;
}
if repl_status == ReplicationStatusType::Completed
&& let (Some(replica_timestamp), Some(replication_timestamp)) =
(self.replica_timestamp, self.replication_timestamp)
&& replica_timestamp > replication_timestamp
{
return self.replica_status.clone();
}
return repl_status;
}
}
} else if !self.replica_status.is_empty() {
return self.replica_status.clone();
}
ReplicationStatusType::default()
}
pub fn composite_version_purge_status(&self) -> VersionPurgeStatusType {
match VersionPurgeStatusType::from(self.version_purge_status_internal.clone().unwrap_or_default().as_str()) {
VersionPurgeStatusType::Pending | VersionPurgeStatusType::Complete | VersionPurgeStatusType::Failed => {
VersionPurgeStatusType::from(self.version_purge_status_internal.clone().unwrap_or_default().as_str())
}
_ => get_composite_version_purge_status(&self.purge_targets),
}
}
pub fn target_state(&self, arn: &str) -> ReplicatedTargetInfo {
let resync_timestamp = self
.reset_statuses_map
.get(&target_reset_header(arn))
.or_else(|| self.reset_statuses_map.get(arn))
.cloned()
.unwrap_or_default();
ReplicatedTargetInfo {
arn: arn.to_string(),
prev_replication_status: self.targets.get(arn).cloned().unwrap_or_default(),
version_purge_status: self.purge_targets.get(arn).cloned().unwrap_or_default(),
target_delete_marker_version_id: self.target_delete_marker_version_ids.get(arn).cloned(),
resync_timestamp,
..Default::default()
}
}
}
pub fn get_composite_replication_status(targets: &HashMap<String, ReplicationStatusType>) -> ReplicationStatusType {
if targets.is_empty() {
return ReplicationStatusType::Empty;
}
let mut completed = 0;
for status in targets.values() {
match status {
ReplicationStatusType::Failed => return ReplicationStatusType::Failed,
ReplicationStatusType::Completed => completed += 1,
_ => {}
}
}
if completed == targets.len() {
ReplicationStatusType::Completed
} else {
ReplicationStatusType::Pending
}
}
pub fn get_composite_version_purge_status(targets: &HashMap<String, VersionPurgeStatusType>) -> VersionPurgeStatusType {
if targets.is_empty() {
return VersionPurgeStatusType::default();
}
let mut completed = 0;
for status in targets.values() {
match status {
VersionPurgeStatusType::Failed => return VersionPurgeStatusType::Failed,
VersionPurgeStatusType::Complete => completed += 1,
_ => {}
}
}
if completed == targets.len() {
VersionPurgeStatusType::Complete
} else {
VersionPurgeStatusType::Pending
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum ReplicationAction {
All,
Metadata,
#[default]
None,
}
impl ReplicationAction {
pub fn as_str(&self) -> &'static str {
match self {
ReplicationAction::All => "all",
ReplicationAction::Metadata => "metadata",
ReplicationAction::None => "none",
}
}
}
impl fmt::Display for ReplicationAction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl From<&str> for ReplicationAction {
fn from(s: &str) -> Self {
match s {
"all" => ReplicationAction::All,
"metadata" => ReplicationAction::Metadata,
"none" => ReplicationAction::None,
_ => ReplicationAction::None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ReplicatedTargetInfo {
pub arn: String,
pub size: i64,
pub duration: Duration,
pub replication_action: ReplicationAction,
pub op_type: ReplicationType,
pub replication_status: ReplicationStatusType,
pub prev_replication_status: ReplicationStatusType,
pub version_purge_status: VersionPurgeStatusType,
pub resync_timestamp: String,
pub replication_resynced: bool,
pub endpoint: String,
pub secure: bool,
pub error: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target_delete_marker_version_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target_version_id: Option<String>,
}
impl ReplicatedTargetInfo {
pub fn is_empty(&self) -> bool {
self.arn.is_empty()
}
}
#[derive(Debug, Clone)]
pub struct ReplicatedInfos {
pub replication_timestamp: Option<OffsetDateTime>,
pub targets: Vec<ReplicatedTargetInfo>,
}
impl ReplicatedInfos {
pub fn completed_size(&self) -> i64 {
let mut sz = 0i64;
for target in &self.targets {
if target.is_empty() {
continue;
}
if target.replication_status == ReplicationStatusType::Completed
&& target.prev_replication_status != ReplicationStatusType::Completed
{
sz += target.size;
}
}
sz
}
pub fn replication_resynced(&self) -> bool {
for target in &self.targets {
if target.is_empty() || !target.replication_resynced {
continue;
}
return true;
}
false
}
pub fn replication_status_internal(&self) -> Option<String> {
let mut result = String::new();
for target in &self.targets {
if target.is_empty() {
continue;
}
result.push_str(&format!("{}={};", target.arn, target.replication_status));
}
if result.is_empty() { None } else { Some(result) }
}
pub fn replication_status(&self) -> ReplicationStatusType {
if self.targets.is_empty() {
return ReplicationStatusType::Empty;
}
let mut completed = 0;
for target in &self.targets {
match target.replication_status {
ReplicationStatusType::Failed => return ReplicationStatusType::Failed,
ReplicationStatusType::Completed => completed += 1,
_ => {}
}
}
if completed == self.targets.len() {
ReplicationStatusType::Completed
} else {
ReplicationStatusType::Pending
}
}
pub fn version_purge_status(&self) -> VersionPurgeStatusType {
if self.targets.is_empty() {
return VersionPurgeStatusType::Empty;
}
let mut completed = 0;
for target in &self.targets {
match target.version_purge_status {
VersionPurgeStatusType::Failed => return VersionPurgeStatusType::Failed,
VersionPurgeStatusType::Complete => completed += 1,
_ => {}
}
}
if completed == self.targets.len() {
VersionPurgeStatusType::Complete
} else {
VersionPurgeStatusType::Pending
}
}
pub fn version_purge_status_internal(&self) -> Option<String> {
let mut result = String::new();
for target in &self.targets {
if target.is_empty() || target.version_purge_status.is_empty() {
continue;
}
result.push_str(&format!("{}={};", target.arn, target.version_purge_status));
}
if result.is_empty() { None } else { Some(result) }
}
pub fn action(&self) -> ReplicationAction {
for target in &self.targets {
if target.is_empty() {
continue;
}
if target.prev_replication_status != ReplicationStatusType::Completed {
return target.replication_action;
}
}
ReplicationAction::None
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum MrfOpKind {
#[default]
#[serde(rename = "object")]
Object,
#[serde(rename = "delete")]
Delete,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct MrfReplicateEntry {
#[serde(rename = "bucket")]
pub bucket: String,
#[serde(rename = "object")]
pub object: String,
#[serde(rename = "versionID", skip_serializing_if = "Option::is_none", default)]
pub version_id: Option<Uuid>,
#[serde(rename = "retryCount")]
pub retry_count: i32,
#[serde(rename = "size", default)]
pub size: i64,
#[serde(rename = "op", default)]
pub op: MrfOpKind,
#[serde(rename = "deleteMarkerVersionID", skip_serializing_if = "Option::is_none", default)]
pub delete_marker_version_id: Option<Uuid>,
#[serde(rename = "deleteMarker", default)]
pub delete_marker: bool,
#[serde(rename = "deleteMarkerMtime", skip_serializing_if = "Option::is_none", default)]
pub delete_marker_mtime: Option<i64>,
}
pub trait ReplicationWorkerOperation: Any + Send + Sync {
fn to_mrf_entry(&self) -> MrfReplicateEntry;
fn as_any(&self) -> &dyn Any;
fn get_bucket(&self) -> &str;
fn get_object(&self) -> &str;
fn get_size(&self) -> i64;
fn is_delete_marker(&self) -> bool;
fn get_op_type(&self) -> ReplicationType;
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ReplicateTargetDecision {
pub replicate: bool,
pub synchronous: bool,
pub arn: String,
pub id: String,
}
impl ReplicateTargetDecision {
pub fn new(arn: String, replicate: bool, sync: bool) -> Self {
Self {
replicate,
synchronous: sync,
arn,
id: String::new(),
}
}
}
impl fmt::Display for ReplicateTargetDecision {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{};{};{};{}", self.replicate, self.synchronous, self.arn, self.id)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReplicateDecision {
pub targets_map: HashMap<String, ReplicateTargetDecision>,
}
impl ReplicateDecision {
pub fn new() -> Self {
Self {
targets_map: HashMap::new(),
}
}
pub fn replicate_any(&self) -> bool {
self.targets_map.values().any(|t| t.replicate)
}
pub fn is_synchronous(&self) -> bool {
self.targets_map.values().any(|t| t.synchronous)
}
pub fn set(&mut self, target: ReplicateTargetDecision) {
self.targets_map.insert(target.arn.clone(), target);
}
pub fn pending_status(&self) -> Option<String> {
let mut result = String::new();
for target in self.targets_map.values() {
if target.replicate {
result.push_str(&format!("{}={};", target.arn, ReplicationStatusType::Pending.as_str()));
}
}
if result.is_empty() { None } else { Some(result) }
}
}
impl fmt::Display for ReplicateDecision {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut result = String::new();
for (key, value) in &self.targets_map {
result.push_str(&format!("{key}={value},"));
}
write!(f, "{}", result.trim_end_matches(','))
}
}
impl Default for ReplicateDecision {
fn default() -> Self {
Self::new()
}
}
pub fn parse_replicate_decision(_bucket: &str, s: &str) -> std::io::Result<ReplicateDecision> {
let mut decision = ReplicateDecision::new();
if s.is_empty() {
return Ok(decision);
}
for p in s.split(',') {
if p.is_empty() {
continue;
}
let slc = p.split('=').collect::<Vec<&str>>();
if slc.len() != 2 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("invalid replicate decision format: {s}"),
));
}
let tgt_str = slc[1].trim_matches('"');
let tgt = tgt_str.split(';').collect::<Vec<&str>>();
if tgt.len() != 4 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("invalid replicate decision format: {s}"),
));
}
let tgt = ReplicateTargetDecision {
replicate: tgt[0] == "true",
synchronous: tgt[1] == "true",
arn: tgt[2].to_string(),
id: tgt[3].to_string(),
};
decision.targets_map.insert(slc[0].to_string(), tgt);
}
Ok(decision)
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ReplicateObjectInfo {
pub name: String,
pub size: i64,
pub actual_size: i64,
pub bucket: String,
pub version_id: Option<Uuid>,
pub etag: Option<String>,
pub mod_time: Option<OffsetDateTime>,
pub replication_status: ReplicationStatusType,
pub replication_status_internal: Option<String>,
pub delete_marker: bool,
pub version_purge_status_internal: Option<String>,
pub version_purge_status: VersionPurgeStatusType,
pub replication_state: Option<ReplicationState>,
pub op_type: ReplicationType,
pub event_type: String,
pub dsc: ReplicateDecision,
pub existing_obj_resync: ResyncDecision,
pub target_statuses: HashMap<String, ReplicationStatusType>,
pub target_purge_statuses: HashMap<String, VersionPurgeStatusType>,
pub replication_timestamp: Option<OffsetDateTime>,
pub ssec: bool,
pub user_tags: String,
pub checksum: Option<Bytes>,
pub retry_count: u32,
}
impl ReplicationWorkerOperation for ReplicateObjectInfo {
fn as_any(&self) -> &dyn Any {
self
}
fn to_mrf_entry(&self) -> MrfReplicateEntry {
MrfReplicateEntry {
bucket: self.bucket.clone(),
object: self.name.clone(),
version_id: self.version_id,
retry_count: self.retry_count as i32,
size: self.size,
op: MrfOpKind::Object,
delete_marker_version_id: None,
delete_marker: false,
delete_marker_mtime: None,
}
}
fn get_bucket(&self) -> &str {
&self.bucket
}
fn get_object(&self) -> &str {
&self.name
}
fn get_size(&self) -> i64 {
self.size
}
fn is_delete_marker(&self) -> bool {
self.delete_marker
}
fn get_op_type(&self) -> ReplicationType {
self.op_type
}
}
static REPL_STATUS_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"([^=].*?)=([^,].*?);").unwrap());
impl ReplicateObjectInfo {
pub fn target_replication_status(&self, arn: &str) -> ReplicationStatusType {
let binding = self.replication_status_internal.clone().unwrap_or_default();
let captures = REPL_STATUS_REGEX.captures_iter(&binding);
for cap in captures {
if cap.len() == 3 && &cap[1] == arn {
return ReplicationStatusType::from(&cap[2]);
}
}
ReplicationStatusType::default()
}
pub fn to_mrf_entry(&self) -> MrfReplicateEntry {
MrfReplicateEntry {
bucket: self.bucket.clone(),
object: self.name.clone(),
version_id: self.version_id,
retry_count: self.retry_count as i32,
size: self.size,
op: MrfOpKind::Object,
delete_marker_version_id: None,
delete_marker: false,
delete_marker_mtime: None,
}
}
}
pub fn replication_statuses_map(s: &str) -> HashMap<String, ReplicationStatusType> {
let mut targets = HashMap::new();
let rep_stat_matches = REPL_STATUS_REGEX.captures_iter(s).map(|c| c.extract());
for (_, [arn, status]) in rep_stat_matches {
if arn.is_empty() {
continue;
}
let status = ReplicationStatusType::from(status);
targets.insert(arn.to_string(), status);
}
targets
}
pub fn version_purge_statuses_map(s: &str) -> HashMap<String, VersionPurgeStatusType> {
let mut targets = HashMap::new();
let purge_status_matches = REPL_STATUS_REGEX.captures_iter(s).map(|c| c.extract());
for (_, [arn, status]) in purge_status_matches {
if arn.is_empty() {
continue;
}
let status = VersionPurgeStatusType::from(status);
targets.insert(arn.to_string(), status);
}
targets
}
fn replication_statuses_string(targets: &HashMap<String, ReplicationStatusType>) -> Option<String> {
let mut result = String::new();
for (arn, status) in targets {
if arn.is_empty() || status.is_empty() {
continue;
}
result.push_str(&format!("{arn}={status};"));
}
if result.is_empty() { None } else { Some(result) }
}
fn version_purge_statuses_string(targets: &HashMap<String, VersionPurgeStatusType>) -> Option<String> {
let mut result = String::new();
for (arn, status) in targets {
if arn.is_empty() || status.is_empty() {
continue;
}
result.push_str(&format!("{arn}={status};"));
}
if result.is_empty() { None } else { Some(result) }
}
pub fn get_replication_state(rinfos: &ReplicatedInfos, prev_state: &ReplicationState, _vid: Option<String>) -> ReplicationState {
let reset_status_map: Vec<(String, String)> = rinfos
.targets
.iter()
.filter(|v| !v.resync_timestamp.is_empty())
.map(|t| (target_reset_header(t.arn.as_str()), t.resync_timestamp.clone()))
.collect();
let mut targets = prev_state.targets.clone();
for (arn, status) in replication_statuses_map(&rinfos.replication_status_internal().unwrap_or_default()) {
targets.insert(arn, status);
}
let mut purge_targets = prev_state.purge_targets.clone();
for (arn, status) in version_purge_statuses_map(&rinfos.version_purge_status_internal().unwrap_or_default()) {
purge_targets.insert(arn, status);
}
let repl_statuses = replication_statuses_string(&targets);
let vpurge_statuses = version_purge_statuses_string(&purge_targets);
let mut reset_statuses_map = prev_state.reset_statuses_map.clone();
for (key, value) in reset_status_map {
reset_statuses_map.insert(key, value);
}
let mut target_delete_marker_version_ids = prev_state.target_delete_marker_version_ids.clone();
target_delete_marker_version_ids.retain(|arn, version_id| {
!arn.is_empty()
&& arn.len() <= MAX_REPLICATION_TARGET_ARN_LEN
&& !version_id.is_empty()
&& version_id.len() <= MAX_REPLICATION_TARGET_VERSION_ID_LEN
});
if target_delete_marker_version_ids.len() > MAX_REPLICATION_TARGET_VERSION_ENTRIES {
target_delete_marker_version_ids.clear();
}
for target in &rinfos.targets {
let Some(version_id) = target.target_delete_marker_version_id.as_ref() else {
continue;
};
if (!target_delete_marker_version_ids.contains_key(&target.arn)
&& target_delete_marker_version_ids.len() >= MAX_REPLICATION_TARGET_VERSION_ENTRIES)
|| target.arn.is_empty()
|| target.arn.len() > MAX_REPLICATION_TARGET_ARN_LEN
|| version_id.is_empty()
|| version_id.len() > MAX_REPLICATION_TARGET_VERSION_ID_LEN
{
continue;
}
target_delete_marker_version_ids.insert(target.arn.clone(), version_id.clone());
}
ReplicationState {
replicate_decision_str: prev_state.replicate_decision_str.clone(),
reset_statuses_map,
replica_timestamp: prev_state.replica_timestamp,
replica_status: prev_state.replica_status.clone(),
targets,
replication_status_internal: repl_statuses,
replication_timestamp: rinfos.replication_timestamp,
purge_targets,
version_purge_status_internal: vpurge_statuses,
target_delete_marker_version_ids,
target_delete_marker_version_ids_corrupt: prev_state.target_delete_marker_version_ids_corrupt,
..Default::default()
}
}
pub fn target_reset_header(arn: &str) -> String {
internal_key_rustfs(&format!("{REPLICATION_RESET}-{arn}"))
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ResyncTargetDecision {
pub replicate: bool,
pub reset_id: String,
pub reset_before_date: Option<OffsetDateTime>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResyncDecision {
pub targets: HashMap<String, ResyncTargetDecision>,
}
impl ResyncDecision {
pub fn new() -> Self {
Self { targets: HashMap::new() }
}
pub fn is_empty(&self) -> bool {
self.targets.is_empty()
}
pub fn must_resync(&self) -> bool {
self.targets.values().any(|v| v.replicate)
}
pub fn must_resync_target(&self, tgt_arn: &str) -> bool {
self.targets.get(tgt_arn).map(|v| v.replicate).unwrap_or(false)
}
}
impl Default for ResyncDecision {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn target_state_reads_resync_timestamp_from_target_reset_header_key() {
let arn = "arn:rustfs:replication:us-east-1:target:bucket";
let timestamp = "2026-06-30T00:00:00Z;reset-1".to_string();
let mut state = ReplicationState::default();
state.reset_statuses_map.insert(target_reset_header(arn), timestamp.clone());
let target_state = state.target_state(arn);
assert_eq!(target_state.resync_timestamp, timestamp);
}
#[test]
fn get_replication_state_preserves_untouched_target_statuses() {
let target_a = "arn:target:a".to_string();
let target_b = "arn:target:b".to_string();
let mut prev_state = ReplicationState::default();
prev_state.targets.insert(target_a.clone(), ReplicationStatusType::Failed);
prev_state.targets.insert(target_b.clone(), ReplicationStatusType::Completed);
let rinfos = ReplicatedInfos {
replication_timestamp: None,
targets: vec![ReplicatedTargetInfo {
arn: target_a.clone(),
replication_status: ReplicationStatusType::Completed,
..Default::default()
}],
};
let state = get_replication_state(&rinfos, &prev_state, None);
assert_eq!(state.targets.get(&target_a), Some(&ReplicationStatusType::Completed));
assert_eq!(state.targets.get(&target_b), Some(&ReplicationStatusType::Completed));
assert_eq!(
replication_statuses_map(&state.replication_status_internal.unwrap_or_default()).get(&target_b),
Some(&ReplicationStatusType::Completed)
);
}
}