refactor: Reimplement bucket replication system with enhanced architecture (#590)

* feat:refactor replication

* use aws sdk for replication client

* refactor/replication

* merge main

* fix lifecycle test
This commit is contained in:
weisd
2025-09-26 14:27:53 +08:00
committed by GitHub
parent 9b029d18b2
commit 90f21a9102
91 changed files with 10532 additions and 4917 deletions
+1 -1
View File
@@ -35,7 +35,7 @@ uuid = { workspace = true, features = ["v4", "fast-rng", "serde"] }
tokio = { workspace = true, features = ["io-util", "macros", "sync"] }
xxhash-rust = { workspace = true, features = ["xxh64"] }
bytes.workspace = true
rustfs-utils = { workspace = true, features = ["hash"] }
rustfs-utils = { workspace = true, features = ["hash","http"] }
byteorder = { workspace = true }
tracing.workspace = true
thiserror.workspace = true
+26 -3
View File
@@ -13,11 +13,11 @@
// limitations under the License.
use crate::error::{Error, Result};
use crate::headers::RESERVED_METADATA_PREFIX_LOWER;
use crate::headers::RUSTFS_HEALING;
use crate::{ReplicationState, ReplicationStatusType, VersionPurgeStatusType};
use bytes::Bytes;
use rmp_serde::Serializer;
use rustfs_utils::HashAlgorithm;
use rustfs_utils::http::headers::{RESERVED_METADATA_PREFIX_LOWER, RUSTFS_HEALING};
use serde::Deserialize;
use serde::Serialize;
use std::collections::HashMap;
@@ -202,7 +202,7 @@ pub struct FileInfo {
// MarkDeleted marks this version as deleted
pub mark_deleted: bool,
// ReplicationState - Internal replication state to be passed back in ObjectInfo
// pub replication_state: Option<ReplicationState>, // TODO: implement ReplicationState
pub replication_state_internal: Option<ReplicationState>,
pub data: Option<Bytes>,
pub num_versions: usize,
pub successor_mod_time: Option<OffsetDateTime>,
@@ -471,6 +471,29 @@ impl FileInfo {
// TODO: Add replication_state comparison when implemented
// && self.replication_state == other.replication_state
}
pub fn version_purge_status(&self) -> VersionPurgeStatusType {
self.replication_state_internal
.as_ref()
.map(|v| v.composite_version_purge_status())
.unwrap_or(VersionPurgeStatusType::Empty)
}
pub fn replication_status(&self) -> ReplicationStatusType {
self.replication_state_internal
.as_ref()
.map(|v| v.composite_replication_status())
.unwrap_or(ReplicationStatusType::Empty)
}
pub fn delete_marker_replication_status(&self) -> ReplicationStatusType {
if self.deleted {
self.replication_state_internal
.as_ref()
.map(|v| v.composite_replication_status())
.unwrap_or(ReplicationStatusType::Empty)
} else {
ReplicationStatusType::Empty
}
}
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
+297 -103
View File
@@ -15,12 +15,13 @@
use crate::error::{Error, Result};
use crate::fileinfo::{ErasureAlgo, ErasureInfo, FileInfo, FileInfoVersions, ObjectPartInfo, RawFileInfo};
use crate::filemeta_inline::InlineData;
use crate::headers::{
use crate::{ReplicationStatusType, VersionPurgeStatusType};
use byteorder::ByteOrder;
use bytes::Bytes;
use rustfs_utils::http::headers::{
self, AMZ_META_UNENCRYPTED_CONTENT_LENGTH, AMZ_META_UNENCRYPTED_CONTENT_MD5, AMZ_STORAGE_CLASS, RESERVED_METADATA_PREFIX,
RESERVED_METADATA_PREFIX_LOWER, VERSION_PURGE_STATUS_KEY,
};
use byteorder::ByteOrder;
use bytes::Bytes;
use s3s::header::X_AMZ_RESTORE;
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
@@ -30,6 +31,7 @@ use std::io::{Read, Write};
use std::{collections::HashMap, io::Cursor};
use time::OffsetDateTime;
use tokio::io::AsyncRead;
use tracing::error;
use uuid::Uuid;
use xxhash_rust::xxh64;
@@ -159,39 +161,57 @@ impl FileMeta {
let i = buf.len() as u64;
// check version, buf = buf[8..]
let (buf, _, _) = Self::check_xl2_v1(buf)?;
let (buf, _, _) = Self::check_xl2_v1(buf).map_err(|e| {
error!("failed to check XL2 v1 format: {}", e);
e
})?;
let (mut size_buf, buf) = buf.split_at(5);
// Get meta data, buf = crc + data
let bin_len = rmp::decode::read_bin_len(&mut size_buf)?;
let bin_len = rmp::decode::read_bin_len(&mut size_buf).map_err(|e| {
error!("failed to read binary length for metadata: {}", e);
Error::other(format!("failed to read binary length for metadata: {e}"))
})?;
if buf.len() < bin_len as usize {
error!("insufficient data for metadata: expected {} bytes, got {} bytes", bin_len, buf.len());
return Err(Error::other("insufficient data for metadata"));
}
let (meta, buf) = buf.split_at(bin_len as usize);
if buf.len() < 5 {
error!("insufficient data for CRC: expected 5 bytes, got {} bytes", buf.len());
return Err(Error::other("insufficient data for CRC"));
}
let (mut crc_buf, buf) = buf.split_at(5);
// crc check
let crc = rmp::decode::read_u32(&mut crc_buf)?;
let crc = rmp::decode::read_u32(&mut crc_buf).map_err(|e| {
error!("failed to read CRC value: {}", e);
Error::other(format!("failed to read CRC value: {e}"))
})?;
let meta_crc = xxh64::xxh64(meta, XXHASH_SEED) as u32;
if crc != meta_crc {
error!("xl file crc check failed: expected CRC {:#x}, got {:#x}", meta_crc, crc);
return Err(Error::other("xl file crc check failed"));
}
if !buf.is_empty() {
self.data.update(buf);
self.data.validate()?;
self.data.validate().map_err(|e| {
error!("data validation failed: {}", e);
e
})?;
}
// Parse meta
if !meta.is_empty() {
let (versions_len, _, meta_ver, meta) = Self::decode_xl_headers(meta)?;
let (versions_len, _, meta_ver, meta) = Self::decode_xl_headers(meta).map_err(|e| {
error!("failed to decode XL headers: {}", e);
e
})?;
// let (_, meta) = meta.split_at(read_size as usize);
@@ -201,24 +221,30 @@ impl FileMeta {
let mut cur: Cursor<&[u8]> = Cursor::new(meta);
for _ in 0..versions_len {
let bin_len = rmp::decode::read_bin_len(&mut cur)? as usize;
let start = cur.position() as usize;
let end = start + bin_len;
let header_buf = &meta[start..end];
let bin_len = rmp::decode::read_bin_len(&mut cur).map_err(|e| {
error!("failed to read binary length for version header: {}", e);
Error::other(format!("failed to read binary length for version header: {e}"))
})? as usize;
let mut header_buf = vec![0u8; bin_len];
cur.read_exact(&mut header_buf)?;
let mut ver = FileMetaShallowVersion::default();
ver.header.unmarshal_msg(header_buf)?;
ver.header.unmarshal_msg(&header_buf).map_err(|e| {
error!("failed to unmarshal version header: {}", e);
e
})?;
cur.set_position(end as u64);
let bin_len = rmp::decode::read_bin_len(&mut cur).map_err(|e| {
error!("failed to read binary length for version metadata: {}", e);
Error::other(format!("failed to read binary length for version metadata: {e}"))
})? as usize;
let bin_len = rmp::decode::read_bin_len(&mut cur)? as usize;
let start = cur.position() as usize;
let end = start + bin_len;
let ver_meta_buf = &meta[start..end];
let mut ver_meta_buf = vec![0u8; bin_len];
cur.read_exact(&mut ver_meta_buf)?;
ver.meta.extend_from_slice(ver_meta_buf);
cur.set_position(end as u64);
ver.meta.extend_from_slice(&ver_meta_buf);
self.versions.push(ver);
}
@@ -487,39 +513,39 @@ impl FileMeta {
let version = FileMetaVersion::from(fi);
self.add_version_filemata(version)
}
pub fn add_version_filemata(&mut self, version: FileMetaVersion) -> Result<()> {
if !version.valid() {
return Err(Error::other("file meta version invalid"));
}
// should replace
for (idx, ver) in self.versions.iter().enumerate() {
if ver.header.version_id != vid {
continue;
}
return self.set_idx(idx, version);
// 1000 is the limit of versions TODO: make it configurable
if self.versions.len() + 1 > 1000 {
return Err(Error::other(
"You've exceeded the limit on the number of versions you can create on this object",
));
}
// TODO: version count limit !
if self.versions.is_empty() {
self.versions.push(FileMetaShallowVersion::try_from(version)?);
return Ok(());
}
let vid = version.get_version_id();
if let Some(fidx) = self.versions.iter().position(|v| v.header.version_id == vid) {
return self.set_idx(fidx, version);
}
let mod_time = version.get_mod_time();
// puth a -1 mod time value , so we can relplace this
self.versions.push(FileMetaShallowVersion {
header: FileMetaVersionHeader {
mod_time: Some(OffsetDateTime::from_unix_timestamp(-1)?),
..Default::default()
},
..Default::default()
});
for (idx, exist) in self.versions.iter().enumerate() {
if let Some(ref ex_mt) = exist.header.mod_time {
if let Some(ref in_md) = mod_time {
if ex_mt <= in_md {
// insert
self.versions.insert(idx, FileMetaShallowVersion::try_from(version)?);
self.versions.pop();
return Ok(());
}
}
@@ -527,35 +553,33 @@ impl FileMeta {
}
Err(Error::other("add_version failed"))
}
pub fn add_version_filemata(&mut self, ver: FileMetaVersion) -> Result<()> {
if !ver.valid() {
return Err(Error::other("attempted to add invalid version"));
}
// if !ver.valid() {
// return Err(Error::other("attempted to add invalid version"));
// }
if self.versions.len() + 1 >= 100 {
return Err(Error::other(
"You've exceeded the limit on the number of versions you can create on this object",
));
}
// if self.versions.len() + 1 >= 100 {
// return Err(Error::other(
// "You've exceeded the limit on the number of versions you can create on this object",
// ));
// }
let mod_time = ver.get_mod_time();
let encoded = ver.marshal_msg()?;
let new_version = FileMetaShallowVersion {
header: ver.header(),
meta: encoded,
};
// let mod_time = ver.get_mod_time();
// let encoded = ver.marshal_msg()?;
// let new_version = FileMetaShallowVersion {
// header: ver.header(),
// meta: encoded,
// };
// Find the insertion position: insert before the first element with mod_time >= new mod_time
// This maintains descending order by mod_time (newest first)
let insert_pos = self
.versions
.iter()
.position(|existing| existing.header.mod_time <= mod_time)
.unwrap_or(self.versions.len());
self.versions.insert(insert_pos, new_version);
Ok(())
// // Find the insertion position: insert before the first element with mod_time >= new mod_time
// // This maintains descending order by mod_time (newest first)
// let insert_pos = self
// .versions
// .iter()
// .position(|existing| existing.header.mod_time <= mod_time)
// .unwrap_or(self.versions.len());
// self.versions.insert(insert_pos, new_version);
// Ok(())
}
// delete_version deletes version, returns data_dir
@@ -575,10 +599,97 @@ impl FileMeta {
}
let mut update_version = fi.mark_deleted;
/*if fi.version_purge_status().is_empty()
if fi.version_purge_status().is_empty()
&& (fi.delete_marker_replication_status() == ReplicationStatusType::Replica
|| fi.delete_marker_replication_status() == ReplicationStatusType::Empty)
{
update_version = fi.mark_deleted;
}*/
} else {
if fi.deleted
&& fi.version_purge_status() != VersionPurgeStatusType::Complete
&& (!fi.version_purge_status().is_empty() || fi.delete_marker_replication_status().is_empty())
{
update_version = true;
}
if !fi.version_purge_status().is_empty() && fi.version_purge_status() != VersionPurgeStatusType::Complete {
update_version = true;
}
}
if fi.deleted {
if !fi.delete_marker_replication_status().is_empty() {
if let Some(delete_marker) = ventry.delete_marker.as_mut() {
if fi.delete_marker_replication_status() == ReplicationStatusType::Replica {
delete_marker.meta_sys.insert(
format!("{}{}", RESERVED_METADATA_PREFIX_LOWER, "replica-status"),
fi.replication_state_internal
.as_ref()
.map(|v| v.replica_status.clone())
.unwrap_or_default()
.as_str()
.as_bytes()
.to_vec(),
);
delete_marker.meta_sys.insert(
format!("{}{}", RESERVED_METADATA_PREFIX_LOWER, "replica-timestamp"),
fi.replication_state_internal
.as_ref()
.map(|v| v.replica_timestamp.unwrap_or(OffsetDateTime::UNIX_EPOCH).to_string())
.unwrap_or_default()
.as_bytes()
.to_vec(),
);
} else {
delete_marker.meta_sys.insert(
format!("{}{}", RESERVED_METADATA_PREFIX_LOWER, "replication-status"),
fi.replication_state_internal
.as_ref()
.map(|v| v.replication_status_internal.clone().unwrap_or_default())
.unwrap_or_default()
.as_bytes()
.to_vec(),
);
delete_marker.meta_sys.insert(
format!("{}{}", RESERVED_METADATA_PREFIX_LOWER, "replication-timestamp"),
fi.replication_state_internal
.as_ref()
.map(|v| v.replication_timestamp.unwrap_or(OffsetDateTime::UNIX_EPOCH).to_string())
.unwrap_or_default()
.as_bytes()
.to_vec(),
);
}
}
}
if !fi.version_purge_status().is_empty() {
if let Some(delete_marker) = ventry.delete_marker.as_mut() {
delete_marker.meta_sys.insert(
VERSION_PURGE_STATUS_KEY.to_string(),
fi.replication_state_internal
.as_ref()
.map(|v| v.version_purge_status_internal.clone().unwrap_or_default())
.unwrap_or_default()
.as_bytes()
.to_vec(),
);
}
}
if let Some(delete_marker) = ventry.delete_marker.as_mut() {
for (k, v) in fi
.replication_state_internal
.as_ref()
.map(|v| v.reset_statuses_map.clone())
.unwrap_or_default()
{
delete_marker.meta_sys.insert(k.clone(), v.clone().as_bytes().to_vec());
}
}
}
// ???
if fi.transition_status == TRANSITION_COMPLETE {
update_version = false;
}
@@ -591,22 +702,111 @@ impl FileMeta {
match ver.header.version_type {
VersionType::Invalid | VersionType::Legacy => return Err(Error::other("invalid file meta version")),
VersionType::Delete => {
self.versions.remove(i);
if fi.deleted && fi.version_id.is_none() {
self.add_version_filemata(ventry)?;
if update_version {
let mut v = self.get_idx(i)?;
if v.delete_marker.is_none() {
v.delete_marker = Some(MetaDeleteMarker {
version_id: fi.version_id,
mod_time: fi.mod_time,
meta_sys: HashMap::new(),
});
}
if let Some(delete_marker) = v.delete_marker.as_mut() {
if !fi.delete_marker_replication_status().is_empty() {
if fi.delete_marker_replication_status() == ReplicationStatusType::Replica {
delete_marker.meta_sys.insert(
format!("{}{}", RESERVED_METADATA_PREFIX_LOWER, "replica-status"),
fi.replication_state_internal
.as_ref()
.map(|v| v.replica_status.clone())
.unwrap_or_default()
.as_str()
.as_bytes()
.to_vec(),
);
delete_marker.meta_sys.insert(
format!("{}{}", RESERVED_METADATA_PREFIX_LOWER, "replica-timestamp"),
fi.replication_state_internal
.as_ref()
.map(|v| v.replica_timestamp.unwrap_or(OffsetDateTime::UNIX_EPOCH).to_string())
.unwrap_or_default()
.as_bytes()
.to_vec(),
);
} else {
delete_marker.meta_sys.insert(
format!("{}{}", RESERVED_METADATA_PREFIX_LOWER, "replication-status"),
fi.replication_state_internal
.as_ref()
.map(|v| v.replication_status_internal.clone().unwrap_or_default())
.unwrap_or_default()
.as_bytes()
.to_vec(),
);
delete_marker.meta_sys.insert(
format!("{}{}", RESERVED_METADATA_PREFIX_LOWER, "replication-timestamp"),
fi.replication_state_internal
.as_ref()
.map(|v| v.replication_timestamp.unwrap_or(OffsetDateTime::UNIX_EPOCH).to_string())
.unwrap_or_default()
.as_bytes()
.to_vec(),
);
}
}
for (k, v) in fi
.replication_state_internal
.as_ref()
.map(|v| v.reset_statuses_map.clone())
.unwrap_or_default()
{
delete_marker.meta_sys.insert(k.clone(), v.clone().as_bytes().to_vec());
}
}
self.set_idx(i, v)?;
return Ok(None);
}
self.versions.remove(i);
if (fi.mark_deleted && fi.version_purge_status() != VersionPurgeStatusType::Complete)
|| (fi.deleted && fi.version_id.is_none())
{
self.add_version_filemata(ventry)?;
}
return Ok(None);
}
VersionType::Object => {
if update_version && !fi.deleted {
let v = self.get_idx(i)?;
let mut v = self.get_idx(i)?;
self.versions.remove(i);
if let Some(obj) = v.object.as_mut() {
obj.meta_sys.insert(
VERSION_PURGE_STATUS_KEY.to_string(),
fi.replication_state_internal
.as_ref()
.map(|v| v.version_purge_status_internal.clone().unwrap_or_default())
.unwrap_or_default()
.as_bytes()
.to_vec(),
);
for (k, v) in fi
.replication_state_internal
.as_ref()
.map(|v| v.reset_statuses_map.clone())
.unwrap_or_default()
{
obj.meta_sys.insert(k.clone(), v.clone().as_bytes().to_vec());
}
}
let a = v.object.map(|v| v.data_dir).unwrap_or_default();
return Ok(a);
let old_dir = v.object.as_ref().map(|v| v.data_dir).unwrap_or_default();
self.set_idx(i, v)?;
return Ok(old_dir);
}
}
}
@@ -641,31 +841,37 @@ impl FileMeta {
let obj_version_id = obj.version_id;
let obj_data_dir = obj.data_dir;
if fi.expire_restored {
let mut err = if fi.expire_restored {
obj.remove_restore_hdrs();
self.set_idx(i, ver)?;
self.set_idx(i, ver).err()
} else if fi.transition_status == TRANSITION_COMPLETE {
obj.set_transition(fi);
obj.reset_inline_data();
self.set_idx(i, ver)?;
self.set_idx(i, ver).err()
} else {
self.versions.remove(i);
let (free_version, to_free) = obj.init_free_version(fi);
if to_free {
self.add_version_filemata(free_version)?;
self.add_version_filemata(free_version).err()
} else {
None
}
}
};
if fi.deleted {
self.add_version_filemata(ventry)?;
err = self.add_version_filemata(ventry).err();
}
if self.shared_data_dir_count(obj_version_id, obj_data_dir) > 0 {
return Ok(None);
}
if let Some(e) = err {
return Err(e);
}
Ok(obj_data_dir)
}
@@ -1642,17 +1848,15 @@ impl MetaObject {
free_entry.delete_marker = Some(MetaDeleteMarker {
version_id: Some(vid),
mod_time: self.mod_time,
meta_sys: Some(HashMap::<String, Vec<u8>>::new()),
meta_sys: HashMap::<String, Vec<u8>>::new(),
});
free_entry
.delete_marker
.as_mut()
.unwrap()
let delete_marker = free_entry.delete_marker.as_mut().unwrap();
delete_marker
.meta_sys
.as_mut()
.unwrap()
.insert(format!("{RESERVED_METADATA_PREFIX_LOWER}{FREE_VERSION}"), vec![]);
let tier_key = format!("{RESERVED_METADATA_PREFIX_LOWER}{TRANSITION_TIER}");
let tier_obj_key = format!("{RESERVED_METADATA_PREFIX_LOWER}{TRANSITIONED_OBJECTNAME}");
let tier_obj_vid_key = format!("{RESERVED_METADATA_PREFIX_LOWER}{TRANSITIONED_VERSION_ID}");
@@ -1660,14 +1864,7 @@ impl MetaObject {
let aa = [tier_key, tier_obj_key, tier_obj_vid_key];
for (k, v) in &self.meta_sys {
if aa.contains(k) {
free_entry
.delete_marker
.as_mut()
.unwrap()
.meta_sys
.as_mut()
.unwrap()
.insert(k.clone(), v.clone());
delete_marker.meta_sys.insert(k.clone(), v.clone());
}
}
return (free_entry, true);
@@ -1737,19 +1934,16 @@ pub struct MetaDeleteMarker {
#[serde(rename = "MTime")]
pub mod_time: Option<OffsetDateTime>, // Object delete marker modified time
#[serde(rename = "MetaSys")]
pub meta_sys: Option<HashMap<String, Vec<u8>>>, // Delete marker internal metadata
pub meta_sys: HashMap<String, Vec<u8>>, // Delete marker internal metadata
}
impl MetaDeleteMarker {
pub fn free_version(&self) -> bool {
self.meta_sys
.as_ref()
.map(|v| v.get(FREE_VERSION_META_HEADER).is_some())
.unwrap_or_default()
self.meta_sys.contains_key(FREE_VERSION_META_HEADER)
}
pub fn into_fileinfo(&self, volume: &str, path: &str, _all_parts: bool) -> FileInfo {
let metadata = self.meta_sys.clone().unwrap_or_default();
let metadata = self.meta_sys.clone();
FileInfo {
version_id: self.version_id.filter(|&vid| !vid.is_nil()),
@@ -1895,7 +2089,7 @@ impl From<FileInfo> for MetaDeleteMarker {
Self {
version_id: value.version_id,
mod_time: value.mod_time,
meta_sys: None,
meta_sys: HashMap::new(),
}
}
}
@@ -2794,7 +2988,7 @@ mod test {
let delete_marker = MetaDeleteMarker {
version_id: Some(Uuid::new_v4()),
mod_time: Some(OffsetDateTime::now_utc()),
meta_sys: None,
meta_sys: HashMap::new(),
};
let delete_version = FileMetaVersion {
+4 -2
View File
@@ -16,8 +16,9 @@ mod error;
pub mod fileinfo;
mod filemeta;
mod filemeta_inline;
pub mod headers;
pub mod metacache;
// pub mod headers;
mod metacache;
mod replication;
pub mod test_data;
@@ -26,3 +27,4 @@ pub use fileinfo::*;
pub use filemeta::*;
pub use filemeta_inline::*;
pub use metacache::*;
pub use replication::*;
+494
View File
@@ -0,0 +1,494 @@
use core::fmt;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::Duration;
use time::OffsetDateTime;
/// StatusType of Replication for x-amz-replication-status header
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default, Hash)]
pub enum ReplicationStatusType {
/// Pending - replication is pending.
Pending,
/// Completed - replication completed ok.
Completed,
/// CompletedLegacy was called "COMPLETE" incorrectly.
CompletedLegacy,
/// Failed - replication failed.
Failed,
/// Replica - this is a replica.
Replica,
#[default]
Empty,
}
impl ReplicationStatusType {
/// Returns string representation of status
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 {
/// Returns string representation of version purge status
pub fn as_str(&self) -> &'static str {
match self {
VersionPurgeStatusType::Pending => "PENDING",
VersionPurgeStatusType::Complete => "COMPLETE",
VersionPurgeStatusType::Failed => "FAILED",
VersionPurgeStatusType::Empty => "",
}
}
/// Returns true if the version is pending purge.
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,
}
}
}
/// Type - replication type enum
#[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,
}
}
}
/// ReplicationState represents internal replication state
#[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>,
}
impl ReplicationState {
pub fn new() -> Self {
Self::default()
}
/// Returns true if replication state is identical for version purge statuses and replication statuses
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
}
/// Returns overall replication status for the object version being replicated
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 {
if let (Some(replica_timestamp), Some(replication_timestamp)) =
(self.replica_timestamp, self.replication_timestamp)
{
if replica_timestamp > replication_timestamp {
return self.replica_status.clone();
}
}
}
return repl_status;
}
}
} else if self.replica_status != ReplicationStatusType::default() {
return self.replica_status.clone();
}
ReplicationStatusType::default()
}
/// Returns overall replication purge status for the permanent delete being replicated
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),
}
}
/// Returns replicatedInfos struct initialized with the previous state of replication
pub fn target_state(&self, arn: &str) -> ReplicatedTargetInfo {
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(),
resync_timestamp: self.reset_statuses_map.get(arn).cloned().unwrap_or_default(),
..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 {
/// Replicate all data
All,
/// Replicate only metadata
Metadata,
/// Do not replicate
#[default]
None,
}
impl ReplicationAction {
/// Returns string representation of replication action
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,
}
}
}
/// ReplicatedTargetInfo struct represents replication info on a target
#[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>,
}
impl ReplicatedTargetInfo {
/// Returns true for a target if arn is empty
pub fn is_empty(&self) -> bool {
self.arn.is_empty()
}
}
/// ReplicatedInfos struct contains replication information for multiple targets
#[derive(Debug, Clone)]
pub struct ReplicatedInfos {
pub replication_timestamp: Option<OffsetDateTime>,
pub targets: Vec<ReplicatedTargetInfo>,
}
impl ReplicatedInfos {
/// Returns the total size of completed replications
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
}
/// Returns true if replication was attempted on any of the targets for the object version queued
pub fn replication_resynced(&self) -> bool {
for target in &self.targets {
if target.is_empty() || !target.replication_resynced {
continue;
}
return true;
}
false
}
/// Returns internal representation of replication status for all targets
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) }
}
/// Returns overall replication status across all targets
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
}
}
/// Returns overall version purge status across all targets
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
}
}
/// Returns internal representation of version purge status for all targets
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) }
}
/// Returns replication action based on target that actually performed replication
pub fn action(&self) -> ReplicationAction {
for target in &self.targets {
if target.is_empty() {
continue;
}
// rely on replication action from target that actually performed replication now.
if target.prev_replication_status != ReplicationStatusType::Completed {
return target.replication_action;
}
}
ReplicationAction::None
}
}
+2 -2
View File
@@ -67,7 +67,7 @@ pub fn create_real_xlmeta() -> Result<Vec<u8>> {
let delete_marker = MetaDeleteMarker {
version_id: Some(delete_version_id),
mod_time: Some(OffsetDateTime::from_unix_timestamp(1705312260)?), // 1 minute later
meta_sys: None,
meta_sys: HashMap::new(),
};
let delete_file_version = FileMetaVersion {
@@ -151,7 +151,7 @@ pub fn create_complex_xlmeta() -> Result<Vec<u8>> {
let delete_marker = MetaDeleteMarker {
version_id: Some(delete_version_id),
mod_time: Some(OffsetDateTime::from_unix_timestamp(1705312200 + i * 60 + 30)?),
meta_sys: None,
meta_sys: HashMap::new(),
};
let delete_file_version = FileMetaVersion {