mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-11 15:46:53 +00:00
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:
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
@@ -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"));
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user