From 7001e535469bee5ea17da6034187bfc5b1cf52f5 Mon Sep 17 00:00:00 2001 From: Zhengchao An Date: Fri, 3 Jul 2026 18:48:26 +0800 Subject: [PATCH] refactor(replication): isolate stats and app contract boundaries (#4235) * refactor(replication): isolate stats boundary adapters * refactor(replication): route app contracts through storage boundary --- .../ecstore/src/bucket/replication/README.md | 4 +- crates/ecstore/src/bucket/replication/mod.rs | 3 +- .../bucket/replication/replication_state.rs | 4 +- .../replication/replication_stats_boundary.rs | 19 +++++ rustfs/src/app/bucket_usecase.rs | 18 +++-- rustfs/src/app/object_usecase.rs | 40 ++-------- rustfs/src/app/storage_api.rs | 74 ++++++++++++++++++- scripts/check_architecture_migration_rules.sh | 39 ++++++++++ 8 files changed, 156 insertions(+), 45 deletions(-) create mode 100644 crates/ecstore/src/bucket/replication/replication_stats_boundary.rs diff --git a/crates/ecstore/src/bucket/replication/README.md b/crates/ecstore/src/bucket/replication/README.md index a0ed7f736..c7c04f0ad 100644 --- a/crates/ecstore/src/bucket/replication/README.md +++ b/crates/ecstore/src/bucket/replication/README.md @@ -17,7 +17,8 @@ paths. | `replication_queue_boundary.rs` | Queue/admission DTOs, heal queue DTOs, worker sizing, and backpressure helpers. | Keeps ECStore runtime modules from importing queue/backpressure contracts directly from `rustfs-replication`. | | `replication_resync_boundary.rs` | Resync DTOs, status classifiers, persisted resync/MRF codec wrappers, and ECStore error mapping. | Keeps ECStore runtime modules from importing resync contract helpers directly from `rustfs-replication`. | | `replication_resyncer.rs` | Object replication, delete replication, resync execution, target calls, and multipart target upload paths. | Depends on target calls and target config types through the replication target boundary, metadata paths and metadata systems through the replication metadata boundary, file metadata replication contracts through the filemeta boundary, object decisions and multipart planning through the object decision boundary, resync contracts through the resync boundary, queue DTOs through the queue boundary, error contracts through the error boundary, versioning systems, storage contracts through the replication storage boundary, config-derived storage class labels through the config store, runtime sources, notification events and local event host selection through the event sink, bandwidth reader wrapping, and SetDisks lock timing. | -| `replication_state.rs` | Replication queue/stat state and worker accounting. | Reads runtime sources, file metadata replication contracts, error contracts, and bucket monitor handles through local boundaries, and owns shared replication pool/stat state. | +| `replication_state.rs` | Replication queue/stat state and worker accounting. | Reads stats DTOs through the stats boundary, runtime sources, file metadata replication contracts, error contracts, and bucket monitor handles through local boundaries, and owns shared replication pool/stat state. | +| `replication_stats_boundary.rs` | Bucket replication stats DTOs, queue/proxy metric caches, and worker metric snapshots. | Keeps ECStore runtime modules from importing stats contracts directly from `rustfs-replication`. | | `replication_lifecycle_bridge.rs` | Lifecycle-originated delete replication admission and version-purge state construction. | Depends on replication config/rule matching, delete-replication decisions, and replication delete scheduling through a local contract type. | | `replication_migration_bridge.rs` | Bucket migration access to persisted replication resync codec helpers. | Keeps migration normalization behind a bridge instead of re-exporting resyncer codec helpers. | | `replication_object_bridge.rs` | App and SetDisks object replication decisions plus object/delete scheduling. | Keeps object write/delete replication call sites behind a bridge instead of exporting low-level resyncer and pool helpers. | @@ -38,6 +39,7 @@ paths. | `ReplicationFileMeta` | Replication status, decisions, MRF entries, resync decisions, and target reset helpers. | `rustfs_filemeta` replication contracts are concentrated in `replication_filemeta_boundary.rs`; `FileInfo` remains in the storage boundary for storage trait bindings and walk options. | | `ReplicationObjectDecisionContracts` | Object replication options, delete replication decisions, resync target projection, multipart planning, and delete-marker retry classifiers. | Owned by `crates/replication`; ECStore imports them through `replication_object_decision_boundary.rs`. | | `ReplicationQueueContracts` | Queue admission, heal queue results/actions, worker operations, worker sizing, and backpressure decisions. | Owned by `crates/replication`; ECStore imports them through `replication_queue_boundary.rs`. | +| `ReplicationStatsContracts` | Bucket stats, replication target stats, queue/proxy metrics, and worker metric snapshots. | Owned by `crates/replication`; ECStore imports them through `replication_stats_boundary.rs`. | | `ReplicationErrorBoundary` | ECStore error/result contracts and replication-specific error classifiers. | `crate::error` imports are concentrated in `replication_error_boundary.rs`. | | `ReplicationTargetStore` | Bucket target listing, target client lookup, target offline checks, target config types, target operation option types, and target HeadObject comparison adapters. | Bucket target sys access, `BucketTargets`, target operation types, and HeadObject-to-replication DTO adapters are exposed through the contract type in `replication_target_boundary.rs`. | | `ReplicationRuntime` | Worker pool, queue sizing, stats, bucket monitor, local node identity, cancellation, and admission state. | Direct runtime source/global access and shared replication pool/stat state; ECStore object store and bucket monitor implementation types stay behind local storage/bandwidth boundaries. | diff --git a/crates/ecstore/src/bucket/replication/mod.rs b/crates/ecstore/src/bucket/replication/mod.rs index 83e101d81..10b0cdec0 100644 --- a/crates/ecstore/src/bucket/replication/mod.rs +++ b/crates/ecstore/src/bucket/replication/mod.rs @@ -34,6 +34,7 @@ mod replication_resync_boundary; mod replication_resyncer; mod replication_scanner_bridge; mod replication_state; +mod replication_stats_boundary; mod replication_storage_boundary; mod replication_tagging_boundary; mod replication_target_boundary; @@ -59,6 +60,6 @@ pub use replication_queue_boundary::{ pub use replication_resync_boundary::{BucketReplicationResyncStatus, ResyncOpts, TargetReplicationResyncStatus}; pub use replication_scanner_bridge::ReplicationScannerBridge; pub use replication_state::ReplicationStats; +pub use replication_stats_boundary::BucketStats; pub use replication_storage_boundary::{ReplicationObjectIO, ReplicationStorage}; pub(crate) use replication_target_config_bridge::ReplicationTargetConfigBridge; -pub use rustfs_replication::BucketStats; diff --git a/crates/ecstore/src/bucket/replication/replication_state.rs b/crates/ecstore/src/bucket/replication/replication_state.rs index fbb912886..7799a6f4e 100644 --- a/crates/ecstore/src/bucket/replication/replication_state.rs +++ b/crates/ecstore/src/bucket/replication/replication_state.rs @@ -14,11 +14,11 @@ use super::replication_error_boundary::Error; use super::replication_filemeta_boundary::{ReplicatedTargetInfo, ReplicationStatusType, ReplicationType}; -use super::runtime_boundary as runtime_sources; -use rustfs_replication::{ +use super::replication_stats_boundary::{ ActiveWorkerStat, BucketReplicationStat, BucketReplicationStats, BucketStats, InQueueMetric, ProxyMetric, ProxyStatsCache, QueueCache, SRMetricsSummary, XferStats, }; +use super::runtime_boundary as runtime_sources; use std::collections::HashMap; use std::sync::Arc; use std::sync::atomic::{AtomicI64, Ordering}; diff --git a/crates/ecstore/src/bucket/replication/replication_stats_boundary.rs b/crates/ecstore/src/bucket/replication/replication_stats_boundary.rs new file mode 100644 index 000000000..4f12638f4 --- /dev/null +++ b/crates/ecstore/src/bucket/replication/replication_stats_boundary.rs @@ -0,0 +1,19 @@ +// Copyright 2024 RustFS Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub use rustfs_replication::BucketStats; +pub(crate) use rustfs_replication::{ + ActiveWorkerStat, BucketReplicationStat, BucketReplicationStats, InQueueMetric, ProxyMetric, ProxyStatsCache, QueueCache, + SRMetricsSummary, XferStats, +}; diff --git a/rustfs/src/app/bucket_usecase.rs b/rustfs/src/app/bucket_usecase.rs index cb4ac1dd4..b8f29db05 100644 --- a/rustfs/src/app/bucket_usecase.rs +++ b/rustfs/src/app/bucket_usecase.rs @@ -33,6 +33,10 @@ use super::storage_api::bucket_usecase::bucket::{ }, metadata_sys, policy_sys::PolicySys, + replication::{ + ReplicationTargetValidationError, replication_target_arns, should_remove_replication_target, + validate_replication_config_target_arns, + }, target::{BucketTargetType, BucketTargets}, utils::serialize, versioning_sys::BucketVersioningSys, @@ -252,14 +256,14 @@ fn validate_replication_config_targets(targets: &BucketTargets, config: &Replica .filter(|target| target.target_type == BucketTargetType::ReplicationService) .map(|target| target.arn.as_str()); - match rustfs_replication::validate_replication_config_target_arns(configured_arns, config) { + match validate_replication_config_target_arns(configured_arns, config) { Ok(()) => Ok(()), Err(err) => { let message = match err { - rustfs_replication::ReplicationTargetValidationError::RoleWithMultipleDestinations => { + ReplicationTargetValidationError::RoleWithMultipleDestinations => { "replication config with Role cannot define multiple destination targets" } - rustfs_replication::ReplicationTargetValidationError::StaleTarget => "replication config has a stale target", + ReplicationTargetValidationError::StaleTarget => "replication config has a stale target", }; Err(S3Error::with_message(S3ErrorCode::InvalidRequest, message)) } @@ -290,7 +294,7 @@ async fn replication_targets_without_config_targets( bucket: &str, config: &ReplicationConfiguration, ) -> S3Result> { - let target_arns = rustfs_replication::replication_target_arns(config); + let target_arns = replication_target_arns(config); if target_arns.is_empty() { return Ok(None); } @@ -315,7 +319,7 @@ async fn replication_targets_without_config_targets( fn remove_replication_targets_from_config_targets(targets: &mut BucketTargets, target_arns: &HashSet) -> usize { let original_len = targets.targets.len(); targets.targets.retain(|target| { - !rustfs_replication::should_remove_replication_target( + !should_remove_replication_target( target.arn.as_str(), target.target_type == BucketTargetType::ReplicationService, target_arns, @@ -2387,7 +2391,7 @@ mod tests { rules: vec![replication_rule_for_target(destination)], }; - let arns = rustfs_replication::replication_target_arns(&config); + let arns = replication_target_arns(&config); assert!(arns.contains(role)); assert!(!arns.contains(destination)); @@ -2401,7 +2405,7 @@ mod tests { rules: vec![replication_rule_for_target(destination)], }; - let arns = rustfs_replication::replication_target_arns(&config); + let arns = replication_target_arns(&config); assert!(arns.contains(destination)); } diff --git a/rustfs/src/app/object_usecase.rs b/rustfs/src/app/object_usecase.rs index b56e9839a..1e8ba41ee 100644 --- a/rustfs/src/app/object_usecase.rs +++ b/rustfs/src/app/object_usecase.rs @@ -22,6 +22,8 @@ use super::storage_api::object_usecase::access::{ PostObjectRequestMarker, authorize_request, has_bypass_governance_header, req_info_mut, }; use super::storage_api::object_usecase::bucket::quota::checker::QuotaChecker; +#[cfg(test)] +use super::storage_api::object_usecase::bucket::replication::{ReplicationState, replication_statuses_map}; use super::storage_api::object_usecase::bucket::{ VersioningConfigExt as _, lifecycle::{ @@ -38,8 +40,10 @@ use super::storage_api::object_usecase::bucket::{ predict_lifecycle_expiration, quota::QuotaOperation, replication::{ - DeletedObjectReplicationInfo, check_replicate_delete, delete_replication_state_from_config, must_replicate_object, - schedule_object_replication, schedule_replication_delete, + DeletedObjectReplicationInfo, REPLICATE_INCOMING_DELETE, ReplicationStatusType, VersionPurgeStatusType, + check_replicate_delete, delete_replication_state_from_config, delete_replication_version_id, must_replicate_object, + schedule_object_replication, schedule_replication_delete, should_schedule_delete_replication, + should_use_existing_delete_replication_info, should_use_existing_delete_replication_source, }, tagging::decode_tags, validate_restore_request, @@ -116,9 +120,6 @@ use rustfs_lock::NamespaceLockGuard; use rustfs_notify::EventArgsBuilder; use rustfs_object_capacity::capacity_manager::get_capacity_manager; use rustfs_policy::policy::action::{Action, S3Action}; -use rustfs_replication::{REPLICATE_INCOMING_DELETE, ReplicationStatusType, VersionPurgeStatusType}; -#[cfg(test)] -use rustfs_replication::{ReplicationState, replication_statuses_map}; use rustfs_s3_ops::{S3Operation, delete_event_name_for_marker, put_event_name_for_post_object}; use rustfs_s3select_api::object_store::bytes_stream; use rustfs_targets::{ @@ -1523,21 +1524,6 @@ async fn enrich_delete_replication_state_if_needed( } } -fn should_schedule_delete_replication( - opts: &ObjectOptions, - replication_source: &ObjectInfo, - deleted_delete_marker_version: bool, -) -> bool { - rustfs_replication::should_schedule_delete_replication(rustfs_replication::ReplicationDeleteScheduleInput { - replication_request: opts.replication_request, - version_id_requested: opts.version_id.is_some(), - source_delete_marker: replication_source.delete_marker, - source_replication_status: &replication_source.replication_status, - source_version_purge_status: &replication_source.version_purge_status, - deleted_delete_marker_version, - }) -} - async fn should_schedule_replica_delete_replication( bucket: &str, replication_source: &ObjectInfo, @@ -1550,18 +1536,6 @@ async fn should_schedule_replica_delete_replication( delete_replication_state_from_config(&config, replication_source, version_id, true).is_some() } -fn delete_replication_version_id(replication_source: &ObjectInfo, deleted_delete_marker_version: bool) -> Option { - rustfs_replication::delete_replication_version_id( - replication_source.delete_marker, - replication_source.version_id, - deleted_delete_marker_version, - ) -} - -fn should_use_existing_delete_replication_info(opts: &ObjectOptions) -> bool { - rustfs_replication::should_use_existing_delete_replication_info(opts.version_id.is_some(), opts.delete_marker) -} - fn internal_object_info_lookup_opts(mut opts: ObjectOptions) -> ObjectOptions { opts.http_preconditions = None; opts @@ -1596,7 +1570,7 @@ fn delete_replication_state_source<'a>( existing_object_info: Option<&'a ObjectInfo>, deleted_object_info: &'a ObjectInfo, ) -> &'a ObjectInfo { - if rustfs_replication::should_use_existing_delete_replication_source( + if should_use_existing_delete_replication_source( opts.replication_request, deleted_object_info.delete_marker, existing_object_info.is_some(), diff --git a/rustfs/src/app/storage_api.rs b/rustfs/src/app/storage_api.rs index dab24be9b..2d446f18d 100644 --- a/rustfs/src/app/storage_api.rs +++ b/rustfs/src/app/storage_api.rs @@ -605,6 +605,7 @@ pub(crate) mod bucket { pub(crate) mod replication { use std::collections::HashMap; + use std::collections::HashSet; use std::sync::Arc; use uuid::Uuid; @@ -612,6 +613,14 @@ pub(crate) mod bucket { crate::storage::storage_api::ecstore_bucket::replication::DeletedObjectReplicationInfo; type ReplicationObjectBridge = crate::storage::storage_api::ecstore_bucket::replication::ReplicationObjectBridge; pub(crate) type ReplicateDecision = rustfs_replication::ReplicateDecision; + #[cfg(test)] + pub(crate) type ReplicationState = rustfs_replication::ReplicationState; + pub(crate) type ReplicationStatusType = rustfs_replication::ReplicationStatusType; + pub(crate) type ReplicationTargetValidationError = rustfs_replication::ReplicationTargetValidationError; + pub(crate) type VersionPurgeStatusType = rustfs_replication::VersionPurgeStatusType; + pub(crate) const REPLICATE_INCOMING_DELETE: &str = rustfs_replication::REPLICATE_INCOMING_DELETE; + #[cfg(test)] + pub(crate) use rustfs_replication::replication_statuses_map; pub(crate) async fn check_replicate_delete( bucket: &str, @@ -623,12 +632,23 @@ pub(crate) mod bucket { ReplicationObjectBridge::check_delete(bucket, dobj, oi, del_opts, gerr).await } + pub(crate) fn delete_replication_version_id( + replication_source: &crate::storage::storage_api::StorageObjectInfo, + deleted_delete_marker_version: bool, + ) -> Option { + rustfs_replication::delete_replication_version_id( + replication_source.delete_marker, + replication_source.version_id, + deleted_delete_marker_version, + ) + } + pub(crate) async fn must_replicate_object( bucket: &str, object: &str, user_defined: &HashMap, user_tags: String, - status: rustfs_replication::ReplicationStatusType, + status: ReplicationStatusType, opts: crate::storage::storage_api::StorageObjectOptions, ) -> ReplicateDecision { let mopts = ReplicationObjectBridge::must_replicate_options( @@ -668,6 +688,58 @@ pub(crate) mod bucket { }; rustfs_replication::delete_replication_state_from_config(config, &source) } + + pub(crate) fn replication_target_arns(config: &s3s::dto::ReplicationConfiguration) -> HashSet { + rustfs_replication::replication_target_arns(config) + } + + pub(crate) fn should_remove_replication_target( + target_arn: &str, + is_replication_service: bool, + target_arns: &HashSet, + ) -> bool { + rustfs_replication::should_remove_replication_target(target_arn, is_replication_service, target_arns) + } + + pub(crate) fn should_schedule_delete_replication( + opts: &crate::storage::storage_api::StorageObjectOptions, + replication_source: &crate::storage::storage_api::StorageObjectInfo, + deleted_delete_marker_version: bool, + ) -> bool { + rustfs_replication::should_schedule_delete_replication(rustfs_replication::ReplicationDeleteScheduleInput { + replication_request: opts.replication_request, + version_id_requested: opts.version_id.is_some(), + source_delete_marker: replication_source.delete_marker, + source_replication_status: &replication_source.replication_status, + source_version_purge_status: &replication_source.version_purge_status, + deleted_delete_marker_version, + }) + } + + pub(crate) fn should_use_existing_delete_replication_info( + opts: &crate::storage::storage_api::StorageObjectOptions, + ) -> bool { + rustfs_replication::should_use_existing_delete_replication_info(opts.version_id.is_some(), opts.delete_marker) + } + + pub(crate) fn should_use_existing_delete_replication_source( + replication_request: bool, + deleted_delete_marker: bool, + has_existing_info: bool, + ) -> bool { + rustfs_replication::should_use_existing_delete_replication_source( + replication_request, + deleted_delete_marker, + has_existing_info, + ) + } + + pub(crate) fn validate_replication_config_target_arns<'a>( + configured_arns: impl Iterator, + config: &s3s::dto::ReplicationConfiguration, + ) -> Result<(), ReplicationTargetValidationError> { + rustfs_replication::validate_replication_config_target_arns(configured_arns, config) + } } pub(crate) mod tagging { diff --git a/scripts/check_architecture_migration_rules.sh b/scripts/check_architecture_migration_rules.sh index fb7c0fd32..1ac540b94 100755 --- a/scripts/check_architecture_migration_rules.sh +++ b/scripts/check_architecture_migration_rules.sh @@ -205,6 +205,7 @@ REPLICATION_OPERATION_CONTRACT_BACKSLIDE_HITS_FILE="${TMP_DIR}/replication_opera REPLICATION_QUEUE_CONTRACT_BACKSLIDE_HITS_FILE="${TMP_DIR}/replication_queue_contract_backslide_hits.txt" REPLICATION_QUEUE_BOUNDARY_BYPASS_HITS_FILE="${TMP_DIR}/replication_queue_boundary_bypass_hits.txt" REPLICATION_STATS_CONTRACT_BACKSLIDE_HITS_FILE="${TMP_DIR}/replication_stats_contract_backslide_hits.txt" +REPLICATION_STATS_BOUNDARY_BYPASS_HITS_FILE="${TMP_DIR}/replication_stats_boundary_bypass_hits.txt" REPLICATION_RUNTIME_CONTRACT_BACKSLIDE_HITS_FILE="${TMP_DIR}/replication_runtime_contract_backslide_hits.txt" REPLICATION_RESYNC_CONTRACT_BACKSLIDE_HITS_FILE="${TMP_DIR}/replication_resync_contract_backslide_hits.txt" REPLICATION_RESYNC_BOUNDARY_BYPASS_HITS_FILE="${TMP_DIR}/replication_resync_boundary_bypass_hits.txt" @@ -214,6 +215,7 @@ REPLICATION_MRF_WIRE_FORMAT_BACKSLIDE_HITS_FILE="${TMP_DIR}/replication_mrf_wire STORAGE_REPLICATION_HANDLE_BOUNDARY_BYPASS_HITS_FILE="${TMP_DIR}/storage_replication_handle_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" +APP_REPLICATION_CRATE_BYPASS_HITS_FILE="${TMP_DIR}/app_replication_crate_bypass_hits.txt" SCANNER_REPLICATION_DTO_BOUNDARY_BYPASS_HITS_FILE="${TMP_DIR}/scanner_replication_dto_boundary_bypass_hits.txt" OBS_REPLICATION_STATS_BOUNDARY_BYPASS_HITS_FILE="${TMP_DIR}/obs_replication_stats_boundary_bypass_hits.txt" REPLICATION_BANDWIDTH_BOUNDARY_BYPASS_HITS_FILE="${TMP_DIR}/replication_bandwidth_boundary_bypass_hits.txt" @@ -2653,6 +2655,31 @@ if [[ -s "$REPLICATION_STATS_CONTRACT_BACKSLIDE_HITS_FILE" ]]; then report_failure "replication stats contracts must stay in crates/replication: $(paste -sd '; ' "$REPLICATION_STATS_CONTRACT_BACKSLIDE_HITS_FILE")" fi +( + cd "$ROOT_DIR" + replication_stats_boundary_status=0 + rg -n --with-filename 'rustfs_replication::(ExponentialMovingAverage|XferStats|InQueueStats|QueueSample|InQueueMetric|QueueCache|ProxyMetric|ProxyStatsCache|FailureSample|FailStats|FailedMetric|LatencyStats|BucketReplicationStat|QueueStats|QueueNode|BucketReplicationStats|BucketStats|SRMetricsSummary|ActiveWorkerStat|WorkerSample)\b' \ + crates/ecstore/src/bucket/replication \ + --glob '*.rs' \ + --glob '!replication_stats_boundary.rs' >"$REPLICATION_STATS_BOUNDARY_BYPASS_HITS_FILE" || replication_stats_boundary_status=$? + if [[ "$replication_stats_boundary_status" -ne 0 && "$replication_stats_boundary_status" -ne 1 ]]; then + exit "$replication_stats_boundary_status" + fi + + replication_stats_boundary_grouped_status=0 + rg -n -U --with-filename 'use\s+rustfs_replication::\{[^}]*\b(ExponentialMovingAverage|XferStats|InQueueStats|QueueSample|InQueueMetric|QueueCache|ProxyMetric|ProxyStatsCache|FailureSample|FailStats|FailedMetric|LatencyStats|BucketReplicationStat|QueueStats|QueueNode|BucketReplicationStats|BucketStats|SRMetricsSummary|ActiveWorkerStat|WorkerSample)\b' \ + crates/ecstore/src/bucket/replication \ + --glob '*.rs' \ + --glob '!replication_stats_boundary.rs' >>"$REPLICATION_STATS_BOUNDARY_BYPASS_HITS_FILE" || replication_stats_boundary_grouped_status=$? + if [[ "$replication_stats_boundary_grouped_status" -ne 0 && "$replication_stats_boundary_grouped_status" -ne 1 ]]; then + exit "$replication_stats_boundary_grouped_status" + fi +) + +if [[ -s "$REPLICATION_STATS_BOUNDARY_BYPASS_HITS_FILE" ]]; then + report_failure "replication stats contracts must stay behind replication_stats_boundary: $(paste -sd '; ' "$REPLICATION_STATS_BOUNDARY_BYPASS_HITS_FILE")" +fi + ( cd "$ROOT_DIR" replication_runtime_status=0 @@ -2832,6 +2859,18 @@ 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" + rg -n --with-filename 'rustfs_replication::|use\s+rustfs_replication\b' \ + rustfs/src/app \ + --glob '*.rs' \ + --glob '!rustfs/src/app/storage_api.rs' || true +) >"$APP_REPLICATION_CRATE_BYPASS_HITS_FILE" + +if [[ -s "$APP_REPLICATION_CRATE_BYPASS_HITS_FILE" ]]; then + report_failure "app replication crate access must stay behind rustfs/src/app/storage_api.rs: $(paste -sd '; ' "$APP_REPLICATION_CRATE_BYPASS_HITS_FILE")" +fi + ( cd "$ROOT_DIR" {