refactor(app): hide replication object DTOs (#4147)

This commit is contained in:
Zhengchao An
2026-07-01 23:02:00 +08:00
committed by GitHub
parent a305160087
commit b43655ccdd
5 changed files with 112 additions and 124 deletions
@@ -123,6 +123,9 @@ Current coupling:
- admin replication extension target filtering and resync request construction - admin replication extension target filtering and resync request construction
stay behind the admin storage boundary instead of exposing replication work stay behind the admin storage boundary instead of exposing replication work
DTO construction to handlers; DTO construction to handlers;
- app object and multipart writes call object-replication boundary helpers
instead of constructing replication work DTOs or choosing object replication
operation types at the use-case layer;
- global replication pool/stat initialization still lives with ECStore runtime - global replication pool/stat initialization still lives with ECStore runtime
compatibility state; compatibility state;
- modules inside `bucket/replication` use local relative paths rather than the - modules inside `bucket/replication` use local relative paths rather than the
+8 -19
View File
@@ -21,7 +21,7 @@ use super::storage_api::multipart_usecase::bucket::{
lifecycle::{bucket_lifecycle_audit::LcEventSrc, bucket_lifecycle_ops::enqueue_transition_immediate}, lifecycle::{bucket_lifecycle_audit::LcEventSrc, bucket_lifecycle_ops::enqueue_transition_immediate},
metadata_sys, metadata_sys,
quota::QuotaOperation, quota::QuotaOperation,
replication::{get_must_replicate_options, must_replicate, schedule_replication}, replication::{must_replicate_object, schedule_object_replication},
versioning_sys::BucketVersioningSys, versioning_sys::BucketVersioningSys,
}; };
use super::storage_api::multipart_usecase::compression::is_disk_compressible; use super::storage_api::multipart_usecase::compression::is_disk_compressible;
@@ -63,7 +63,6 @@ use crate::table_catalog;
use bytes::Bytes; use bytes::Bytes;
use futures::StreamExt; use futures::StreamExt;
use http::{HeaderMap, Uri}; use http::{HeaderMap, Uri};
use rustfs_filemeta::ReplicationType;
use rustfs_s3_ops::S3Operation; use rustfs_s3_ops::S3Operation;
use rustfs_targets::EventName; use rustfs_targets::EventName;
use rustfs_utils::CompressionAlgorithm; use rustfs_utils::CompressionAlgorithm;
@@ -560,18 +559,13 @@ impl DefaultMultipartUsecase {
..Default::default() ..Default::default()
}; };
let mt2 = obj_info.user_defined.clone(); let mt2 = obj_info.user_defined.clone();
let replicate_options = get_must_replicate_options( let dsc =
&mt2, must_replicate_object(&bucket, &key, &mt2, "".to_string(), opts.delete_marker_replication_status(), opts.clone())
"".to_string(), .await;
opts.delete_marker_replication_status(),
ReplicationType::Object,
opts.clone(),
);
let dsc = must_replicate(&bucket, &key, replicate_options).await;
if dsc.replicate_any() { if dsc.replicate_any() {
warn!("need multipart replication"); warn!("need multipart replication");
schedule_replication(obj_info.clone(), store, dsc, ReplicationType::Object).await; schedule_object_replication(obj_info.clone(), store, dsc).await;
} }
// Set object info for event notification // Set object info for event notification
@@ -699,14 +693,9 @@ impl DefaultMultipartUsecase {
.await .await
.map_err(ApiError::from)?; .map_err(ApiError::from)?;
let replicate_options = get_must_replicate_options( let dsc =
&mt2, must_replicate_object(&bucket, &key, &mt2, "".to_string(), opts.delete_marker_replication_status(), opts.clone())
"".to_string(), .await;
opts.delete_marker_replication_status(),
ReplicationType::Object,
opts.clone(),
);
let dsc = must_replicate(&bucket, &key, replicate_options).await;
if dsc.replicate_any() { if dsc.replicate_any() {
insert_str(&mut opts.user_defined, SUFFIX_REPLICATION_TIMESTAMP, jiff::Zoned::now().to_string()); insert_str(&mut opts.user_defined, SUFFIX_REPLICATION_TIMESTAMP, jiff::Zoned::now().to_string());
insert_str( insert_str(
+14 -72
View File
@@ -23,7 +23,7 @@ use super::storage_api::object_usecase::access::{
}; };
use super::storage_api::object_usecase::bucket::quota::checker::QuotaChecker; use super::storage_api::object_usecase::bucket::quota::checker::QuotaChecker;
use super::storage_api::object_usecase::bucket::{ use super::storage_api::object_usecase::bucket::{
ReplicationConfigExt as _, VersioningConfigExt as _, VersioningConfigExt as _,
lifecycle::{ lifecycle::{
bucket_lifecycle_audit::LcEventSrc, bucket_lifecycle_audit::LcEventSrc,
bucket_lifecycle_ops::{enqueue_transition_immediate, post_restore_opts}, bucket_lifecycle_ops::{enqueue_transition_immediate, post_restore_opts},
@@ -38,8 +38,8 @@ use super::storage_api::object_usecase::bucket::{
predict_lifecycle_expiration, predict_lifecycle_expiration,
quota::QuotaOperation, quota::QuotaOperation,
replication::{ replication::{
DeletedObjectReplicationInfo, ObjectOpts as ReplicationObjectOpts, check_replicate_delete, get_must_replicate_options, DeletedObjectReplicationInfo, check_replicate_delete, delete_replication_state_from_config, must_replicate_object,
must_replicate, schedule_replication, schedule_replication_delete, schedule_object_replication, schedule_replication_delete,
}, },
tagging::decode_tags, tagging::decode_tags,
validate_restore_request, validate_restore_request,
@@ -109,10 +109,10 @@ use pin_project_lite::pin_project;
use rustfs_concurrency::GetObjectQueueSnapshot; use rustfs_concurrency::GetObjectQueueSnapshot;
use rustfs_config::MI_B; use rustfs_config::MI_B;
use rustfs_filemeta::{ use rustfs_filemeta::{
REPLICATE_INCOMING_DELETE, ReplicateDecision, ReplicateTargetDecision, ReplicationState, ReplicationStatusType, REPLICATE_INCOMING_DELETE, ReplicationStatusType, RestoreStatusOps, VersionPurgeStatusType, parse_restore_obj_status,
ReplicationType, RestoreStatusOps, VersionPurgeStatusType, parse_restore_obj_status, replication_statuses_map,
version_purge_statuses_map,
}; };
#[cfg(test)]
use rustfs_filemeta::{ReplicationState, replication_statuses_map};
use rustfs_io_core::{BytesPool, PooledBuffer}; use rustfs_io_core::{BytesPool, PooledBuffer};
use rustfs_io_metrics; use rustfs_io_metrics;
use rustfs_lock::NamespaceLockGuard; use rustfs_lock::NamespaceLockGuard;
@@ -147,9 +147,9 @@ use s3s::dto::{
DeleteObjectsOutput, DeletedObject, ETag, GetObjectAttributesInput, GetObjectAttributesOutput, GetObjectAttributesParts, DeleteObjectsOutput, DeletedObject, ETag, GetObjectAttributesInput, GetObjectAttributesOutput, GetObjectAttributesParts,
GetObjectInput, GetObjectOutput, HeadObjectInput, HeadObjectOutput, MetadataDirective, ObjectAttributes, ObjectLockLegalHold, GetObjectInput, GetObjectOutput, HeadObjectInput, HeadObjectOutput, MetadataDirective, ObjectAttributes, ObjectLockLegalHold,
ObjectLockLegalHoldStatus, ObjectLockMode, ObjectLockRetention, ObjectLockRetentionMode, ObjectPart, PutObjectInput, ObjectLockLegalHoldStatus, ObjectLockMode, ObjectLockRetention, ObjectLockRetentionMode, ObjectPart, PutObjectInput,
PutObjectOutput, Range, ReplicationConfiguration, RequestCharged, RestoreObjectInput, RestoreObjectOutput, RestoreStatus, PutObjectOutput, Range, RequestCharged, RestoreObjectInput, RestoreObjectOutput, RestoreStatus, SSECustomerAlgorithm,
SSECustomerAlgorithm, SSECustomerKeyMD5, SSEKMSKeyId, SelectObjectContentInput, SelectObjectContentOutput, SSECustomerKeyMD5, SSEKMSKeyId, SelectObjectContentInput, SelectObjectContentOutput, ServerSideEncryption, StorageClass,
ServerSideEncryption, StorageClass, StreamingBlob, TaggingHeader, Timestamp, TimestampFormat, WebsiteRedirectLocation, StreamingBlob, TaggingHeader, Timestamp, TimestampFormat, WebsiteRedirectLocation,
}; };
use s3s::header::{X_AMZ_RESTORE, X_AMZ_RESTORE_OUTPUT_PATH}; use s3s::header::{X_AMZ_RESTORE, X_AMZ_RESTORE_OUTPUT_PATH};
use s3s::stream::{ByteStream, RemainingLength}; use s3s::stream::{ByteStream, RemainingLength};
@@ -1403,51 +1403,6 @@ fn build_put_object_expiration_header(event: &lifecycle::Event) -> Option<String
Some(format!("expiry-date=\"{}\", rule-id=\"{}\"", expiry_date, event.rule_id)) Some(format!("expiry-date=\"{}\", rule-id=\"{}\"", expiry_date, event.rule_id))
} }
fn delete_replication_state_from_config(
config: &ReplicationConfiguration,
obj_info: &ObjectInfo,
version_id: Option<Uuid>,
replica: bool,
) -> Option<ReplicationState> {
let opts = ReplicationObjectOpts {
name: obj_info.name.clone(),
user_tags: (*obj_info.user_tags).clone(),
version_id,
delete_marker: obj_info.delete_marker,
op_type: ReplicationType::Delete,
replica,
..Default::default()
};
let target_arns = config.filter_target_arns(&opts);
if target_arns.is_empty() {
return None;
}
let mut decision = ReplicateDecision::new();
for target_arn in target_arns {
let mut target_opts = opts.clone();
target_opts.target_arn = target_arn.clone();
decision.set(ReplicateTargetDecision::new(target_arn, config.replicate(&target_opts), false));
}
if !decision.replicate_any() {
return None;
}
let pending_status = decision.pending_status();
let mut state = ReplicationState {
replicate_decision_str: decision.to_string(),
..Default::default()
};
if version_id.is_some() {
state.version_purge_status_internal = pending_status.clone();
state.purge_targets = version_purge_statuses_map(pending_status.as_deref().unwrap_or_default());
} else {
state.replication_status_internal = pending_status.clone();
state.targets = replication_statuses_map(pending_status.as_deref().unwrap_or_default());
}
Some(state)
}
async fn enrich_delete_replication_state_if_needed( async fn enrich_delete_replication_state_if_needed(
bucket: &str, bucket: &str,
delete_object: &mut StorageDeletedObject, delete_object: &mut StorageDeletedObject,
@@ -3168,14 +3123,9 @@ impl DefaultObjectUsecase {
.map(|ctx| ctx.request_id.clone()) .map(|ctx| ctx.request_id.clone())
.unwrap_or_else(|| request_context::RequestContext::fallback().request_id); .unwrap_or_else(|| request_context::RequestContext::fallback().request_id);
let repoptions = get_must_replicate_options( let dsc =
&mt2, must_replicate_object(&bucket, &key, &mt2, "".to_string(), opts.delete_marker_replication_status(), opts.clone())
"".to_string(), .await;
opts.delete_marker_replication_status(),
ReplicationType::Object,
opts.clone(),
);
let dsc = must_replicate(&bucket, &key, repoptions).await;
if dsc.replicate_any() { if dsc.replicate_any() {
insert_str(&mut opts.user_defined, SUFFIX_REPLICATION_TIMESTAMP, jiff::Zoned::now().to_string()); insert_str(&mut opts.user_defined, SUFFIX_REPLICATION_TIMESTAMP, jiff::Zoned::now().to_string());
@@ -3284,19 +3234,11 @@ impl DefaultObjectUsecase {
let e_tag = obj_info.etag.clone().map(|etag| to_s3s_etag(&etag)); let e_tag = obj_info.etag.clone().map(|etag| to_s3s_etag(&etag));
let repoptions = get_must_replicate_options( let dsc = must_replicate_object(&bucket, &key, &mt2, "".to_string(), opts.delete_marker_replication_status(), opts).await;
&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; let expiration = resolve_put_object_expiration(&bucket, &obj_info).await;
if dsc.replicate_any() { if dsc.replicate_any() {
schedule_replication(obj_info.clone(), store, dsc, ReplicationType::Object).await; schedule_object_replication(obj_info.clone(), store, dsc).await;
} }
let mut checksums = PutObjectChecksums { let mut checksums = PutObjectChecksums {
+68 -33
View File
@@ -237,25 +237,6 @@ pub(crate) mod bucket {
} }
} }
pub(crate) trait ReplicationConfigExt {
fn filter_target_arns(&self, obj: &replication::ObjectOpts) -> Vec<String>;
fn replicate(&self, opts: &replication::ObjectOpts) -> bool;
}
impl ReplicationConfigExt for s3s::dto::ReplicationConfiguration {
fn filter_target_arns(&self, obj: &replication::ObjectOpts) -> Vec<String> {
<s3s::dto::ReplicationConfiguration as crate::storage::storage_api::ecstore_bucket::replication::ReplicationConfigurationExt>::filter_target_arns(
self, obj,
)
}
fn replicate(&self, opts: &replication::ObjectOpts) -> bool {
<s3s::dto::ReplicationConfiguration as crate::storage::storage_api::ecstore_bucket::replication::ReplicationConfigurationExt>::replicate(
self, opts,
)
}
}
pub(crate) trait VersioningConfigExt { pub(crate) trait VersioningConfigExt {
fn enabled(&self) -> bool; fn enabled(&self) -> bool;
fn prefix_enabled(&self, prefix: &str) -> bool; fn prefix_enabled(&self, prefix: &str) -> bool;
@@ -625,13 +606,12 @@ pub(crate) mod bucket {
pub(crate) mod replication { pub(crate) mod replication {
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use uuid::Uuid;
pub(crate) type DeletedObjectReplicationInfo = pub(crate) type DeletedObjectReplicationInfo =
crate::storage::storage_api::ecstore_bucket::replication::DeletedObjectReplicationInfo; crate::storage::storage_api::ecstore_bucket::replication::DeletedObjectReplicationInfo;
pub(crate) type MustReplicateOptions = crate::storage::storage_api::ecstore_bucket::replication::MustReplicateOptions; type ObjectOpts = crate::storage::storage_api::ecstore_bucket::replication::ObjectOpts;
pub(crate) type ObjectOpts = crate::storage::storage_api::ecstore_bucket::replication::ObjectOpts; type ReplicationObjectBridge = crate::storage::storage_api::ecstore_bucket::replication::ReplicationObjectBridge;
pub(crate) type ReplicationObjectBridge =
crate::storage::storage_api::ecstore_bucket::replication::ReplicationObjectBridge;
pub(crate) type ReplicateDecision = rustfs_filemeta::ReplicateDecision; pub(crate) type ReplicateDecision = rustfs_filemeta::ReplicateDecision;
pub(crate) async fn check_replicate_delete( pub(crate) async fn check_replicate_delete(
@@ -644,32 +624,87 @@ pub(crate) mod bucket {
ReplicationObjectBridge::check_delete(bucket, dobj, oi, del_opts, gerr).await ReplicationObjectBridge::check_delete(bucket, dobj, oi, del_opts, gerr).await
} }
pub(crate) fn get_must_replicate_options( pub(crate) async fn must_replicate_object(
bucket: &str,
object: &str,
user_defined: &HashMap<String, String>, user_defined: &HashMap<String, String>,
user_tags: String, user_tags: String,
status: rustfs_filemeta::ReplicationStatusType, status: rustfs_filemeta::ReplicationStatusType,
op_type: rustfs_filemeta::ReplicationType,
opts: crate::storage::storage_api::StorageObjectOptions, opts: crate::storage::storage_api::StorageObjectOptions,
) -> MustReplicateOptions { ) -> ReplicateDecision {
ReplicationObjectBridge::must_replicate_options(user_defined, user_tags, status, op_type, opts) let mopts = ReplicationObjectBridge::must_replicate_options(
} user_defined,
user_tags,
pub(crate) async fn must_replicate(bucket: &str, object: &str, mopts: MustReplicateOptions) -> ReplicateDecision { status,
rustfs_filemeta::ReplicationType::Object,
opts,
);
ReplicationObjectBridge::must_replicate(bucket, object, mopts).await ReplicationObjectBridge::must_replicate(bucket, object, mopts).await
} }
pub(crate) async fn schedule_replication( pub(crate) async fn schedule_object_replication(
oi: crate::storage::storage_api::StorageObjectInfo, oi: crate::storage::storage_api::StorageObjectInfo,
store: Arc<crate::storage::storage_api::ECStore>, store: Arc<crate::storage::storage_api::ECStore>,
dsc: ReplicateDecision, dsc: ReplicateDecision,
op_type: rustfs_filemeta::ReplicationType,
) { ) {
ReplicationObjectBridge::schedule_object(oi, store, dsc, op_type).await; ReplicationObjectBridge::schedule_object(oi, store, dsc, rustfs_filemeta::ReplicationType::Object).await;
} }
pub(crate) async fn schedule_replication_delete(dv: DeletedObjectReplicationInfo) { pub(crate) async fn schedule_replication_delete(dv: DeletedObjectReplicationInfo) {
ReplicationObjectBridge::schedule_delete(dv).await; ReplicationObjectBridge::schedule_delete(dv).await;
} }
pub(crate) fn delete_replication_state_from_config(
config: &s3s::dto::ReplicationConfiguration,
obj_info: &crate::storage::storage_api::StorageObjectInfo,
version_id: Option<Uuid>,
replica: bool,
) -> Option<rustfs_filemeta::ReplicationState> {
let opts = ObjectOpts {
name: obj_info.name.clone(),
user_tags: (*obj_info.user_tags).clone(),
version_id,
delete_marker: obj_info.delete_marker,
op_type: rustfs_filemeta::ReplicationType::Delete,
replica,
..Default::default()
};
let target_arns =
crate::storage::storage_api::ecstore_bucket::replication::ReplicationConfigurationExt::filter_target_arns(
config, &opts,
);
if target_arns.is_empty() {
return None;
}
let mut decision = ReplicateDecision::new();
for target_arn in target_arns {
let mut target_opts = opts.clone();
target_opts.target_arn = target_arn.clone();
let replicate = crate::storage::storage_api::ecstore_bucket::replication::ReplicationConfigurationExt::replicate(
config,
&target_opts,
);
decision.set(rustfs_filemeta::ReplicateTargetDecision::new(target_arn, replicate, false));
}
if !decision.replicate_any() {
return None;
}
let pending_status = decision.pending_status();
let mut state = rustfs_filemeta::ReplicationState {
replicate_decision_str: decision.to_string(),
..Default::default()
};
if version_id.is_some() {
state.version_purge_status_internal = pending_status.clone();
state.purge_targets = rustfs_filemeta::version_purge_statuses_map(pending_status.as_deref().unwrap_or_default());
} else {
state.replication_status_internal = pending_status.clone();
state.targets = rustfs_filemeta::replication_statuses_map(pending_status.as_deref().unwrap_or_default());
}
Some(state)
}
} }
pub(crate) mod tagging { pub(crate) mod tagging {
@@ -200,6 +200,7 @@ EXTERNAL_ECSTORE_API_BOUNDARY_HITS_FILE="${TMP_DIR}/external_ecstore_api_boundar
REPLICATION_FACADE_BYPASS_HITS_FILE="${TMP_DIR}/replication_facade_bypass_hits.txt" REPLICATION_FACADE_BYPASS_HITS_FILE="${TMP_DIR}/replication_facade_bypass_hits.txt"
REPLICATION_FACADE_WILDCARD_EXPORT_HITS_FILE="${TMP_DIR}/replication_facade_wildcard_export_hits.txt" REPLICATION_FACADE_WILDCARD_EXPORT_HITS_FILE="${TMP_DIR}/replication_facade_wildcard_export_hits.txt"
ADMIN_REPLICATION_DTO_BOUNDARY_BYPASS_HITS_FILE="${TMP_DIR}/admin_replication_dto_boundary_bypass_hits.txt" ADMIN_REPLICATION_DTO_BOUNDARY_BYPASS_HITS_FILE="${TMP_DIR}/admin_replication_dto_boundary_bypass_hits.txt"
APP_REPLICATION_DTO_BOUNDARY_BYPASS_HITS_FILE="${TMP_DIR}/app_replication_dto_boundary_bypass_hits.txt"
REPLICATION_BANDWIDTH_BOUNDARY_BYPASS_HITS_FILE="${TMP_DIR}/replication_bandwidth_boundary_bypass_hits.txt" REPLICATION_BANDWIDTH_BOUNDARY_BYPASS_HITS_FILE="${TMP_DIR}/replication_bandwidth_boundary_bypass_hits.txt"
REPLICATION_CONFIG_STORE_BYPASS_HITS_FILE="${TMP_DIR}/replication_config_store_bypass_hits.txt" REPLICATION_CONFIG_STORE_BYPASS_HITS_FILE="${TMP_DIR}/replication_config_store_bypass_hits.txt"
REPLICATION_ERROR_BOUNDARY_BYPASS_HITS_FILE="${TMP_DIR}/replication_error_boundary_bypass_hits.txt" REPLICATION_ERROR_BOUNDARY_BYPASS_HITS_FILE="${TMP_DIR}/replication_error_boundary_bypass_hits.txt"
@@ -2555,6 +2556,24 @@ if [[ -s "$REPLICATION_OBJECT_BRIDGE_BYPASS_HITS_FILE" ]]; then
report_failure "object replication work entry points must stay behind ReplicationObjectBridge: $(paste -sd '; ' "$REPLICATION_OBJECT_BRIDGE_BYPASS_HITS_FILE")" report_failure "object replication work entry points must stay behind ReplicationObjectBridge: $(paste -sd '; ' "$REPLICATION_OBJECT_BRIDGE_BYPASS_HITS_FILE")"
fi fi
(
cd "$ROOT_DIR"
{
rg -n --with-filename '\b(get_must_replicate_options|must_replicate|schedule_replication|MustReplicateOptions|ReplicationObjectBridge|ReplicationObjectOpts)\b|ObjectOpts as ReplicationObjectOpts|ReplicationType::Object' \
rustfs/src/app \
--glob '*.rs' \
--glob '!rustfs/src/app/storage_api.rs' || true
rg -n --with-filename '(?:ecstore_bucket::replication|crate::storage::storage_api::ecstore_bucket::replication)::(?:ObjectOpts|MustReplicateOptions|ReplicationObjectBridge|ReplicationConfigurationExt)' \
rustfs/src/app \
--glob '*.rs' \
--glob '!rustfs/src/app/storage_api.rs' || true
}
) >"$APP_REPLICATION_DTO_BOUNDARY_BYPASS_HITS_FILE"
if [[ -s "$APP_REPLICATION_DTO_BOUNDARY_BYPASS_HITS_FILE" ]]; then
report_failure "app replication ObjectOpts/MustReplicateOptions/bridge access must stay behind rustfs/src/app/storage_api.rs: $(paste -sd '; ' "$APP_REPLICATION_DTO_BOUNDARY_BYPASS_HITS_FILE")"
fi
( (
cd "$ROOT_DIR" cd "$ROOT_DIR"
{ {