mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-20 19:42:17 +00:00
fix(replication): mark failed targets offline (#4405)
This commit is contained in:
@@ -1759,7 +1759,30 @@ impl TargetClient {
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_res) => Ok(()),
|
Ok(_res) => Ok(()),
|
||||||
Err(e) => Err(e.into()),
|
Err(e) => match e {
|
||||||
|
SdkError::ServiceError(service_err) => {
|
||||||
|
let err = service_err.into_err();
|
||||||
|
let meta = err.meta();
|
||||||
|
let error = match (meta.code(), meta.message()) {
|
||||||
|
(Some(code), Some(message)) => format!("remove_object failed: {code}: {message}"),
|
||||||
|
(Some(code), None) => format!("remove_object failed: {code}"),
|
||||||
|
(None, Some(message)) => format!("remove_object failed: {message}"),
|
||||||
|
(None, None) => format!("remove_object failed: {err:?}"),
|
||||||
|
};
|
||||||
|
Err(S3ClientError::with_metadata(
|
||||||
|
error,
|
||||||
|
None,
|
||||||
|
meta.code().map(ToOwned::to_owned),
|
||||||
|
meta.message().map(ToOwned::to_owned),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
SdkError::DispatchFailure(dispatch_err) => Err(S3ClientError::new(format!(
|
||||||
|
"remove_object dispatch failure for bucket:{bucket} object:{object}: {dispatch_err:?}"
|
||||||
|
))),
|
||||||
|
other => Err(S3ClientError::new(format!(
|
||||||
|
"remove_object request failed for bucket:{bucket} object:{object}: {other:?}"
|
||||||
|
))),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ use rustfs_utils::{DEFAULT_SIP_HASH_KEY, sip_hash};
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use s3s::dto::ReplicationConfiguration;
|
use s3s::dto::ReplicationConfiguration;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::fmt::Display;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
use time::format_description::well_known::Rfc3339;
|
use time::format_description::well_known::Rfc3339;
|
||||||
@@ -94,6 +95,21 @@ const EVENT_RESYNC_TASK_FAILED: &str = "replication_resync_task_failed";
|
|||||||
const EVENT_RESYNC_TARGET_OPERATION_FAILED: &str = "replication_resync_target_operation_failed";
|
const EVENT_RESYNC_TARGET_OPERATION_FAILED: &str = "replication_resync_target_operation_failed";
|
||||||
const EVENT_RESYNC_RUNTIME_CHANNEL_FAILED: &str = "replication_resync_runtime_channel_failed";
|
const EVENT_RESYNC_RUNTIME_CHANNEL_FAILED: &str = "replication_resync_runtime_channel_failed";
|
||||||
const ERR_REPLICATION_METADATA_COPY_UNSUPPORTED: &str = "metadata-only replication is not implemented";
|
const ERR_REPLICATION_METADATA_COPY_UNSUPPORTED: &str = "metadata-only replication is not implemented";
|
||||||
|
const REPLICATION_TARGET_OFFLINE_ERROR_MARKERS: &[&str] = &[
|
||||||
|
"dispatch failure",
|
||||||
|
"timeouterror",
|
||||||
|
"timed out",
|
||||||
|
"connection refused",
|
||||||
|
"connection reset",
|
||||||
|
"connection closed",
|
||||||
|
"connection aborted",
|
||||||
|
"broken pipe",
|
||||||
|
"dns error",
|
||||||
|
"failed to lookup address",
|
||||||
|
"name or service not known",
|
||||||
|
"deadline has elapsed",
|
||||||
|
"tcp connect error",
|
||||||
|
];
|
||||||
|
|
||||||
const RESYNC_TIME_INTERVAL: TokioDuration = TokioDuration::from_secs(60);
|
const RESYNC_TIME_INTERVAL: TokioDuration = TokioDuration::from_secs(60);
|
||||||
|
|
||||||
@@ -171,6 +187,19 @@ fn is_version_id_format_mismatch(err: &SdkError<HeadObjectError>) -> bool {
|
|||||||
is_version_id_mismatch(code, raw_status)
|
is_version_id_mismatch(code, raw_status)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_replication_target_offline_error(err: &(impl Display + ?Sized)) -> bool {
|
||||||
|
let message = err.to_string().to_ascii_lowercase();
|
||||||
|
REPLICATION_TARGET_OFFLINE_ERROR_MARKERS
|
||||||
|
.iter()
|
||||||
|
.any(|marker| message.contains(marker))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn mark_replication_target_offline_if_needed(target_client: &TargetClient, err: &(impl Display + ?Sized)) {
|
||||||
|
if is_replication_target_offline_error(err) {
|
||||||
|
ReplicationTargetStore::mark_target_offline(target_client).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn head_object_fallback(
|
async fn head_object_fallback(
|
||||||
source_bucket: &str,
|
source_bucket: &str,
|
||||||
tgt_client: &TargetClient,
|
tgt_client: &TargetClient,
|
||||||
@@ -1845,7 +1874,7 @@ async fn replicate_delete_to_target(dobj: &DeletedObjectReplicationInfo, tgt_cli
|
|||||||
} else {
|
} else {
|
||||||
rinfo.version_purge_status = VersionPurgeStatusType::Failed;
|
rinfo.version_purge_status = VersionPurgeStatusType::Failed;
|
||||||
}
|
}
|
||||||
// TODO: check offline
|
mark_replication_target_offline_if_needed(&tgt_client, &e).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2406,7 +2435,7 @@ impl ReplicateObjectInfoExt for ReplicateObjectInfo {
|
|||||||
"Replication target operation failed"
|
"Replication target operation failed"
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO: check offline
|
mark_replication_target_offline_if_needed(&tgt_client, &err).await;
|
||||||
return rinfo;
|
return rinfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2835,7 +2864,7 @@ impl ReplicateObjectInfoExt for ReplicateObjectInfo {
|
|||||||
);
|
);
|
||||||
rinfo.duration = (OffsetDateTime::now_utc() - start_time).unsigned_abs();
|
rinfo.duration = (OffsetDateTime::now_utc() - start_time).unsigned_abs();
|
||||||
|
|
||||||
// TODO: check offline
|
mark_replication_target_offline_if_needed(&tgt_client, &err).await;
|
||||||
return rinfo;
|
return rinfo;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3018,6 +3047,65 @@ mod tests {
|
|||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
fn test_target_client(endpoint: String) -> TargetClient {
|
||||||
|
let config = aws_sdk_s3::Config::builder()
|
||||||
|
.endpoint_url(endpoint.clone())
|
||||||
|
.region(aws_sdk_s3::config::Region::new("us-east-1"))
|
||||||
|
.credentials_provider(aws_sdk_s3::config::SharedCredentialsProvider::new(
|
||||||
|
aws_credential_types::Credentials::new("access", "secret", None, None, "test"),
|
||||||
|
))
|
||||||
|
.behavior_version(aws_sdk_s3::config::BehaviorVersion::latest())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
TargetClient {
|
||||||
|
endpoint,
|
||||||
|
credentials: None,
|
||||||
|
bucket: "target-bucket".to_string(),
|
||||||
|
storage_class: String::new(),
|
||||||
|
disable_proxy: false,
|
||||||
|
arn: format!("arn:rustfs:replication:us-east-1:target:{}", Uuid::new_v4()),
|
||||||
|
reset_id: String::new(),
|
||||||
|
secure: false,
|
||||||
|
health_check_duration: std::time::Duration::from_secs(5),
|
||||||
|
replicate_sync: false,
|
||||||
|
client: Arc::new(aws_sdk_s3::Client::from_conf(config)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn replication_target_offline_error_classifier_is_network_scoped() {
|
||||||
|
assert!(is_replication_target_offline_error(&"put_object dispatch failure: connector error"));
|
||||||
|
assert!(is_replication_target_offline_error(&"request TimeoutError after retry"));
|
||||||
|
assert!(is_replication_target_offline_error(&"tcp connect error: connection refused"));
|
||||||
|
assert!(!is_replication_target_offline_error(&"put_object failed: AccessDenied: denied"));
|
||||||
|
assert!(!is_replication_target_offline_error(&"put_object failed: NoSuchBucket"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn replication_target_network_failure_marks_target_offline() {
|
||||||
|
let endpoint = format!("http://network-failure-{}.example:9000", Uuid::new_v4());
|
||||||
|
let target_client = test_target_client(endpoint);
|
||||||
|
|
||||||
|
assert!(!ReplicationTargetStore::target_is_offline(&target_client).await);
|
||||||
|
|
||||||
|
let err = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "connection refused");
|
||||||
|
mark_replication_target_offline_if_needed(&target_client, &err).await;
|
||||||
|
|
||||||
|
assert!(ReplicationTargetStore::target_is_offline(&target_client).await);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn replication_target_service_failure_keeps_target_online() {
|
||||||
|
let endpoint = format!("http://service-failure-{}.example:9000", Uuid::new_v4());
|
||||||
|
let target_client = test_target_client(endpoint);
|
||||||
|
|
||||||
|
assert!(!ReplicationTargetStore::target_is_offline(&target_client).await);
|
||||||
|
|
||||||
|
mark_replication_target_offline_if_needed(&target_client, &"put_object failed: AccessDenied: denied").await;
|
||||||
|
|
||||||
|
assert!(!ReplicationTargetStore::target_is_offline(&target_client).await);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_unmarshal_resync_payload() {
|
fn test_unmarshal_resync_payload() {
|
||||||
let start = OffsetDateTime::from_unix_timestamp(1_700_000_000).expect("valid ts");
|
let start = OffsetDateTime::from_unix_timestamp(1_700_000_000).expect("valid ts");
|
||||||
|
|||||||
@@ -92,6 +92,10 @@ impl ReplicationTargetStore {
|
|||||||
pub(crate) async fn target_is_offline(target_client: &TargetClient) -> bool {
|
pub(crate) async fn target_is_offline(target_client: &TargetClient) -> bool {
|
||||||
BucketTargetSys::get().is_offline(&target_client.to_url()).await
|
BucketTargetSys::get().is_offline(&target_client.to_url()).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn mark_target_offline(target_client: &TargetClient) {
|
||||||
|
BucketTargetSys::get().mark_offline(&target_client.to_url()).await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn replication_put_object_options(sc: &str, object_info: &ObjectInfo) -> Result<(PutObjectOptions, bool)> {
|
pub(crate) fn replication_put_object_options(sc: &str, object_info: &ObjectInfo) -> Result<(PutObjectOptions, bool)> {
|
||||||
|
|||||||
Reference in New Issue
Block a user