fix(site-replication): site replication flag issue and resolved the replication storm (#4120)

* fix(site-replication): Add Docker Compose setup for site replication testing

- Fixed the site replication flag issue and resolved the replication storm.
- Introduced a new directory for site replication tests with Docker Compose.
- Created `docker-compose.yml` to define three RustFS sites and a setup container.
- Added `README.md` to document the purpose, usage, and test flow of the replication setup.
- Implemented `run-object-flow-check.sh` script to verify object replication across sites.
- Configured health checks and volume permissions for the RustFS containers.
- Enabled customization of access keys, bucket names, and other parameters via environment variables.

* fix

* fix
This commit is contained in:
唐小鸭
2026-06-30 21:52:06 +08:00
committed by GitHub
parent 5e68455eed
commit 4efefd67b5
14 changed files with 728 additions and 20 deletions
+15 -5
View File
@@ -63,7 +63,7 @@ use crate::table_catalog;
use bytes::Bytes;
use futures::StreamExt;
use http::{HeaderMap, Uri};
use rustfs_filemeta::{ReplicationStatusType, ReplicationType};
use rustfs_filemeta::ReplicationType;
use rustfs_s3_ops::S3Operation;
use rustfs_targets::EventName;
use rustfs_utils::CompressionAlgorithm;
@@ -560,8 +560,13 @@ impl DefaultMultipartUsecase {
..Default::default()
};
let mt2 = obj_info.user_defined.clone();
let replicate_options =
get_must_replicate_options(&mt2, "".to_string(), ReplicationStatusType::Empty, ReplicationType::Object, opts.clone());
let replicate_options = get_must_replicate_options(
&mt2,
"".to_string(),
opts.delete_marker_replication_status(),
ReplicationType::Object,
opts.clone(),
);
let dsc = must_replicate(&bucket, &key, replicate_options).await;
if dsc.replicate_any() {
@@ -694,8 +699,13 @@ impl DefaultMultipartUsecase {
.await
.map_err(ApiError::from)?;
let replicate_options =
get_must_replicate_options(&mt2, "".to_string(), ReplicationStatusType::Empty, ReplicationType::Object, opts.clone());
let replicate_options = get_must_replicate_options(
&mt2,
"".to_string(),
opts.delete_marker_replication_status(),
ReplicationType::Object,
opts.clone(),
);
let dsc = must_replicate(&bucket, &key, replicate_options).await;
if dsc.replicate_any() {
insert_str(&mut opts.user_defined, SUFFIX_REPLICATION_TIMESTAMP, jiff::Zoned::now().to_string());
+14 -4
View File
@@ -2994,8 +2994,13 @@ impl DefaultObjectUsecase {
.map(|ctx| ctx.request_id.clone())
.unwrap_or_else(|| request_context::RequestContext::fallback().request_id);
let repoptions =
get_must_replicate_options(&mt2, "".to_string(), ReplicationStatusType::Empty, ReplicationType::Object, opts.clone());
let repoptions = get_must_replicate_options(
&mt2,
"".to_string(),
opts.delete_marker_replication_status(),
ReplicationType::Object,
opts.clone(),
);
let dsc = must_replicate(&bucket, &key, repoptions).await;
if dsc.replicate_any() {
@@ -3105,8 +3110,13 @@ impl DefaultObjectUsecase {
let e_tag = obj_info.etag.clone().map(|etag| to_s3s_etag(&etag));
let repoptions =
get_must_replicate_options(&mt2, "".to_string(), ReplicationStatusType::Empty, ReplicationType::Object, opts);
let repoptions = get_must_replicate_options(
&mt2,
"".to_string(),
opts.delete_marker_replication_status(),
ReplicationType::Object,
opts,
);
let dsc = must_replicate(&bucket, &key, repoptions).await;
let expiration = resolve_put_object_expiration(&bucket, &obj_info).await;
+50 -9
View File
@@ -16,15 +16,16 @@ use super::{BucketVersioningSys, Result, StorageError};
use crate::storage::storage_api::options_consumer::contract::{object::HTTPPreconditions, range::HTTPRangeSpec};
use http::header::{IF_MATCH, IF_NONE_MATCH};
use http::{HeaderMap, HeaderValue};
use rustfs_filemeta::ReplicationStatusType;
use rustfs_utils::http::{
AMZ_BUCKET_REPLICATION_STATUS, SUFFIX_FORCE_DELETE, SUFFIX_REPLICATION_ACTUAL_OBJECT_SIZE, SUFFIX_REPLICATION_SSEC_CRC,
SUFFIX_SOURCE_DELETEMARKER, SUFFIX_SOURCE_MTIME, SUFFIX_SOURCE_REPLICATION_REQUEST, SUFFIX_SOURCE_VERSION_ID, get_header,
insert_header_map, is_encryption_metadata_key, is_internal_key,
};
use rustfs_utils::http::{
AMZ_META_UNENCRYPTED_CONTENT_LENGTH, AMZ_META_UNENCRYPTED_CONTENT_MD5, AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER,
AMZ_OBJECT_LOCK_MODE_LOWER, AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER,
};
use rustfs_utils::http::{
SUFFIX_FORCE_DELETE, SUFFIX_REPLICATION_ACTUAL_OBJECT_SIZE, SUFFIX_REPLICATION_SSEC_CRC, SUFFIX_SOURCE_DELETEMARKER,
SUFFIX_SOURCE_MTIME, SUFFIX_SOURCE_REPLICATION_REQUEST, SUFFIX_SOURCE_VERSION_ID, get_header, insert_header_map,
is_encryption_metadata_key, is_internal_key,
};
use s3s::header::X_AMZ_OBJECT_LOCK_MODE;
use s3s::header::X_AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE;
@@ -275,6 +276,7 @@ pub fn get_complete_multipart_upload_opts(headers: &HeaderMap<HeaderValue>) -> s
replication_request,
..Default::default()
};
apply_replica_status_from_headers(headers, &mut opts);
fill_conditional_writes_opts_from_header(headers, &mut opts)?;
Ok(opts)
@@ -297,6 +299,7 @@ pub fn copy_src_opts(_bucket: &str, _object: &str, headers: &HeaderMap<HeaderVal
pub fn put_opts_from_headers(headers: &HeaderMap<HeaderValue>, metadata: HashMap<String, String>) -> Result<ObjectOptions> {
let mut opts = get_default_opts(headers, metadata, false)?;
apply_replica_status_from_headers(headers, &mut opts);
if get_header(headers, SUFFIX_SOURCE_REPLICATION_REQUEST).as_deref() == Some("true") {
opts.replication_request = true;
if let Some(v) = get_header(headers, SUFFIX_SOURCE_MTIME) {
@@ -313,6 +316,16 @@ pub fn put_opts_from_headers(headers: &HeaderMap<HeaderValue>, metadata: HashMap
Ok(opts)
}
fn apply_replica_status_from_headers(headers: &HeaderMap<HeaderValue>, opts: &mut ObjectOptions) {
if headers
.get(AMZ_BUCKET_REPLICATION_STATUS)
.and_then(|value| value.to_str().ok())
.is_some_and(|status| status.eq_ignore_ascii_case(ReplicationStatusType::Replica.as_str()))
{
opts.set_replica_status(ReplicationStatusType::Replica);
}
}
/// Creates default options for getting an object from a bucket.
pub fn get_default_opts(
_headers: &HeaderMap<HeaderValue>,
@@ -788,13 +801,15 @@ mod tests {
use super::{
ENV_REJECT_ARCHIVE_CONTENT_ENCODING, SUPPORTED_HEADERS, copy_dst_opts, copy_src_opts, del_opts,
detect_content_type_from_object_name, extract_metadata, extract_metadata_from_mime,
extract_metadata_from_mime_with_object_name, filter_object_metadata, get_default_opts, get_opts, parse_copy_source_range,
put_opts, put_opts_from_headers, validate_archive_content_encoding,
extract_metadata_from_mime_with_object_name, filter_object_metadata, get_complete_multipart_upload_opts,
get_default_opts, get_opts, parse_copy_source_range, put_opts, put_opts_from_headers, validate_archive_content_encoding,
};
use http::{HeaderMap, HeaderValue};
use rustfs_filemeta::ReplicationStatusType;
use rustfs_utils::http::{
AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER, AMZ_OBJECT_LOCK_MODE_LOWER, AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER,
SUFFIX_FORCE_DELETE, SUFFIX_SOURCE_MTIME, SUFFIX_SOURCE_REPLICATION_REQUEST, insert_header,
AMZ_BUCKET_REPLICATION_STATUS, AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER, AMZ_OBJECT_LOCK_MODE_LOWER,
AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER, SUFFIX_FORCE_DELETE, SUFFIX_SOURCE_MTIME, SUFFIX_SOURCE_REPLICATION_REQUEST,
insert_header,
};
use s3s::S3ErrorCode;
use std::collections::HashMap;
@@ -1126,6 +1141,32 @@ mod tests {
assert!(opts_invalid.mod_time.is_none());
}
#[test]
fn test_put_opts_from_headers_with_replica_status() {
let mut headers = HeaderMap::new();
headers.insert(
AMZ_BUCKET_REPLICATION_STATUS,
HeaderValue::from_static(ReplicationStatusType::Replica.as_str()),
);
let opts = put_opts_from_headers(&headers, HashMap::new()).expect("replica status header should parse");
assert_eq!(opts.delete_marker_replication_status(), ReplicationStatusType::Replica);
}
#[test]
fn test_complete_multipart_opts_with_replica_status() {
let mut headers = HeaderMap::new();
headers.insert(
AMZ_BUCKET_REPLICATION_STATUS,
HeaderValue::from_static(ReplicationStatusType::Replica.as_str()),
);
let opts = get_complete_multipart_upload_opts(&headers).expect("replica status header should parse");
assert_eq!(opts.delete_marker_replication_status(), ReplicationStatusType::Replica);
}
#[test]
fn test_get_default_opts_with_metadata() {
let headers = create_test_headers();