fix(replication): refresh targets after site endpoint edits (#4756)

* fix(replication): refresh targets after site endpoint edits

* fix(replication): serialize site bucket lifecycle
This commit is contained in:
Zhengchao An
2026-07-12 05:03:17 +08:00
committed by GitHub
parent af831bde4b
commit 676f2276b4
10 changed files with 1548 additions and 237 deletions
+7
View File
@@ -27,6 +27,7 @@ use crate::admin::storage_api::bucket::{
};
use crate::admin::storage_api::contract::bucket::{BucketOperations, BucketOptions, MakeBucketOptions};
use crate::admin::storage_api::error::StorageError;
use crate::storage::storage_api::lock_bucket_targets_metadata;
use crate::{
admin::{
auth::validate_admin_request,
@@ -836,6 +837,11 @@ impl Operation for ImportBucketMetadata {
for (bucket_name, metadata) in &bucket_metadatas {
for (config_file, data) in imported_configs_to_persist(metadata) {
let site_replication_item = imported_config_to_site_replication_item(bucket_name, metadata, config_file, &data)?;
let targets_guard = if matches!(config_file, BUCKET_REPLICATION_CONFIG | BUCKET_TARGETS_FILE) {
Some(lock_bucket_targets_metadata(bucket_name).await)
} else {
None
};
if let Err(e) = metadata_sys::update(bucket_name, config_file, data).await {
warn!(
event = EVENT_ADMIN_BUCKET_META_STATE,
@@ -853,6 +859,7 @@ impl Operation for ImportBucketMetadata {
"failed to persist imported bucket metadata for {bucket_name}/{config_file}: {e}"
));
}
drop(targets_guard);
if let Some(item) = site_replication_item
&& let Err(err) = site_replication_bucket_meta_hook(item).await
{
+3
View File
@@ -29,6 +29,7 @@ use crate::admin::utils::read_compatible_admin_body;
use crate::auth::{check_key_valid, get_session_token};
use crate::error::ApiError;
use crate::server::{ADMIN_PREFIX, RemoteAddr};
use crate::storage::storage_api::lock_bucket_targets_metadata;
use http::{HeaderMap, HeaderValue, Uri};
use hyper::{Method, StatusCode};
use matchit::Params;
@@ -431,6 +432,7 @@ impl Operation for SetRemoteTargetHandler {
if remote_target.arn.is_empty() {
return Err(S3Error::with_message(S3ErrorCode::InvalidRequest, "ARN is empty".to_string()));
}
let _targets_guard = lock_bucket_targets_metadata(bucket).await;
if update {
let Some(mut target) = bucket_target_sys
@@ -577,6 +579,7 @@ impl Operation for RemoveRemoteTargetHandler {
.map_err(ApiError::from)?;
let sys = BucketTargetSys::get();
let _targets_guard = lock_bucket_targets_metadata(bucket).await;
let targets = sys.remove_target(bucket, arn_str).await.map_err(map_bucket_target_error)?;
File diff suppressed because it is too large Load Diff
+6
View File
@@ -598,6 +598,12 @@ pub const ADMIN_ROUTE_POLICY_SPECS: &[AdminRouteSpec] = &[
SITE_REPLICATION_ADD,
RouteRiskLevel::High,
),
admin(
HttpMethod::Put,
"/rustfs/admin/v3/site-replication/peer/edit-capabilities",
SITE_REPLICATION_OPERATION,
RouteRiskLevel::High,
),
admin(
HttpMethod::Put,
"/rustfs/admin/v3/site-replication/peer/edit",
@@ -272,6 +272,7 @@ fn expected_admin_route_matrix() -> Vec<RouteMatrixEntry> {
admin_route(Method::PUT, "/v3/site-replication/peer/bucket-meta"),
admin_route(Method::GET, "/v3/site-replication/peer/idp-settings"),
admin_route(Method::PUT, "/v3/site-replication/edit"),
admin_route(Method::PUT, "/v3/site-replication/peer/edit-capabilities"),
admin_route(Method::PUT, "/v3/site-replication/peer/edit"),
admin_route(Method::PUT, "/v3/site-replication/peer/remove"),
admin_route(Method::PUT, "/v3/site-replication/resync/op"),
@@ -1192,6 +1193,7 @@ fn test_register_routes_cover_representative_admin_paths() {
assert_route(&router, Method::PUT, &admin_path("/v3/site-replication/peer/bucket-meta"));
assert_route(&router, Method::GET, &admin_path("/v3/site-replication/peer/idp-settings"));
assert_route(&router, Method::PUT, &admin_path("/v3/site-replication/edit"));
assert_route(&router, Method::PUT, &admin_path("/v3/site-replication/peer/edit-capabilities"));
assert_route(&router, Method::PUT, &admin_path("/v3/site-replication/peer/edit"));
assert_route(&router, Method::PUT, &admin_path("/v3/site-replication/peer/remove"));
assert_route(&router, Method::PUT, &admin_path("/v3/site-replication/resync/op"));
+3
View File
@@ -40,6 +40,7 @@ use crate::license::license_check;
use crate::server::{
ADMIN_PREFIX, HEALTH_PREFIX, HEALTH_READY_PATH, MINIO_ADMIN_PREFIX, PROFILE_CPU_PATH, PROFILE_MEMORY_PATH, is_admin_path,
};
use crate::storage::storage_api::lock_bucket_targets_metadata;
use aws_sdk_s3::primitives::ByteStream as AwsByteStream;
use bytes::Bytes;
use futures::{Stream, StreamExt};
@@ -2136,6 +2137,7 @@ async fn target_client_object_lock_enabled(bucket: &str, target: &BucketTarget)
}
async fn start_replication_resync(bucket: &str, reset: &ReplicationResetStartRequest) -> S3Result<ReplicationResetTarget> {
let targets_guard = lock_bucket_targets_metadata(bucket).await;
let (config, _) = metadata_sys::get_replication_config(bucket).await.map_err(ApiError::from)?;
let resolved_arn = resolve_replication_reset_target_arn(&config, &reset.arn)?;
let mut resolved_reset = reset.clone();
@@ -2149,6 +2151,7 @@ async fn start_replication_resync(bucket: &str, reset: &ReplicationResetStartReq
.await
.map_err(ApiError::from)?;
BucketTargetSys::get().update_all_targets(bucket, Some(&targets)).await;
drop(targets_guard);
let Some(pool) = current_replication_pool_handle() else {
return Err(s3_error!(InternalError, "replication pool is not initialized"));
+5
View File
@@ -71,6 +71,7 @@ use crate::app::runtime_sources::{
use crate::auth::get_condition_values_with_client_info;
use crate::error::ApiError;
use crate::server::RemoteAddr;
use crate::storage::storage_api::lock_bucket_targets_metadata;
use futures::StreamExt;
use http::StatusCode;
use metrics::counter;
@@ -1455,6 +1456,7 @@ impl DefaultBucketUsecase {
.get_bucket_info(&bucket, &BucketOptions::default())
.await
.map_err(ApiError::from)?;
let targets_guard = lock_bucket_targets_metadata(&bucket).await;
let replication_config = match metadata_sys::get_replication_config(&bucket).await {
Ok((config, _)) => Some(config),
Err(StorageError::ConfigNotFound) => None,
@@ -1477,6 +1479,7 @@ impl DefaultBucketUsecase {
}
return Err(err);
}
drop(targets_guard);
notify_bucket_metadata_reload(bucket.clone(), "delete bucket replication", request_context);
@@ -2296,11 +2299,13 @@ impl DefaultBucketUsecase {
.await
.map_err(ApiError::from)?;
let targets_guard = lock_bucket_targets_metadata(&bucket).await;
validate_bucket_replication_update(&bucket, &replication_configuration).await?;
let data = serialize_config(&replication_configuration)?;
metadata_sys::update(&bucket, BUCKET_REPLICATION_CONFIG, data)
.await
.map_err(ApiError::from)?;
drop(targets_guard);
notify_bucket_metadata_reload(bucket.clone(), "put bucket replication", request_context);
+57 -1
View File
@@ -14,11 +14,34 @@
//! Storage owner-local boundary for ECStore facade and storage contract symbols.
use std::sync::Arc;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::sync::{Arc, LazyLock};
use rustfs_storage_api as storage_contracts;
use tokio::sync::{Mutex, OwnedMutexGuard};
use tokio_util::sync::CancellationToken;
const BUCKET_TARGETS_METADATA_LOCK_SHARDS: usize = 256;
static BUCKET_TARGETS_METADATA_LOCKS: LazyLock<Vec<Arc<Mutex<()>>>> = LazyLock::new(|| {
(0..BUCKET_TARGETS_METADATA_LOCK_SHARDS)
.map(|_| Arc::new(Mutex::new(())))
.collect()
});
pub(crate) async fn lock_bucket_targets_metadata(bucket: &str) -> OwnedMutexGuard<()> {
BUCKET_TARGETS_METADATA_LOCKS[bucket_targets_metadata_lock_shard(bucket)]
.clone()
.lock_owned()
.await
}
fn bucket_targets_metadata_lock_shard(bucket: &str) -> usize {
let mut hasher = DefaultHasher::new();
bucket.hash(&mut hasher);
hasher.finish() as usize % BUCKET_TARGETS_METADATA_LOCK_SHARDS
}
pub(crate) mod contract {
pub(crate) mod admin {
pub(crate) use super::super::storage_contracts::StorageAdminApi;
@@ -1435,3 +1458,36 @@ pub(crate) async fn store_compression_total_in_backend() {
pub(crate) async fn init_compression_total_memory_from_backend(store: Arc<ECStore>) {
ecstore_data_usage::init_compression_total_memory_from_backend(store).await
}
#[cfg(test)]
mod tests {
use super::{bucket_targets_metadata_lock_shard, lock_bucket_targets_metadata};
use std::time::Duration;
#[tokio::test]
async fn bucket_target_metadata_locks_serialize_only_matching_shards() {
let bucket = "bucket-target-lock";
let other = (0..1000)
.map(|index| format!("other-bucket-{index}"))
.find(|candidate| bucket_targets_metadata_lock_shard(candidate) != bucket_targets_metadata_lock_shard(bucket))
.expect("find bucket on another lock shard");
let guard = lock_bucket_targets_metadata(bucket).await;
assert!(
tokio::time::timeout(Duration::from_millis(20), lock_bucket_targets_metadata(bucket))
.await
.is_err()
);
assert!(
tokio::time::timeout(Duration::from_secs(1), lock_bucket_targets_metadata(&other))
.await
.is_ok()
);
drop(guard);
assert!(
tokio::time::timeout(Duration::from_secs(1), lock_bucket_targets_metadata(bucket))
.await
.is_ok()
);
}
}