mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-13 16:46:55 +00:00
2983 lines
107 KiB
Rust
2983 lines
107 KiB
Rust
// 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.
|
|
|
|
use crate::bucket::bandwidth::reader::{BucketOptions, MonitorReaderOptions, MonitoredReader};
|
|
use crate::bucket::bucket_target_sys::{
|
|
AdvancedPutOptions, BucketTargetSys, PutObjectOptions, PutObjectPartOptions, RemoveObjectOptions, TargetClient,
|
|
};
|
|
use crate::bucket::metadata_sys;
|
|
use crate::bucket::replication::ResyncStatusType;
|
|
use crate::bucket::replication::{ObjectOpts, ReplicationConfigurationExt as _};
|
|
use crate::bucket::tagging::decode_tags_to_map;
|
|
use crate::bucket::target::BucketTargets;
|
|
use crate::bucket::versioning_sys::BucketVersioningSys;
|
|
use crate::client::api_get_options::{AdvancedGetOptions, StatObjectOptions};
|
|
use crate::config::com::save_config;
|
|
use crate::disk::BUCKET_META_PREFIX;
|
|
use crate::error::{Error, Result, is_err_object_not_found, is_err_version_not_found};
|
|
use crate::event::name::EventName;
|
|
use crate::event_notification::{EventArgs, send_event};
|
|
use crate::global::GLOBAL_LocalNodeName;
|
|
use crate::global::get_global_bucket_monitor;
|
|
use crate::set_disk::get_lock_acquire_timeout;
|
|
use crate::store_api::{DeletedObject, HTTPRangeSpec, ObjectInfo, ObjectOptions, ObjectToDelete, WalkOptions};
|
|
use crate::{StorageAPI, new_object_layer_fn};
|
|
use aws_sdk_s3::error::SdkError;
|
|
use aws_sdk_s3::operation::head_object::HeadObjectOutput;
|
|
use aws_sdk_s3::primitives::ByteStream;
|
|
use aws_sdk_s3::types::{CompletedPart, ObjectLockLegalHoldStatus};
|
|
use aws_smithy_types::body::SdkBody;
|
|
use byteorder::ByteOrder;
|
|
use futures::future::join_all;
|
|
use futures::stream::StreamExt;
|
|
use http::HeaderMap;
|
|
use http_body::Frame;
|
|
use http_body_util::StreamBody;
|
|
use regex::Regex;
|
|
use rustfs_filemeta::{
|
|
MrfReplicateEntry, REPLICATE_EXISTING, REPLICATE_EXISTING_DELETE, REPLICATION_RESET, ReplicateDecision, ReplicateObjectInfo,
|
|
ReplicateTargetDecision, ReplicatedInfos, ReplicatedTargetInfo, ReplicationAction, ReplicationState, ReplicationStatusType,
|
|
ReplicationType, ReplicationWorkerOperation, ResyncDecision, ResyncTargetDecision, VersionPurgeStatusType,
|
|
get_replication_state, parse_replicate_decision, replication_statuses_map, target_reset_header, version_purge_statuses_map,
|
|
};
|
|
use rustfs_utils::http::{
|
|
AMZ_BUCKET_REPLICATION_STATUS, AMZ_OBJECT_TAGGING, AMZ_TAGGING_DIRECTIVE, CONTENT_ENCODING, HeaderExt as _,
|
|
RESERVED_METADATA_PREFIX, RESERVED_METADATA_PREFIX_LOWER, RUSTFS_REPLICATION_ACTUAL_OBJECT_SIZE,
|
|
RUSTFS_REPLICATION_RESET_STATUS, SSEC_ALGORITHM_HEADER, SSEC_KEY_HEADER, SSEC_KEY_MD5_HEADER, headers,
|
|
};
|
|
use rustfs_utils::path::path_join_buf;
|
|
use rustfs_utils::string::strings_has_prefix_fold;
|
|
use rustfs_utils::{DEFAULT_SIP_HASH_KEY, sip_hash};
|
|
use s3s::dto::ReplicationConfiguration;
|
|
use serde::Deserialize;
|
|
use serde::Serialize;
|
|
use std::any::Any;
|
|
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
use time::OffsetDateTime;
|
|
use time::format_description::well_known::Rfc3339;
|
|
use tokio::io::AsyncRead;
|
|
use tokio::sync::RwLock;
|
|
use tokio::task::JoinSet;
|
|
use tokio::time::Duration as TokioDuration;
|
|
use tokio_util::io::ReaderStream;
|
|
use tokio_util::sync::CancellationToken;
|
|
use tracing::{error, info, warn};
|
|
|
|
const REPLICATION_DIR: &str = ".replication";
|
|
const RESYNC_FILE_NAME: &str = "resync.bin";
|
|
const RESYNC_META_FORMAT: u16 = 1;
|
|
const RESYNC_META_VERSION: u16 = 1;
|
|
const RESYNC_TIME_INTERVAL: TokioDuration = TokioDuration::from_secs(60);
|
|
|
|
static WARNED_MONITOR_UNINIT: std::sync::Once = std::sync::Once::new();
|
|
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct ResyncOpts {
|
|
pub bucket: String,
|
|
pub arn: String,
|
|
pub resync_id: String,
|
|
pub resync_before: Option<OffsetDateTime>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
pub struct TargetReplicationResyncStatus {
|
|
pub start_time: Option<OffsetDateTime>,
|
|
pub last_update: Option<OffsetDateTime>,
|
|
pub resync_id: String,
|
|
pub resync_before_date: Option<OffsetDateTime>,
|
|
pub resync_status: ResyncStatusType,
|
|
pub failed_size: i64,
|
|
pub failed_count: i64,
|
|
pub replicated_size: i64,
|
|
pub replicated_count: i64,
|
|
pub bucket: String,
|
|
pub object: String,
|
|
pub error: Option<String>,
|
|
}
|
|
|
|
impl TargetReplicationResyncStatus {
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
pub struct BucketReplicationResyncStatus {
|
|
pub version: u16,
|
|
pub targets_map: HashMap<String, TargetReplicationResyncStatus>,
|
|
pub id: i32,
|
|
pub last_update: Option<OffsetDateTime>,
|
|
}
|
|
|
|
impl BucketReplicationResyncStatus {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
version: RESYNC_META_VERSION,
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
pub fn clone_tgt_stats(&self) -> HashMap<String, TargetReplicationResyncStatus> {
|
|
self.targets_map.clone()
|
|
}
|
|
|
|
pub fn marshal_msg(&self) -> Result<Vec<u8>> {
|
|
Ok(rmp_serde::to_vec(&self)?)
|
|
}
|
|
|
|
pub fn unmarshal_msg(data: &[u8]) -> Result<Self> {
|
|
Ok(rmp_serde::from_slice(data)?)
|
|
}
|
|
}
|
|
|
|
static RESYNC_WORKER_COUNT: usize = 10;
|
|
|
|
#[derive(Debug)]
|
|
pub struct ReplicationResyncer {
|
|
pub status_map: Arc<RwLock<HashMap<String, BucketReplicationResyncStatus>>>,
|
|
pub worker_size: usize,
|
|
pub resync_cancel_tx: CancellationToken,
|
|
pub resync_cancel_rx: CancellationToken,
|
|
pub worker_tx: tokio::sync::broadcast::Sender<()>,
|
|
pub worker_rx: tokio::sync::broadcast::Receiver<()>,
|
|
}
|
|
|
|
impl ReplicationResyncer {
|
|
pub async fn new() -> Self {
|
|
let resync_cancel_tx = CancellationToken::new();
|
|
let resync_cancel_rx = resync_cancel_tx.clone();
|
|
let (worker_tx, worker_rx) = tokio::sync::broadcast::channel(RESYNC_WORKER_COUNT);
|
|
|
|
for _ in 0..RESYNC_WORKER_COUNT {
|
|
if let Err(err) = worker_tx.send(()) {
|
|
error!("Failed to send worker message: {}", err);
|
|
}
|
|
}
|
|
|
|
Self {
|
|
status_map: Arc::new(RwLock::new(HashMap::new())),
|
|
worker_size: RESYNC_WORKER_COUNT,
|
|
resync_cancel_tx,
|
|
resync_cancel_rx,
|
|
worker_tx,
|
|
worker_rx,
|
|
}
|
|
}
|
|
|
|
pub async fn mark_status<S: StorageAPI>(&self, status: ResyncStatusType, opts: ResyncOpts, obj_layer: Arc<S>) -> Result<()> {
|
|
let bucket_status = {
|
|
let mut status_map = self.status_map.write().await;
|
|
|
|
let bucket_status = if let Some(bucket_status) = status_map.get_mut(&opts.bucket) {
|
|
bucket_status
|
|
} else {
|
|
let mut bucket_status = BucketReplicationResyncStatus::new();
|
|
bucket_status.id = 0;
|
|
status_map.insert(opts.bucket.clone(), bucket_status);
|
|
status_map.get_mut(&opts.bucket).unwrap()
|
|
};
|
|
|
|
let state = if let Some(state) = bucket_status.targets_map.get_mut(&opts.arn) {
|
|
state
|
|
} else {
|
|
let state = TargetReplicationResyncStatus::new();
|
|
bucket_status.targets_map.insert(opts.arn.clone(), state);
|
|
bucket_status.targets_map.get_mut(&opts.arn).unwrap()
|
|
};
|
|
|
|
state.resync_status = status;
|
|
state.last_update = Some(OffsetDateTime::now_utc());
|
|
|
|
bucket_status.last_update = Some(OffsetDateTime::now_utc());
|
|
|
|
bucket_status.clone()
|
|
};
|
|
|
|
save_resync_status(&opts.bucket, &bucket_status, obj_layer).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn inc_stats(&self, status: &TargetReplicationResyncStatus, opts: ResyncOpts) {
|
|
let mut status_map = self.status_map.write().await;
|
|
|
|
let bucket_status = if let Some(bucket_status) = status_map.get_mut(&opts.bucket) {
|
|
bucket_status
|
|
} else {
|
|
let mut bucket_status = BucketReplicationResyncStatus::new();
|
|
bucket_status.id = 0;
|
|
status_map.insert(opts.bucket.clone(), bucket_status);
|
|
status_map.get_mut(&opts.bucket).unwrap()
|
|
};
|
|
|
|
let state = if let Some(state) = bucket_status.targets_map.get_mut(&opts.arn) {
|
|
state
|
|
} else {
|
|
let state = TargetReplicationResyncStatus::new();
|
|
bucket_status.targets_map.insert(opts.arn.clone(), state);
|
|
bucket_status.targets_map.get_mut(&opts.arn).unwrap()
|
|
};
|
|
|
|
state.object = status.object.clone();
|
|
state.replicated_count += status.replicated_count;
|
|
state.replicated_size += status.replicated_size;
|
|
state.failed_count += status.failed_count;
|
|
state.failed_size += status.failed_size;
|
|
state.last_update = Some(OffsetDateTime::now_utc());
|
|
bucket_status.last_update = Some(OffsetDateTime::now_utc());
|
|
}
|
|
|
|
pub async fn persist_to_disk<S: StorageAPI>(&self, cancel_token: CancellationToken, api: Arc<S>) {
|
|
let mut interval = tokio::time::interval(RESYNC_TIME_INTERVAL);
|
|
|
|
let mut last_update_times = HashMap::new();
|
|
|
|
loop {
|
|
tokio::select! {
|
|
_ = cancel_token.cancelled() => {
|
|
return;
|
|
}
|
|
_ = interval.tick() => {
|
|
|
|
let status_map = self.status_map.read().await;
|
|
|
|
let mut update = false;
|
|
for (bucket, status) in status_map.iter() {
|
|
for target in status.targets_map.values() {
|
|
if target.last_update.is_none() {
|
|
update = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if let Some(last_update) = status.last_update
|
|
&& last_update > *last_update_times.get(bucket).unwrap_or(&OffsetDateTime::UNIX_EPOCH) {
|
|
update = true;
|
|
}
|
|
|
|
if update {
|
|
if let Err(err) = save_resync_status(bucket, status, api.clone()).await {
|
|
error!("Failed to save resync status: {}", err);
|
|
} else {
|
|
last_update_times.insert(bucket.clone(), status.last_update.unwrap());
|
|
}
|
|
}
|
|
}
|
|
|
|
interval.reset();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn resync_bucket_mark_status<S: StorageAPI>(&self, status: ResyncStatusType, opts: ResyncOpts, storage: Arc<S>) {
|
|
if let Err(err) = self.mark_status(status, opts.clone(), storage.clone()).await {
|
|
error!("Failed to mark resync status: {}", err);
|
|
}
|
|
if let Err(err) = self.worker_tx.send(()) {
|
|
error!("Failed to send worker message: {}", err);
|
|
}
|
|
// TODO: Metrics
|
|
}
|
|
|
|
pub async fn resync_bucket<S: StorageAPI>(
|
|
self: Arc<Self>,
|
|
cancellation_token: CancellationToken,
|
|
storage: Arc<S>,
|
|
heal: bool,
|
|
opts: ResyncOpts,
|
|
) {
|
|
let mut worker_rx = self.worker_rx.resubscribe();
|
|
|
|
tokio::select! {
|
|
_ = cancellation_token.cancelled() => {
|
|
return;
|
|
}
|
|
|
|
_ = worker_rx.recv() => {}
|
|
}
|
|
|
|
let cfg = match get_replication_config(&opts.bucket).await {
|
|
Ok(cfg) => cfg,
|
|
Err(err) => {
|
|
error!("Failed to get replication config: {}", err);
|
|
self.resync_bucket_mark_status(ResyncStatusType::ResyncFailed, opts.clone(), storage.clone())
|
|
.await;
|
|
return;
|
|
}
|
|
};
|
|
|
|
let targets = match BucketTargetSys::get().list_bucket_targets(&opts.bucket).await {
|
|
Ok(targets) => targets,
|
|
Err(err) => {
|
|
warn!("Failed to list bucket targets: {}", err);
|
|
self.resync_bucket_mark_status(ResyncStatusType::ResyncFailed, opts.clone(), storage.clone())
|
|
.await;
|
|
return;
|
|
}
|
|
};
|
|
|
|
let rcfg = ReplicationConfig::new(cfg.clone(), Some(targets));
|
|
|
|
let target_arns = if let Some(cfg) = cfg {
|
|
cfg.filter_target_arns(&ObjectOpts {
|
|
op_type: ReplicationType::Resync,
|
|
target_arn: opts.arn.clone(),
|
|
..Default::default()
|
|
})
|
|
} else {
|
|
vec![]
|
|
};
|
|
|
|
if target_arns.len() != 1 {
|
|
error!(
|
|
"replication resync failed for {} - arn specified {} is missing in the replication config",
|
|
opts.bucket, opts.arn
|
|
);
|
|
self.resync_bucket_mark_status(ResyncStatusType::ResyncFailed, opts.clone(), storage.clone())
|
|
.await;
|
|
return;
|
|
}
|
|
|
|
let Some(target_client) = BucketTargetSys::get()
|
|
.get_remote_target_client(&opts.bucket, &target_arns[0])
|
|
.await
|
|
else {
|
|
error!(
|
|
"replication resync failed for {} - arn specified {} is missing in the bucket targets",
|
|
opts.bucket, opts.arn
|
|
);
|
|
self.resync_bucket_mark_status(ResyncStatusType::ResyncFailed, opts.clone(), storage.clone())
|
|
.await;
|
|
return;
|
|
};
|
|
|
|
if !heal
|
|
&& let Err(e) = self
|
|
.mark_status(ResyncStatusType::ResyncStarted, opts.clone(), storage.clone())
|
|
.await
|
|
{
|
|
error!("Failed to mark resync status: {}", e);
|
|
}
|
|
|
|
let (tx, mut rx) = tokio::sync::mpsc::channel(100);
|
|
|
|
if let Err(err) = storage
|
|
.clone()
|
|
.walk(cancellation_token.clone(), &opts.bucket, "", tx.clone(), WalkOptions::default())
|
|
.await
|
|
{
|
|
error!("Failed to walk bucket {}: {}", opts.bucket, err);
|
|
self.resync_bucket_mark_status(ResyncStatusType::ResyncFailed, opts.clone(), storage.clone())
|
|
.await;
|
|
return;
|
|
}
|
|
|
|
let status = {
|
|
self.status_map
|
|
.read()
|
|
.await
|
|
.get(&opts.bucket)
|
|
.and_then(|status| status.targets_map.get(&opts.arn))
|
|
.cloned()
|
|
.unwrap_or_default()
|
|
};
|
|
|
|
let mut last_checkpoint = if status.resync_status == ResyncStatusType::ResyncStarted
|
|
|| status.resync_status == ResyncStatusType::ResyncFailed
|
|
{
|
|
Some(status.object)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let mut worker_txs = Vec::new();
|
|
let (results_tx, mut results_rx) = tokio::sync::broadcast::channel::<TargetReplicationResyncStatus>(1);
|
|
|
|
let opts_clone = opts.clone();
|
|
let self_clone = self.clone();
|
|
|
|
let mut futures = Vec::new();
|
|
|
|
let results_fut = tokio::spawn(async move {
|
|
while let Ok(st) = results_rx.recv().await {
|
|
self_clone.inc_stats(&st, opts_clone.clone()).await;
|
|
}
|
|
});
|
|
|
|
futures.push(results_fut);
|
|
|
|
for _ in 0..RESYNC_WORKER_COUNT {
|
|
let (tx, mut rx) = tokio::sync::mpsc::channel::<ReplicateObjectInfo>(100);
|
|
worker_txs.push(tx);
|
|
|
|
let cancel_token = cancellation_token.clone();
|
|
let target_client = target_client.clone();
|
|
let resync_cancel_rx = self.resync_cancel_rx.clone();
|
|
let storage = storage.clone();
|
|
let results_tx = results_tx.clone();
|
|
let bucket_name = opts.bucket.clone();
|
|
|
|
let f = tokio::spawn(async move {
|
|
while let Some(mut roi) = rx.recv().await {
|
|
if cancel_token.is_cancelled() {
|
|
return;
|
|
}
|
|
|
|
if roi.delete_marker || !roi.version_purge_status.is_empty() {
|
|
let (version_id, dm_version_id) = if roi.version_purge_status.is_empty() {
|
|
(None, roi.version_id)
|
|
} else {
|
|
(roi.version_id, None)
|
|
};
|
|
|
|
let doi = DeletedObjectReplicationInfo {
|
|
delete_object: DeletedObject {
|
|
object_name: roi.name.clone(),
|
|
delete_marker_version_id: dm_version_id,
|
|
version_id,
|
|
replication_state: roi.replication_state.clone(),
|
|
delete_marker: roi.delete_marker,
|
|
delete_marker_mtime: roi.mod_time,
|
|
..Default::default()
|
|
},
|
|
bucket: roi.bucket.clone(),
|
|
event_type: REPLICATE_EXISTING_DELETE.to_string(),
|
|
op_type: ReplicationType::ExistingObject,
|
|
..Default::default()
|
|
};
|
|
replicate_delete(doi, storage.clone()).await;
|
|
} else {
|
|
roi.op_type = ReplicationType::ExistingObject;
|
|
roi.event_type = REPLICATE_EXISTING.to_string();
|
|
replicate_object(roi.clone(), storage.clone()).await;
|
|
}
|
|
|
|
let mut st = TargetReplicationResyncStatus {
|
|
object: roi.name.clone(),
|
|
bucket: roi.bucket.clone(),
|
|
..Default::default()
|
|
};
|
|
|
|
let reset_id = target_client.reset_id.clone();
|
|
|
|
let (size, err) = if let Err(err) = target_client
|
|
.head_object(&target_client.bucket, &roi.name, roi.version_id.map(|v| v.to_string()))
|
|
.await
|
|
{
|
|
if roi.delete_marker {
|
|
st.replicated_count += 1;
|
|
} else {
|
|
st.failed_count += 1;
|
|
}
|
|
(0, Some(err))
|
|
} else {
|
|
st.replicated_count += 1;
|
|
st.replicated_size += roi.size;
|
|
(roi.size, None)
|
|
};
|
|
|
|
info!(
|
|
"resynced reset_id:{} object: {}/{}-{} size:{} err:{:?}",
|
|
reset_id,
|
|
bucket_name,
|
|
roi.name,
|
|
roi.version_id.unwrap_or_default(),
|
|
size,
|
|
err,
|
|
);
|
|
|
|
if resync_cancel_rx.is_cancelled() {
|
|
return;
|
|
}
|
|
|
|
if cancel_token.is_cancelled() {
|
|
return;
|
|
}
|
|
|
|
if let Err(err) = results_tx.send(st) {
|
|
error!("Failed to send resync status: {}", err);
|
|
}
|
|
}
|
|
});
|
|
|
|
futures.push(f);
|
|
}
|
|
|
|
let resync_cancel_rx = self.resync_cancel_rx.clone();
|
|
|
|
while let Some(res) = rx.recv().await {
|
|
if let Some(err) = res.err {
|
|
error!("Failed to get object info: {}", err);
|
|
self.resync_bucket_mark_status(ResyncStatusType::ResyncFailed, opts.clone(), storage.clone())
|
|
.await;
|
|
return;
|
|
}
|
|
|
|
if resync_cancel_rx.is_cancelled() {
|
|
self.resync_bucket_mark_status(ResyncStatusType::ResyncCanceled, opts.clone(), storage.clone())
|
|
.await;
|
|
return;
|
|
}
|
|
|
|
if cancellation_token.is_cancelled() {
|
|
self.resync_bucket_mark_status(ResyncStatusType::ResyncFailed, opts.clone(), storage.clone())
|
|
.await;
|
|
return;
|
|
}
|
|
|
|
let Some(object) = res.item else {
|
|
continue;
|
|
};
|
|
|
|
if heal
|
|
&& let Some(checkpoint) = &last_checkpoint
|
|
&& &object.name != checkpoint
|
|
{
|
|
continue;
|
|
}
|
|
last_checkpoint = None;
|
|
|
|
let roi = get_heal_replicate_object_info(&object, &rcfg).await;
|
|
if !roi.existing_obj_resync.must_resync() {
|
|
continue;
|
|
}
|
|
|
|
if resync_cancel_rx.is_cancelled() {
|
|
self.resync_bucket_mark_status(ResyncStatusType::ResyncCanceled, opts.clone(), storage.clone())
|
|
.await;
|
|
return;
|
|
}
|
|
|
|
if cancellation_token.is_cancelled() {
|
|
self.resync_bucket_mark_status(ResyncStatusType::ResyncFailed, opts.clone(), storage.clone())
|
|
.await;
|
|
return;
|
|
}
|
|
|
|
let worker_idx = sip_hash(&roi.name, RESYNC_WORKER_COUNT, &DEFAULT_SIP_HASH_KEY) as usize;
|
|
|
|
if let Err(err) = worker_txs[worker_idx].send(roi).await {
|
|
error!("Failed to send object info to worker: {}", err);
|
|
self.resync_bucket_mark_status(ResyncStatusType::ResyncFailed, opts.clone(), storage.clone())
|
|
.await;
|
|
return;
|
|
}
|
|
}
|
|
|
|
for worker_tx in worker_txs {
|
|
drop(worker_tx);
|
|
}
|
|
|
|
join_all(futures).await;
|
|
|
|
self.resync_bucket_mark_status(ResyncStatusType::ResyncCompleted, opts.clone(), storage.clone())
|
|
.await;
|
|
}
|
|
}
|
|
|
|
pub async fn get_heal_replicate_object_info(oi: &ObjectInfo, rcfg: &ReplicationConfig) -> ReplicateObjectInfo {
|
|
let mut oi = oi.clone();
|
|
let mut user_defined = oi.user_defined.clone();
|
|
|
|
if let Some(rc) = rcfg.config.as_ref()
|
|
&& !rc.role.is_empty()
|
|
{
|
|
if !oi.replication_status.is_empty() {
|
|
oi.replication_status_internal = Some(format!("{}={};", rc.role, oi.replication_status.as_str()));
|
|
}
|
|
|
|
if !oi.replication_status.is_empty() {
|
|
oi.replication_status_internal = Some(format!("{}={};", rc.role, oi.replication_status.as_str()));
|
|
}
|
|
|
|
let keys_to_update: Vec<_> = user_defined
|
|
.iter()
|
|
.filter(|(k, _)| k.eq_ignore_ascii_case(format!("{RESERVED_METADATA_PREFIX_LOWER}{REPLICATION_RESET}").as_str()))
|
|
.map(|(k, v)| (k.clone(), v.clone()))
|
|
.collect();
|
|
|
|
for (k, v) in keys_to_update {
|
|
user_defined.remove(&k);
|
|
user_defined.insert(target_reset_header(rc.role.as_str()), v);
|
|
}
|
|
}
|
|
|
|
let dsc = if oi.delete_marker || !oi.replication_status.is_empty() {
|
|
check_replicate_delete(
|
|
oi.bucket.as_str(),
|
|
&ObjectToDelete {
|
|
object_name: oi.name.clone(),
|
|
version_id: oi.version_id,
|
|
..Default::default()
|
|
},
|
|
&oi,
|
|
&ObjectOptions {
|
|
versioned: BucketVersioningSys::prefix_enabled(&oi.bucket, &oi.name).await,
|
|
version_suspended: BucketVersioningSys::prefix_suspended(&oi.bucket, &oi.name).await,
|
|
..Default::default()
|
|
},
|
|
None,
|
|
)
|
|
.await
|
|
} else {
|
|
must_replicate(
|
|
oi.bucket.as_str(),
|
|
&oi.name,
|
|
MustReplicateOptions::new(
|
|
&user_defined,
|
|
oi.user_tags.clone(),
|
|
ReplicationStatusType::Empty,
|
|
ReplicationType::Heal,
|
|
ObjectOptions::default(),
|
|
),
|
|
)
|
|
.await
|
|
};
|
|
|
|
let target_statuses = replication_statuses_map(&oi.replication_status_internal.clone().unwrap_or_default());
|
|
let target_purge_statuses = version_purge_statuses_map(&oi.version_purge_status_internal.clone().unwrap_or_default());
|
|
let existing_obj_resync = rcfg.resync(oi.clone(), dsc.clone(), &target_statuses).await;
|
|
let mut replication_state = oi.replication_state();
|
|
replication_state.replicate_decision_str = dsc.to_string();
|
|
let actual_size = oi.get_actual_size().unwrap_or_default();
|
|
|
|
ReplicateObjectInfo {
|
|
name: oi.name.clone(),
|
|
size: oi.size,
|
|
actual_size,
|
|
bucket: oi.bucket.clone(),
|
|
version_id: oi.version_id,
|
|
etag: oi.etag.clone(),
|
|
mod_time: oi.mod_time,
|
|
replication_status: oi.replication_status,
|
|
replication_status_internal: oi.replication_status_internal.clone(),
|
|
delete_marker: oi.delete_marker,
|
|
version_purge_status_internal: oi.version_purge_status_internal.clone(),
|
|
version_purge_status: oi.version_purge_status,
|
|
replication_state: Some(replication_state),
|
|
op_type: ReplicationType::Heal,
|
|
event_type: "".to_string(),
|
|
dsc,
|
|
existing_obj_resync,
|
|
target_statuses,
|
|
target_purge_statuses,
|
|
replication_timestamp: None,
|
|
ssec: false, // TODO: add ssec support
|
|
user_tags: oi.user_tags.clone(),
|
|
checksum: oi.checksum.clone(),
|
|
retry_count: 0,
|
|
}
|
|
}
|
|
|
|
async fn save_resync_status<S: StorageAPI>(bucket: &str, status: &BucketReplicationResyncStatus, api: Arc<S>) -> Result<()> {
|
|
let buf = status.marshal_msg()?;
|
|
|
|
let mut data = Vec::new();
|
|
|
|
let mut major = [0u8; 2];
|
|
byteorder::LittleEndian::write_u16(&mut major, RESYNC_META_FORMAT);
|
|
data.extend_from_slice(&major);
|
|
|
|
let mut minor = [0u8; 2];
|
|
byteorder::LittleEndian::write_u16(&mut minor, RESYNC_META_VERSION);
|
|
data.extend_from_slice(&minor);
|
|
|
|
data.extend_from_slice(&buf);
|
|
|
|
let config_file = path_join_buf(&[BUCKET_META_PREFIX, bucket, REPLICATION_DIR, RESYNC_FILE_NAME]);
|
|
save_config(api, &config_file, data).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn get_replication_config(bucket: &str) -> Result<Option<ReplicationConfiguration>> {
|
|
let config = match metadata_sys::get_replication_config(bucket).await {
|
|
Ok((config, _)) => Some(config),
|
|
Err(err) => {
|
|
if err != Error::ConfigNotFound {
|
|
return Err(err);
|
|
}
|
|
None
|
|
}
|
|
};
|
|
Ok(config)
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct DeletedObjectReplicationInfo {
|
|
pub delete_object: DeletedObject,
|
|
pub bucket: String,
|
|
pub event_type: String,
|
|
pub op_type: ReplicationType,
|
|
pub reset_id: String,
|
|
pub target_arn: String,
|
|
}
|
|
|
|
impl ReplicationWorkerOperation for DeletedObjectReplicationInfo {
|
|
fn as_any(&self) -> &dyn Any {
|
|
self
|
|
}
|
|
|
|
fn to_mrf_entry(&self) -> MrfReplicateEntry {
|
|
MrfReplicateEntry {
|
|
bucket: self.bucket.clone(),
|
|
object: self.delete_object.object_name.clone(),
|
|
version_id: None,
|
|
retry_count: 0,
|
|
size: 0,
|
|
}
|
|
}
|
|
|
|
fn get_bucket(&self) -> &str {
|
|
&self.bucket
|
|
}
|
|
|
|
fn get_object(&self) -> &str {
|
|
&self.delete_object.object_name
|
|
}
|
|
|
|
fn get_size(&self) -> i64 {
|
|
0
|
|
}
|
|
|
|
fn is_delete_marker(&self) -> bool {
|
|
true
|
|
}
|
|
|
|
fn get_op_type(&self) -> ReplicationType {
|
|
self.op_type
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
pub struct ReplicationConfig {
|
|
pub config: Option<ReplicationConfiguration>,
|
|
pub remotes: Option<BucketTargets>,
|
|
}
|
|
|
|
impl ReplicationConfig {
|
|
pub fn new(config: Option<ReplicationConfiguration>, remotes: Option<BucketTargets>) -> Self {
|
|
Self { config, remotes }
|
|
}
|
|
|
|
pub fn is_empty(&self) -> bool {
|
|
self.config.is_none()
|
|
}
|
|
|
|
pub fn replicate(&self, obj: &ObjectOpts) -> bool {
|
|
self.config.as_ref().is_some_and(|config| config.replicate(obj))
|
|
}
|
|
|
|
pub async fn resync(
|
|
&self,
|
|
oi: ObjectInfo,
|
|
dsc: ReplicateDecision,
|
|
status: &HashMap<String, ReplicationStatusType>,
|
|
) -> ResyncDecision {
|
|
if self.is_empty() {
|
|
return ResyncDecision::default();
|
|
}
|
|
|
|
let mut dsc = dsc;
|
|
|
|
if oi.delete_marker {
|
|
let opts = ObjectOpts {
|
|
name: oi.name.clone(),
|
|
version_id: oi.version_id,
|
|
delete_marker: true,
|
|
op_type: ReplicationType::Delete,
|
|
existing_object: true,
|
|
..Default::default()
|
|
};
|
|
let arns = self
|
|
.config
|
|
.as_ref()
|
|
.map(|config| config.filter_target_arns(&opts))
|
|
.unwrap_or_default();
|
|
|
|
if arns.is_empty() {
|
|
return ResyncDecision::default();
|
|
}
|
|
|
|
for arn in arns {
|
|
let mut opts = opts.clone();
|
|
opts.target_arn = arn;
|
|
|
|
dsc.set(ReplicateTargetDecision::new(opts.target_arn.clone(), self.replicate(&opts), false));
|
|
}
|
|
|
|
return self.resync_internal(oi, dsc, status);
|
|
}
|
|
|
|
let mut user_defined = oi.user_defined.clone();
|
|
user_defined.remove(AMZ_BUCKET_REPLICATION_STATUS);
|
|
|
|
let dsc = must_replicate(
|
|
oi.bucket.as_str(),
|
|
&oi.name,
|
|
MustReplicateOptions::new(
|
|
&user_defined,
|
|
oi.user_tags.clone(),
|
|
ReplicationStatusType::Empty,
|
|
ReplicationType::ExistingObject,
|
|
ObjectOptions::default(),
|
|
),
|
|
)
|
|
.await;
|
|
|
|
self.resync_internal(oi, dsc, status)
|
|
}
|
|
|
|
fn resync_internal(
|
|
&self,
|
|
oi: ObjectInfo,
|
|
dsc: ReplicateDecision,
|
|
status: &HashMap<String, ReplicationStatusType>,
|
|
) -> ResyncDecision {
|
|
let Some(remotes) = self.remotes.as_ref() else {
|
|
return ResyncDecision::default();
|
|
};
|
|
|
|
if remotes.is_empty() {
|
|
return ResyncDecision::default();
|
|
}
|
|
|
|
let mut resync_decision = ResyncDecision::default();
|
|
|
|
for target in remotes.targets.iter() {
|
|
if let Some(decision) = dsc.targets_map.get(&target.arn)
|
|
&& decision.replicate
|
|
{
|
|
resync_decision.targets.insert(
|
|
decision.arn.clone(),
|
|
resync_target(
|
|
&oi,
|
|
&target.arn,
|
|
&target.reset_id,
|
|
target.reset_before_date,
|
|
status.get(&decision.arn).unwrap_or(&ReplicationStatusType::Empty).clone(),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
resync_decision
|
|
}
|
|
}
|
|
|
|
pub fn resync_target(
|
|
oi: &ObjectInfo,
|
|
arn: &str,
|
|
reset_id: &str,
|
|
reset_before_date: Option<OffsetDateTime>,
|
|
status: ReplicationStatusType,
|
|
) -> ResyncTargetDecision {
|
|
let rs = oi
|
|
.user_defined
|
|
.get(target_reset_header(arn).as_str())
|
|
.or(oi.user_defined.get(RUSTFS_REPLICATION_RESET_STATUS))
|
|
.map(|s| s.to_string());
|
|
|
|
let mut dec = ResyncTargetDecision::default();
|
|
|
|
let mod_time = oi.mod_time.unwrap_or(OffsetDateTime::UNIX_EPOCH);
|
|
|
|
if rs.is_none() {
|
|
let reset_before_date = reset_before_date.unwrap_or(OffsetDateTime::UNIX_EPOCH);
|
|
if !reset_id.is_empty() && mod_time < reset_before_date {
|
|
dec.replicate = true;
|
|
return dec;
|
|
}
|
|
|
|
dec.replicate = status == ReplicationStatusType::Empty;
|
|
|
|
return dec;
|
|
}
|
|
|
|
if reset_id.is_empty() || reset_before_date.is_none() {
|
|
return dec;
|
|
}
|
|
|
|
let rs = rs.unwrap();
|
|
let reset_before_date = reset_before_date.unwrap();
|
|
|
|
let parts: Vec<&str> = rs.splitn(2, ';').collect();
|
|
|
|
if parts.len() != 2 {
|
|
return dec;
|
|
}
|
|
|
|
let new_reset = parts[0] == reset_id;
|
|
|
|
if !new_reset && status == ReplicationStatusType::Completed {
|
|
return dec;
|
|
}
|
|
|
|
dec.replicate = new_reset && mod_time < reset_before_date;
|
|
|
|
dec
|
|
}
|
|
|
|
pub struct MustReplicateOptions {
|
|
meta: HashMap<String, String>,
|
|
status: ReplicationStatusType,
|
|
op_type: ReplicationType,
|
|
replication_request: bool,
|
|
}
|
|
|
|
impl MustReplicateOptions {
|
|
pub fn new(
|
|
meta: &HashMap<String, String>,
|
|
user_tags: String,
|
|
status: ReplicationStatusType,
|
|
op_type: ReplicationType,
|
|
opts: ObjectOptions,
|
|
) -> Self {
|
|
let mut meta = meta.clone();
|
|
if !user_tags.is_empty() {
|
|
meta.insert(AMZ_OBJECT_TAGGING.to_string(), user_tags);
|
|
}
|
|
|
|
Self {
|
|
meta,
|
|
status,
|
|
op_type,
|
|
replication_request: opts.replication_request,
|
|
}
|
|
}
|
|
|
|
pub fn from_object_info(oi: &ObjectInfo, op_type: ReplicationType, opts: ObjectOptions) -> Self {
|
|
Self::new(&oi.user_defined, oi.user_tags.clone(), oi.replication_status.clone(), op_type, opts)
|
|
}
|
|
|
|
pub fn replication_status(&self) -> ReplicationStatusType {
|
|
if let Some(rs) = self.meta.get(AMZ_BUCKET_REPLICATION_STATUS) {
|
|
return ReplicationStatusType::from(rs.as_str());
|
|
}
|
|
ReplicationStatusType::default()
|
|
}
|
|
|
|
pub fn is_existing_object_replication(&self) -> bool {
|
|
self.op_type == ReplicationType::ExistingObject
|
|
}
|
|
|
|
pub fn is_metadata_replication(&self) -> bool {
|
|
self.op_type == ReplicationType::Metadata
|
|
}
|
|
}
|
|
|
|
pub fn get_must_replicate_options(
|
|
user_defined: &HashMap<String, String>,
|
|
user_tags: String,
|
|
status: ReplicationStatusType,
|
|
op_type: ReplicationType,
|
|
opts: ObjectOptions,
|
|
) -> MustReplicateOptions {
|
|
MustReplicateOptions::new(user_defined, user_tags, status, op_type, opts)
|
|
}
|
|
|
|
/// Returns whether object version is a delete marker and if object qualifies for replication
|
|
pub async fn check_replicate_delete(
|
|
bucket: &str,
|
|
dobj: &ObjectToDelete,
|
|
oi: &ObjectInfo,
|
|
del_opts: &ObjectOptions,
|
|
gerr: Option<String>,
|
|
) -> ReplicateDecision {
|
|
let rcfg = match get_replication_config(bucket).await {
|
|
Ok(Some(config)) => config,
|
|
Ok(None) => {
|
|
// warn!("No replication config found for bucket: {}", bucket);
|
|
return ReplicateDecision::default();
|
|
}
|
|
Err(err) => {
|
|
error!("Failed to get replication config for bucket {}: {}", bucket, err);
|
|
return ReplicateDecision::default();
|
|
}
|
|
};
|
|
|
|
// If incoming request is a replication request, it does not need to be re-replicated.
|
|
if del_opts.replication_request {
|
|
return ReplicateDecision::default();
|
|
}
|
|
|
|
// Skip replication if this object's prefix is excluded from being versioned.
|
|
if !del_opts.versioned {
|
|
return ReplicateDecision::default();
|
|
}
|
|
|
|
let opts = ObjectOpts {
|
|
name: dobj.object_name.clone(),
|
|
ssec: is_ssec_encrypted(&oi.user_defined),
|
|
user_tags: oi.user_tags.clone(),
|
|
delete_marker: oi.delete_marker,
|
|
version_id: dobj.version_id,
|
|
op_type: ReplicationType::Delete,
|
|
..Default::default()
|
|
};
|
|
|
|
let tgt_arns = rcfg.filter_target_arns(&opts);
|
|
let mut dsc = ReplicateDecision::new();
|
|
|
|
if tgt_arns.is_empty() {
|
|
return dsc;
|
|
}
|
|
|
|
for tgt_arn in tgt_arns {
|
|
let mut opts = opts.clone();
|
|
opts.target_arn = tgt_arn.clone();
|
|
let replicate = rcfg.replicate(&opts);
|
|
let sync = false; // Default sync value
|
|
|
|
// When incoming delete is removal of a delete marker (a.k.a versioned delete),
|
|
// GetObjectInfo returns extra information even though it returns errFileNotFound
|
|
if gerr.is_some() {
|
|
let valid_repl_status = matches!(
|
|
oi.target_replication_status(&tgt_arn),
|
|
ReplicationStatusType::Pending | ReplicationStatusType::Completed | ReplicationStatusType::Failed
|
|
);
|
|
|
|
if oi.delete_marker && (valid_repl_status || replicate) {
|
|
dsc.set(ReplicateTargetDecision::new(tgt_arn, replicate, sync));
|
|
continue;
|
|
}
|
|
|
|
// Can be the case that other cluster is down and duplicate `mc rm --vid`
|
|
// is issued - this still needs to be replicated back to the other target
|
|
if oi.version_purge_status != VersionPurgeStatusType::default() {
|
|
let replicate = oi.version_purge_status == VersionPurgeStatusType::Pending
|
|
|| oi.version_purge_status == VersionPurgeStatusType::Failed;
|
|
dsc.set(ReplicateTargetDecision::new(tgt_arn, replicate, sync));
|
|
}
|
|
continue;
|
|
}
|
|
|
|
let tgt = BucketTargetSys::get().get_remote_target_client(bucket, &tgt_arn).await;
|
|
// The target online status should not be used here while deciding
|
|
// whether to replicate deletes as the target could be temporarily down
|
|
let tgt_dsc = if let Some(tgt) = tgt {
|
|
ReplicateTargetDecision::new(tgt_arn, replicate, tgt.replicate_sync)
|
|
} else {
|
|
ReplicateTargetDecision::new(tgt_arn, false, false)
|
|
};
|
|
dsc.set(tgt_dsc);
|
|
}
|
|
|
|
dsc
|
|
}
|
|
|
|
/// Check if the user-defined metadata contains SSEC encryption headers
|
|
fn is_ssec_encrypted(user_defined: &std::collections::HashMap<String, String>) -> bool {
|
|
user_defined.contains_key(SSEC_ALGORITHM_HEADER)
|
|
|| user_defined.contains_key(SSEC_KEY_HEADER)
|
|
|| user_defined.contains_key(SSEC_KEY_MD5_HEADER)
|
|
}
|
|
|
|
/// Extension trait for ObjectInfo to add replication-related methods
|
|
pub trait ObjectInfoExt {
|
|
fn target_replication_status(&self, arn: &str) -> ReplicationStatusType;
|
|
fn replication_state(&self) -> ReplicationState;
|
|
}
|
|
|
|
impl ObjectInfoExt for ObjectInfo {
|
|
/// Returns replication status of a target
|
|
fn target_replication_status(&self, arn: &str) -> ReplicationStatusType {
|
|
lazy_static::lazy_static! {
|
|
static ref REPL_STATUS_REGEX: Regex = Regex::new(r"([^=].*?)=([^,].*?);").unwrap();
|
|
}
|
|
|
|
let binding = self.replication_status_internal.clone().unwrap_or_default();
|
|
let captures = REPL_STATUS_REGEX.captures_iter(&binding);
|
|
for cap in captures {
|
|
if cap.len() == 3 && &cap[1] == arn {
|
|
return ReplicationStatusType::from(&cap[2]);
|
|
}
|
|
}
|
|
ReplicationStatusType::default()
|
|
}
|
|
|
|
fn replication_state(&self) -> ReplicationState {
|
|
ReplicationState {
|
|
replication_status_internal: self.replication_status_internal.clone(),
|
|
version_purge_status_internal: self.version_purge_status_internal.clone(),
|
|
replicate_decision_str: self.replication_decision.clone(),
|
|
targets: replication_statuses_map(&self.replication_status_internal.clone().unwrap_or_default()),
|
|
purge_targets: version_purge_statuses_map(&self.version_purge_status_internal.clone().unwrap_or_default()),
|
|
reset_statuses_map: self
|
|
.user_defined
|
|
.iter()
|
|
.filter_map(|(k, v)| {
|
|
if k.starts_with(&format!("{RESERVED_METADATA_PREFIX_LOWER}-{REPLICATION_RESET}")) {
|
|
Some((
|
|
k.trim_start_matches(&format!("{RESERVED_METADATA_PREFIX_LOWER}-{REPLICATION_RESET}"))
|
|
.to_string(),
|
|
v.clone(),
|
|
))
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect(),
|
|
..Default::default()
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn must_replicate(bucket: &str, object: &str, mopts: MustReplicateOptions) -> ReplicateDecision {
|
|
if new_object_layer_fn().is_none() {
|
|
return ReplicateDecision::default();
|
|
}
|
|
|
|
if !BucketVersioningSys::prefix_enabled(bucket, object).await {
|
|
return ReplicateDecision::default();
|
|
}
|
|
|
|
let replication_status = mopts.replication_status();
|
|
|
|
if replication_status == ReplicationStatusType::Replica && !mopts.is_metadata_replication() {
|
|
return ReplicateDecision::default();
|
|
}
|
|
|
|
if mopts.replication_request {
|
|
return ReplicateDecision::default();
|
|
}
|
|
|
|
let cfg = match get_replication_config(bucket).await {
|
|
Ok(cfg) => {
|
|
if let Some(cfg) = cfg {
|
|
cfg
|
|
} else {
|
|
return ReplicateDecision::default();
|
|
}
|
|
}
|
|
Err(_err) => {
|
|
return ReplicateDecision::default();
|
|
}
|
|
};
|
|
|
|
let opts = ObjectOpts {
|
|
name: object.to_string(),
|
|
replica: replication_status == ReplicationStatusType::Replica,
|
|
existing_object: mopts.is_existing_object_replication(),
|
|
user_tags: mopts.meta.get(AMZ_OBJECT_TAGGING).map(|s| s.to_string()).unwrap_or_default(),
|
|
..Default::default()
|
|
};
|
|
|
|
let arns = cfg.filter_target_arns(&opts);
|
|
|
|
if arns.is_empty() {
|
|
return ReplicateDecision::default();
|
|
}
|
|
|
|
let mut dsc = ReplicateDecision::default();
|
|
|
|
for arn in arns {
|
|
let cli = BucketTargetSys::get().get_remote_target_client(bucket, &arn).await;
|
|
|
|
let mut sopts = opts.clone();
|
|
sopts.target_arn = arn.clone();
|
|
|
|
let replicate = cfg.replicate(&sopts);
|
|
let synchronous = if let Some(cli) = cli { cli.replicate_sync } else { false };
|
|
|
|
dsc.set(ReplicateTargetDecision::new(arn, replicate, synchronous));
|
|
}
|
|
|
|
dsc
|
|
}
|
|
|
|
pub async fn replicate_delete<S: StorageAPI>(dobj: DeletedObjectReplicationInfo, storage: Arc<S>) {
|
|
if dobj.delete_object.force_delete {
|
|
replicate_force_delete_to_targets(&dobj, storage).await;
|
|
return;
|
|
}
|
|
|
|
let bucket = dobj.bucket.clone();
|
|
let version_id = if let Some(version_id) = &dobj.delete_object.delete_marker_version_id {
|
|
Some(version_id.to_owned())
|
|
} else {
|
|
dobj.delete_object.version_id
|
|
};
|
|
|
|
let _rcfg = match get_replication_config(&bucket).await {
|
|
Ok(Some(config)) => config,
|
|
Ok(None) => {
|
|
warn!("No replication config found for bucket: {}", bucket);
|
|
send_event(EventArgs {
|
|
event_name: EventName::ObjectReplicationNotTracked.as_ref().to_string(),
|
|
bucket_name: bucket.clone(),
|
|
object: ObjectInfo {
|
|
bucket: bucket.clone(),
|
|
name: dobj.delete_object.object_name.clone(),
|
|
version_id,
|
|
delete_marker: dobj.delete_object.delete_marker,
|
|
..Default::default()
|
|
},
|
|
user_agent: "Internal: [Replication]".to_string(),
|
|
host: GLOBAL_LocalNodeName.to_string(),
|
|
..Default::default()
|
|
});
|
|
|
|
return;
|
|
}
|
|
Err(err) => {
|
|
warn!("replication config for bucket: {} error: {}", bucket, err);
|
|
send_event(EventArgs {
|
|
event_name: EventName::ObjectReplicationNotTracked.as_ref().to_string(),
|
|
bucket_name: bucket.clone(),
|
|
object: ObjectInfo {
|
|
bucket: bucket.clone(),
|
|
name: dobj.delete_object.object_name.clone(),
|
|
version_id,
|
|
delete_marker: dobj.delete_object.delete_marker,
|
|
..Default::default()
|
|
},
|
|
user_agent: "Internal: [Replication]".to_string(),
|
|
host: GLOBAL_LocalNodeName.to_string(),
|
|
..Default::default()
|
|
});
|
|
return;
|
|
}
|
|
};
|
|
|
|
let dsc = match parse_replicate_decision(
|
|
&bucket,
|
|
&dobj
|
|
.delete_object
|
|
.replication_state
|
|
.as_ref()
|
|
.map(|v| v.replicate_decision_str.clone())
|
|
.unwrap_or_default(),
|
|
) {
|
|
Ok(dsc) => dsc,
|
|
Err(err) => {
|
|
warn!(
|
|
"failed to parse replicate decision for bucket:{} arn:{} error:{}",
|
|
bucket, dobj.target_arn, err
|
|
);
|
|
send_event(EventArgs {
|
|
event_name: EventName::ObjectReplicationNotTracked.as_ref().to_string(),
|
|
bucket_name: bucket.clone(),
|
|
object: ObjectInfo {
|
|
bucket: bucket.clone(),
|
|
name: dobj.delete_object.object_name.clone(),
|
|
version_id,
|
|
delete_marker: dobj.delete_object.delete_marker,
|
|
..Default::default()
|
|
},
|
|
user_agent: "Internal: [Replication]".to_string(),
|
|
host: GLOBAL_LocalNodeName.to_string(),
|
|
..Default::default()
|
|
});
|
|
return;
|
|
}
|
|
};
|
|
|
|
let ns_lock = match storage
|
|
.new_ns_lock(&bucket, format!("/[replicate]/{}", dobj.delete_object.object_name).as_str())
|
|
.await
|
|
{
|
|
Ok(ns_lock) => ns_lock,
|
|
Err(e) => {
|
|
warn!(
|
|
"failed to get ns lock for bucket:{} object:{} error:{}",
|
|
bucket, dobj.delete_object.object_name, e
|
|
);
|
|
send_event(EventArgs {
|
|
event_name: EventName::ObjectReplicationNotTracked.as_ref().to_string(),
|
|
bucket_name: bucket.clone(),
|
|
object: ObjectInfo {
|
|
bucket: bucket.clone(),
|
|
name: dobj.delete_object.object_name.clone(),
|
|
version_id,
|
|
delete_marker: dobj.delete_object.delete_marker,
|
|
..Default::default()
|
|
},
|
|
user_agent: "Internal: [Replication]".to_string(),
|
|
host: GLOBAL_LocalNodeName.to_string(),
|
|
..Default::default()
|
|
});
|
|
return;
|
|
}
|
|
};
|
|
|
|
let _lock_guard = match ns_lock.get_write_lock(get_lock_acquire_timeout()).await {
|
|
Ok(lock_guard) => lock_guard,
|
|
Err(e) => {
|
|
warn!(
|
|
"failed to get write lock for bucket:{} object:{} error:{}",
|
|
bucket, dobj.delete_object.object_name, e
|
|
);
|
|
send_event(EventArgs {
|
|
event_name: EventName::ObjectReplicationNotTracked.as_ref().to_string(),
|
|
bucket_name: bucket.clone(),
|
|
object: ObjectInfo {
|
|
bucket: bucket.clone(),
|
|
name: dobj.delete_object.object_name.clone(),
|
|
version_id,
|
|
delete_marker: dobj.delete_object.delete_marker,
|
|
..Default::default()
|
|
},
|
|
user_agent: "Internal: [Replication]".to_string(),
|
|
host: GLOBAL_LocalNodeName.to_string(),
|
|
..Default::default()
|
|
});
|
|
return;
|
|
}
|
|
};
|
|
|
|
// Initialize replicated infos
|
|
let mut rinfos = ReplicatedInfos {
|
|
replication_timestamp: Some(OffsetDateTime::now_utc()),
|
|
targets: Vec::with_capacity(dsc.targets_map.len()),
|
|
};
|
|
|
|
let mut join_set = JoinSet::new();
|
|
|
|
// Process each target
|
|
for (_, tgt_entry) in dsc.targets_map.iter() {
|
|
// Skip targets that should not be replicated
|
|
if !tgt_entry.replicate {
|
|
continue;
|
|
}
|
|
|
|
// If dobj.TargetArn is not empty string, this is a case of specific target being re-synced.
|
|
if !dobj.target_arn.is_empty() && dobj.target_arn != tgt_entry.arn {
|
|
continue;
|
|
}
|
|
|
|
// Get the remote target client
|
|
let Some(tgt_client) = BucketTargetSys::get().get_remote_target_client(&bucket, &tgt_entry.arn).await else {
|
|
warn!("failed to get target for bucket:{:?}, arn:{:?}", &bucket, &tgt_entry.arn);
|
|
send_event(EventArgs {
|
|
event_name: EventName::ObjectReplicationNotTracked.as_ref().to_string(),
|
|
bucket_name: bucket.clone(),
|
|
object: ObjectInfo {
|
|
bucket: bucket.clone(),
|
|
name: dobj.delete_object.object_name.clone(),
|
|
version_id,
|
|
delete_marker: dobj.delete_object.delete_marker,
|
|
..Default::default()
|
|
},
|
|
user_agent: "Internal: [Replication]".to_string(),
|
|
host: GLOBAL_LocalNodeName.to_string(),
|
|
..Default::default()
|
|
});
|
|
continue;
|
|
};
|
|
|
|
let dobj_clone = dobj.clone();
|
|
|
|
// Spawn task in the join set
|
|
join_set.spawn(async move { replicate_delete_to_target(&dobj_clone, tgt_client.clone()).await });
|
|
}
|
|
|
|
// Collect all results
|
|
while let Some(result) = join_set.join_next().await {
|
|
match result {
|
|
Ok(tgt_info) => {
|
|
rinfos.targets.push(tgt_info);
|
|
}
|
|
Err(e) => {
|
|
error!("replicate_delete task failed: {}", e);
|
|
send_event(EventArgs {
|
|
event_name: EventName::ObjectReplicationNotTracked.as_ref().to_string(),
|
|
bucket_name: bucket.clone(),
|
|
object: ObjectInfo {
|
|
bucket: bucket.clone(),
|
|
name: dobj.delete_object.object_name.clone(),
|
|
version_id,
|
|
delete_marker: dobj.delete_object.delete_marker,
|
|
..Default::default()
|
|
},
|
|
..Default::default()
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
let (replication_status, prev_status) = if dobj.delete_object.version_id.is_none() {
|
|
(
|
|
rinfos.replication_status(),
|
|
dobj.delete_object
|
|
.replication_state
|
|
.as_ref()
|
|
.map(|v| v.composite_replication_status())
|
|
.unwrap_or(ReplicationStatusType::Empty),
|
|
)
|
|
} else {
|
|
(
|
|
ReplicationStatusType::from(rinfos.version_purge_status()),
|
|
ReplicationStatusType::from(
|
|
dobj.delete_object
|
|
.replication_state
|
|
.as_ref()
|
|
.map(|v| v.composite_version_purge_status())
|
|
.unwrap_or(VersionPurgeStatusType::Empty),
|
|
),
|
|
)
|
|
};
|
|
|
|
for tgt in rinfos.targets.iter() {
|
|
if tgt.replication_status != tgt.prev_replication_status {
|
|
// TODO: update global replication status
|
|
}
|
|
}
|
|
|
|
let mut drs = get_replication_state(
|
|
&rinfos,
|
|
&dobj.delete_object.replication_state.clone().unwrap_or_default(),
|
|
dobj.delete_object.version_id.map(|v| v.to_string()),
|
|
);
|
|
if replication_status != prev_status {
|
|
drs.replication_timestamp = Some(OffsetDateTime::now_utc());
|
|
}
|
|
|
|
let event_name = if replication_status == ReplicationStatusType::Completed {
|
|
EventName::ObjectReplicationComplete.as_ref().to_string()
|
|
} else {
|
|
EventName::ObjectReplicationFailed.as_ref().to_string()
|
|
};
|
|
|
|
match storage
|
|
.delete_object(
|
|
&bucket,
|
|
&dobj.delete_object.object_name,
|
|
ObjectOptions {
|
|
version_id: version_id.map(|v| v.to_string()),
|
|
mod_time: dobj.delete_object.delete_marker_mtime,
|
|
delete_replication: Some(drs),
|
|
versioned: BucketVersioningSys::prefix_enabled(&bucket, &dobj.delete_object.object_name).await,
|
|
version_suspended: BucketVersioningSys::prefix_suspended(&bucket, &dobj.delete_object.object_name).await,
|
|
..Default::default()
|
|
},
|
|
)
|
|
.await
|
|
{
|
|
Ok(object) => {
|
|
send_event(EventArgs {
|
|
event_name,
|
|
bucket_name: bucket.clone(),
|
|
object,
|
|
..Default::default()
|
|
});
|
|
}
|
|
Err(e) => {
|
|
error!("failed to delete object for bucket:{} arn:{} error:{}", bucket, dobj.target_arn, e);
|
|
send_event(EventArgs {
|
|
event_name,
|
|
bucket_name: bucket.clone(),
|
|
object: ObjectInfo {
|
|
bucket: bucket.clone(),
|
|
name: dobj.delete_object.object_name.clone(),
|
|
version_id,
|
|
delete_marker: dobj.delete_object.delete_marker,
|
|
..Default::default()
|
|
},
|
|
..Default::default()
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn replicate_force_delete_to_targets<S: StorageAPI>(dobj: &DeletedObjectReplicationInfo, storage: Arc<S>) {
|
|
let bucket = &dobj.bucket;
|
|
let object_name = &dobj.delete_object.object_name;
|
|
|
|
let rcfg = match get_replication_config(bucket).await {
|
|
Ok(Some(config)) => config,
|
|
Ok(None) => {
|
|
warn!("replicate force-delete: no replication config for bucket:{}", bucket);
|
|
send_event(EventArgs {
|
|
event_name: EventName::ObjectReplicationNotTracked.as_ref().to_string(),
|
|
bucket_name: bucket.clone(),
|
|
object: ObjectInfo {
|
|
bucket: bucket.clone(),
|
|
name: object_name.clone(),
|
|
..Default::default()
|
|
},
|
|
user_agent: "Internal: [Replication]".to_string(),
|
|
host: GLOBAL_LocalNodeName.to_string(),
|
|
..Default::default()
|
|
});
|
|
return;
|
|
}
|
|
Err(err) => {
|
|
warn!("replicate force-delete: replication config error bucket:{} error:{}", bucket, err);
|
|
send_event(EventArgs {
|
|
event_name: EventName::ObjectReplicationNotTracked.as_ref().to_string(),
|
|
bucket_name: bucket.clone(),
|
|
object: ObjectInfo {
|
|
bucket: bucket.clone(),
|
|
name: object_name.clone(),
|
|
..Default::default()
|
|
},
|
|
user_agent: "Internal: [Replication]".to_string(),
|
|
host: GLOBAL_LocalNodeName.to_string(),
|
|
..Default::default()
|
|
});
|
|
return;
|
|
}
|
|
};
|
|
|
|
let ns_lock = match storage
|
|
.new_ns_lock(bucket, format!("/[replicate]/{}", object_name).as_str())
|
|
.await
|
|
{
|
|
Ok(ns_lock) => ns_lock,
|
|
Err(e) => {
|
|
warn!(
|
|
"replicate force-delete: failed to get ns lock bucket:{} object:{} error:{}",
|
|
bucket, object_name, e
|
|
);
|
|
send_event(EventArgs {
|
|
event_name: EventName::ObjectReplicationNotTracked.as_ref().to_string(),
|
|
bucket_name: bucket.clone(),
|
|
object: ObjectInfo {
|
|
bucket: bucket.clone(),
|
|
name: object_name.clone(),
|
|
..Default::default()
|
|
},
|
|
user_agent: "Internal: [Replication]".to_string(),
|
|
host: GLOBAL_LocalNodeName.to_string(),
|
|
..Default::default()
|
|
});
|
|
return;
|
|
}
|
|
};
|
|
|
|
let _lock_guard = match ns_lock.get_write_lock(get_lock_acquire_timeout()).await {
|
|
Ok(guard) => guard,
|
|
Err(e) => {
|
|
warn!(
|
|
"replicate force-delete: failed to get write lock bucket:{} object:{} error:{}",
|
|
bucket, object_name, e
|
|
);
|
|
send_event(EventArgs {
|
|
event_name: EventName::ObjectReplicationNotTracked.as_ref().to_string(),
|
|
bucket_name: bucket.clone(),
|
|
object: ObjectInfo {
|
|
bucket: bucket.clone(),
|
|
name: object_name.clone(),
|
|
..Default::default()
|
|
},
|
|
user_agent: "Internal: [Replication]".to_string(),
|
|
host: GLOBAL_LocalNodeName.to_string(),
|
|
..Default::default()
|
|
});
|
|
return;
|
|
}
|
|
};
|
|
|
|
let tgt_arns = if !dobj.target_arn.is_empty() {
|
|
vec![dobj.target_arn.clone()]
|
|
} else {
|
|
rcfg.filter_target_arns(&ObjectOpts {
|
|
name: object_name.clone(),
|
|
..Default::default()
|
|
})
|
|
};
|
|
|
|
let mut join_set = JoinSet::new();
|
|
|
|
for arn in tgt_arns {
|
|
let Some(tgt_client) = BucketTargetSys::get().get_remote_target_client(bucket, &arn).await else {
|
|
warn!("replicate force-delete: failed to get target client bucket:{} arn:{}", bucket, arn);
|
|
send_event(EventArgs {
|
|
event_name: EventName::ObjectReplicationNotTracked.as_ref().to_string(),
|
|
bucket_name: bucket.clone(),
|
|
object: ObjectInfo {
|
|
bucket: bucket.clone(),
|
|
name: object_name.clone(),
|
|
..Default::default()
|
|
},
|
|
user_agent: "Internal: [Replication]".to_string(),
|
|
host: GLOBAL_LocalNodeName.to_string(),
|
|
..Default::default()
|
|
});
|
|
continue;
|
|
};
|
|
|
|
let bucket = bucket.clone();
|
|
let object_name = object_name.clone();
|
|
|
|
join_set.spawn(async move {
|
|
if BucketTargetSys::get().is_offline(&tgt_client.to_url()).await {
|
|
error!("replicate force-delete: target offline bucket:{} arn:{}", bucket, tgt_client.arn);
|
|
send_event(EventArgs {
|
|
event_name: EventName::ObjectReplicationFailed.as_ref().to_string(),
|
|
bucket_name: bucket.clone(),
|
|
object: ObjectInfo {
|
|
bucket: bucket.clone(),
|
|
name: object_name.clone(),
|
|
..Default::default()
|
|
},
|
|
user_agent: "Internal: [Replication]".to_string(),
|
|
host: GLOBAL_LocalNodeName.to_string(),
|
|
..Default::default()
|
|
});
|
|
return;
|
|
}
|
|
|
|
if let Err(e) = tgt_client
|
|
.remove_object(
|
|
&tgt_client.bucket,
|
|
&object_name,
|
|
None,
|
|
RemoveObjectOptions {
|
|
force_delete: true,
|
|
governance_bypass: false,
|
|
replication_delete_marker: false,
|
|
replication_mtime: None,
|
|
replication_status: ReplicationStatusType::Replica,
|
|
replication_request: true,
|
|
replication_validity_check: false,
|
|
},
|
|
)
|
|
.await
|
|
{
|
|
error!(
|
|
"replicate force-delete failed bucket:{} object:{} arn:{} error:{}",
|
|
bucket, object_name, tgt_client.arn, e
|
|
);
|
|
send_event(EventArgs {
|
|
event_name: EventName::ObjectReplicationFailed.as_ref().to_string(),
|
|
bucket_name: bucket.clone(),
|
|
object: ObjectInfo {
|
|
bucket: bucket.clone(),
|
|
name: object_name.clone(),
|
|
..Default::default()
|
|
},
|
|
user_agent: "Internal: [Replication]".to_string(),
|
|
host: GLOBAL_LocalNodeName.to_string(),
|
|
..Default::default()
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
while let Some(result) = join_set.join_next().await {
|
|
if let Err(e) = result {
|
|
error!("replicate force-delete task panicked: {}", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn replicate_delete_to_target(dobj: &DeletedObjectReplicationInfo, tgt_client: Arc<TargetClient>) -> ReplicatedTargetInfo {
|
|
let version_id = if let Some(version_id) = &dobj.delete_object.delete_marker_version_id {
|
|
version_id.to_owned()
|
|
} else {
|
|
dobj.delete_object.version_id.unwrap_or_default()
|
|
};
|
|
|
|
let mut rinfo = dobj
|
|
.delete_object
|
|
.replication_state
|
|
.clone()
|
|
.unwrap_or_default()
|
|
.target_state(&tgt_client.arn);
|
|
rinfo.op_type = dobj.op_type;
|
|
rinfo.endpoint = tgt_client.endpoint.clone();
|
|
rinfo.secure = tgt_client.secure;
|
|
|
|
if dobj.delete_object.version_id.is_none()
|
|
&& rinfo.prev_replication_status == ReplicationStatusType::Completed
|
|
&& dobj.op_type != ReplicationType::ExistingObject
|
|
{
|
|
rinfo.replication_status = rinfo.prev_replication_status.clone();
|
|
return rinfo;
|
|
}
|
|
|
|
if dobj.delete_object.version_id.is_some() && rinfo.version_purge_status == VersionPurgeStatusType::Complete {
|
|
return rinfo;
|
|
}
|
|
|
|
if BucketTargetSys::get().is_offline(&tgt_client.to_url()).await {
|
|
if dobj.delete_object.version_id.is_none() {
|
|
rinfo.replication_status = ReplicationStatusType::Failed;
|
|
} else {
|
|
rinfo.version_purge_status = VersionPurgeStatusType::Failed;
|
|
}
|
|
return rinfo;
|
|
}
|
|
|
|
let version_id = if version_id.is_nil() {
|
|
None
|
|
} else {
|
|
Some(version_id.to_string())
|
|
};
|
|
|
|
if dobj.delete_object.delete_marker_version_id.is_some()
|
|
&& let Err(e) = tgt_client
|
|
.head_object(&tgt_client.bucket, &dobj.delete_object.object_name, version_id.clone())
|
|
.await
|
|
&& let SdkError::ServiceError(service_err) = &e
|
|
&& !service_err.err().is_not_found()
|
|
{
|
|
rinfo.replication_status = ReplicationStatusType::Failed;
|
|
rinfo.error = Some(e.to_string());
|
|
|
|
return rinfo;
|
|
};
|
|
|
|
match tgt_client
|
|
.remove_object(
|
|
&tgt_client.bucket,
|
|
&dobj.delete_object.object_name,
|
|
version_id.clone(),
|
|
RemoveObjectOptions {
|
|
force_delete: false,
|
|
governance_bypass: false,
|
|
replication_delete_marker: dobj.delete_object.delete_marker_version_id.is_some(),
|
|
replication_mtime: dobj.delete_object.delete_marker_mtime,
|
|
replication_status: ReplicationStatusType::Replica,
|
|
replication_request: true,
|
|
replication_validity_check: false,
|
|
},
|
|
)
|
|
.await
|
|
{
|
|
Ok(_) => {
|
|
if dobj.delete_object.version_id.is_none() {
|
|
rinfo.replication_status = ReplicationStatusType::Completed;
|
|
} else {
|
|
rinfo.version_purge_status = VersionPurgeStatusType::Complete;
|
|
}
|
|
}
|
|
Err(e) => {
|
|
rinfo.error = Some(e.to_string());
|
|
if dobj.delete_object.version_id.is_none() {
|
|
rinfo.replication_status = ReplicationStatusType::Failed;
|
|
} else {
|
|
rinfo.version_purge_status = VersionPurgeStatusType::Failed;
|
|
}
|
|
// TODO: check offline
|
|
}
|
|
}
|
|
|
|
if rinfo.replication_status == ReplicationStatusType::Completed
|
|
&& !tgt_client.reset_id.is_empty()
|
|
&& dobj.op_type == ReplicationType::ExistingObject
|
|
{
|
|
rinfo.resync_timestamp = format!("{};{}", OffsetDateTime::now_utc().format(&Rfc3339).unwrap(), tgt_client.reset_id);
|
|
}
|
|
|
|
rinfo
|
|
}
|
|
|
|
pub async fn replicate_object<S: StorageAPI>(roi: ReplicateObjectInfo, storage: Arc<S>) {
|
|
let bucket = roi.bucket.clone();
|
|
let object = roi.name.clone();
|
|
|
|
let cfg = match get_replication_config(&bucket).await {
|
|
Ok(Some(config)) => config,
|
|
Ok(None) => {
|
|
warn!("No replication config found for bucket: {}", bucket);
|
|
send_event(EventArgs {
|
|
event_name: EventName::ObjectReplicationNotTracked.as_ref().to_string(),
|
|
bucket_name: bucket.clone(),
|
|
object: roi.to_object_info(),
|
|
host: GLOBAL_LocalNodeName.to_string(),
|
|
user_agent: "Internal: [Replication]".to_string(),
|
|
..Default::default()
|
|
});
|
|
return;
|
|
}
|
|
Err(err) => {
|
|
error!("Failed to get replication config for bucket {}: {}", bucket, err);
|
|
send_event(EventArgs {
|
|
event_name: EventName::ObjectReplicationNotTracked.as_ref().to_string(),
|
|
bucket_name: bucket.clone(),
|
|
object: roi.to_object_info(),
|
|
host: GLOBAL_LocalNodeName.to_string(),
|
|
user_agent: "Internal: [Replication]".to_string(),
|
|
..Default::default()
|
|
});
|
|
return;
|
|
}
|
|
};
|
|
|
|
let tgt_arns = cfg.filter_target_arns(&ObjectOpts {
|
|
name: object.clone(),
|
|
user_tags: roi.user_tags.clone(),
|
|
ssec: roi.ssec,
|
|
..Default::default()
|
|
});
|
|
|
|
// TODO: NSLOCK
|
|
|
|
let mut join_set = JoinSet::new();
|
|
|
|
for arn in tgt_arns {
|
|
let Some(tgt_client) = BucketTargetSys::get().get_remote_target_client(&bucket, &arn).await else {
|
|
warn!("failed to get target for bucket:{:?}, arn:{:?}", &bucket, &arn);
|
|
send_event(EventArgs {
|
|
event_name: EventName::ObjectReplicationNotTracked.as_ref().to_string(),
|
|
bucket_name: bucket.clone(),
|
|
object: roi.to_object_info(),
|
|
host: GLOBAL_LocalNodeName.to_string(),
|
|
user_agent: "Internal: [Replication]".to_string(),
|
|
..Default::default()
|
|
});
|
|
continue;
|
|
};
|
|
|
|
let roi_clone = roi.clone();
|
|
let storage_clone = storage.clone();
|
|
join_set.spawn(async move {
|
|
if roi.op_type == ReplicationType::Object {
|
|
roi_clone.replicate_object(storage_clone, tgt_client).await
|
|
} else {
|
|
roi_clone.replicate_all(storage_clone, tgt_client).await
|
|
}
|
|
});
|
|
}
|
|
|
|
let mut rinfos = ReplicatedInfos {
|
|
replication_timestamp: Some(OffsetDateTime::now_utc()),
|
|
targets: Vec::with_capacity(join_set.len()),
|
|
};
|
|
|
|
while let Some(result) = join_set.join_next().await {
|
|
match result {
|
|
Ok(tgt_info) => {
|
|
rinfos.targets.push(tgt_info);
|
|
}
|
|
Err(e) => {
|
|
error!("replicate_object task failed: {}", e);
|
|
send_event(EventArgs {
|
|
event_name: EventName::ObjectReplicationNotTracked.as_ref().to_string(),
|
|
bucket_name: bucket.clone(),
|
|
object: roi.to_object_info(),
|
|
host: GLOBAL_LocalNodeName.to_string(),
|
|
user_agent: "Internal: [Replication]".to_string(),
|
|
..Default::default()
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
let replication_status = rinfos.replication_status();
|
|
let new_replication_internal = rinfos.replication_status_internal();
|
|
let mut object_info = roi.to_object_info();
|
|
|
|
if roi.replication_status_internal != new_replication_internal || rinfos.replication_resynced() {
|
|
let mut eval_metadata = HashMap::new();
|
|
if let Some(ref s) = new_replication_internal {
|
|
eval_metadata.insert(format!("{RESERVED_METADATA_PREFIX_LOWER}replication-status"), s.clone());
|
|
}
|
|
let popts = ObjectOptions {
|
|
version_id: roi.version_id.map(|v| v.to_string()),
|
|
eval_metadata: Some(eval_metadata),
|
|
..Default::default()
|
|
};
|
|
|
|
if let Ok(u) = storage.put_object_metadata(&bucket, &object, &popts).await {
|
|
object_info = u;
|
|
}
|
|
|
|
// TODO: update stats
|
|
}
|
|
|
|
let event_name = if replication_status == ReplicationStatusType::Completed {
|
|
EventName::ObjectReplicationComplete.as_ref().to_string()
|
|
} else {
|
|
EventName::ObjectReplicationFailed.as_ref().to_string()
|
|
};
|
|
|
|
send_event(EventArgs {
|
|
event_name,
|
|
bucket_name: bucket.clone(),
|
|
object: object_info,
|
|
host: GLOBAL_LocalNodeName.to_string(),
|
|
user_agent: "Internal: [Replication]".to_string(),
|
|
..Default::default()
|
|
});
|
|
|
|
if rinfos.replication_status() != ReplicationStatusType::Completed {
|
|
// TODO: update stats
|
|
// pool
|
|
}
|
|
}
|
|
|
|
trait ReplicateObjectInfoExt {
|
|
async fn replicate_object<S: StorageAPI>(&self, storage: Arc<S>, tgt_client: Arc<TargetClient>) -> ReplicatedTargetInfo;
|
|
async fn replicate_all<S: StorageAPI>(&self, storage: Arc<S>, tgt_client: Arc<TargetClient>) -> ReplicatedTargetInfo;
|
|
fn to_object_info(&self) -> ObjectInfo;
|
|
}
|
|
|
|
impl ReplicateObjectInfoExt for ReplicateObjectInfo {
|
|
async fn replicate_object<S: StorageAPI>(&self, storage: Arc<S>, tgt_client: Arc<TargetClient>) -> ReplicatedTargetInfo {
|
|
let bucket = self.bucket.clone();
|
|
let object = self.name.clone();
|
|
|
|
let replication_action = ReplicationAction::All;
|
|
let mut rinfo = ReplicatedTargetInfo {
|
|
arn: tgt_client.arn.clone(),
|
|
size: self.actual_size,
|
|
replication_action,
|
|
op_type: self.op_type,
|
|
replication_status: ReplicationStatusType::Failed,
|
|
prev_replication_status: self.target_replication_status(&tgt_client.arn),
|
|
endpoint: tgt_client.endpoint.clone(),
|
|
secure: tgt_client.secure,
|
|
..Default::default()
|
|
};
|
|
|
|
if self.target_replication_status(&tgt_client.arn) == ReplicationStatusType::Completed
|
|
&& !self.existing_obj_resync.is_empty()
|
|
&& self.existing_obj_resync.must_resync_target(&tgt_client.arn)
|
|
{
|
|
rinfo.replication_status = ReplicationStatusType::Completed;
|
|
rinfo.replication_resynced = true;
|
|
|
|
return rinfo;
|
|
}
|
|
|
|
if BucketTargetSys::get().is_offline(&tgt_client.to_url()).await {
|
|
warn!("target is offline: {}", tgt_client.to_url());
|
|
send_event(EventArgs {
|
|
event_name: EventName::ObjectReplicationNotTracked.as_ref().to_string(),
|
|
bucket_name: bucket.clone(),
|
|
object: self.to_object_info(),
|
|
host: GLOBAL_LocalNodeName.to_string(),
|
|
user_agent: "Internal: [Replication]".to_string(),
|
|
..Default::default()
|
|
});
|
|
return rinfo;
|
|
}
|
|
|
|
let versioned = BucketVersioningSys::prefix_enabled(&bucket, &object).await;
|
|
let version_suspended = BucketVersioningSys::prefix_suspended(&bucket, &object).await;
|
|
|
|
let obj_opts = ObjectOptions {
|
|
version_id: self.version_id.map(|v| v.to_string()),
|
|
version_suspended,
|
|
versioned,
|
|
replication_request: true,
|
|
..Default::default()
|
|
};
|
|
|
|
let mut gr = match storage
|
|
.get_object_reader(&bucket, &object, None, HeaderMap::new(), &obj_opts)
|
|
.await
|
|
{
|
|
Ok(gr) => gr,
|
|
Err(e) => {
|
|
if !is_err_object_not_found(&e) || is_err_version_not_found(&e) {
|
|
warn!("failed to get object reader for bucket:{} arn:{} error:{}", bucket, tgt_client.arn, e);
|
|
|
|
send_event(EventArgs {
|
|
event_name: EventName::ObjectReplicationNotTracked.as_ref().to_string(),
|
|
bucket_name: bucket.clone(),
|
|
object: self.to_object_info(),
|
|
host: GLOBAL_LocalNodeName.to_string(),
|
|
user_agent: "Internal: [Replication]".to_string(),
|
|
..Default::default()
|
|
});
|
|
}
|
|
|
|
return rinfo;
|
|
}
|
|
};
|
|
|
|
let object_info = gr.object_info.clone();
|
|
|
|
rinfo.prev_replication_status = object_info.target_replication_status(&tgt_client.arn);
|
|
|
|
let size = match object_info.get_actual_size() {
|
|
Ok(size) => size,
|
|
Err(e) => {
|
|
warn!("failed to get actual size for bucket:{} arn:{} error:{}", bucket, tgt_client.arn, e);
|
|
send_event(EventArgs {
|
|
event_name: EventName::ObjectReplicationNotTracked.as_ref().to_string(),
|
|
bucket_name: bucket.clone(),
|
|
object: object_info,
|
|
host: GLOBAL_LocalNodeName.to_string(),
|
|
user_agent: "Internal: [Replication]".to_string(),
|
|
..Default::default()
|
|
});
|
|
return rinfo;
|
|
}
|
|
};
|
|
|
|
if tgt_client.bucket.is_empty() {
|
|
warn!("target bucket is empty: {}", tgt_client.bucket);
|
|
send_event(EventArgs {
|
|
event_name: EventName::ObjectReplicationNotTracked.as_ref().to_string(),
|
|
bucket_name: bucket.clone(),
|
|
object: object_info,
|
|
host: GLOBAL_LocalNodeName.to_string(),
|
|
user_agent: "Internal: [Replication]".to_string(),
|
|
..Default::default()
|
|
});
|
|
return rinfo;
|
|
}
|
|
|
|
let mut replication_action = replication_action;
|
|
match tgt_client
|
|
.head_object(&tgt_client.bucket, &object, self.version_id.map(|v| v.to_string()))
|
|
.await
|
|
{
|
|
Ok(oi) => {
|
|
replication_action = get_replication_action(&object_info, &oi, self.op_type);
|
|
if replication_action == ReplicationAction::None {
|
|
rinfo.replication_status = ReplicationStatusType::Completed;
|
|
rinfo.replication_resynced = true;
|
|
rinfo.replication_action = ReplicationAction::None;
|
|
rinfo.size = size;
|
|
return rinfo;
|
|
}
|
|
}
|
|
Err(e) => {
|
|
if let Some(se) = e.as_service_error() {
|
|
if !se.is_not_found() {
|
|
rinfo.error = Some(e.to_string());
|
|
warn!("replication head_object failed bucket:{} arn:{} error:{}", bucket, tgt_client.arn, e);
|
|
return rinfo;
|
|
}
|
|
} else if e.raw_response().is_some_and(|resp| resp.status().as_u16() == 404) {
|
|
// Some HEAD Object 404 responses are surfaced by the AWS SDK as `response error`
|
|
// instead of `service error (NotFound)`. Treat raw HTTP 404 as object-not-found
|
|
// so replication can proceed with PUT.
|
|
} else {
|
|
rinfo.error = Some(e.to_string());
|
|
warn!("replication head_object failed bucket:{} arn:{} error:{}", bucket, tgt_client.arn, e);
|
|
return rinfo;
|
|
}
|
|
}
|
|
}
|
|
|
|
rinfo.replication_status = ReplicationStatusType::Completed;
|
|
rinfo.replication_resynced = true;
|
|
rinfo.size = size;
|
|
rinfo.replication_action = replication_action;
|
|
|
|
let (put_opts, is_multipart) = match put_replication_opts(&tgt_client.storage_class, &object_info) {
|
|
Ok((put_opts, is_mp)) => (put_opts, is_mp),
|
|
Err(e) => {
|
|
warn!(
|
|
"failed to get put replication opts for bucket:{} arn:{} error:{}",
|
|
bucket, tgt_client.arn, e
|
|
);
|
|
send_event(EventArgs {
|
|
event_name: EventName::ObjectReplicationNotTracked.as_ref().to_string(),
|
|
bucket_name: bucket.clone(),
|
|
object: object_info,
|
|
host: GLOBAL_LocalNodeName.to_string(),
|
|
user_agent: "Internal: [Replication]".to_string(),
|
|
..Default::default()
|
|
});
|
|
return rinfo;
|
|
}
|
|
};
|
|
|
|
if let Some(err) = if is_multipart {
|
|
drop(gr);
|
|
replicate_object_with_multipart(MultipartReplicationContext {
|
|
storage: storage.clone(),
|
|
cli: tgt_client.clone(),
|
|
src_bucket: &bucket,
|
|
dst_bucket: &tgt_client.bucket,
|
|
object: &object,
|
|
object_info: &object_info,
|
|
obj_opts: &obj_opts,
|
|
arn: &rinfo.arn,
|
|
put_opts,
|
|
})
|
|
.await
|
|
.err()
|
|
} else {
|
|
gr.stream = wrap_with_bandwidth_monitor(gr.stream, &put_opts, &bucket, &rinfo.arn);
|
|
let byte_stream = async_read_to_bytestream(gr.stream);
|
|
tgt_client
|
|
.put_object(&tgt_client.bucket, &object, size, byte_stream, &put_opts)
|
|
.await
|
|
.map_err(|e| std::io::Error::other(e.to_string()))
|
|
.err()
|
|
} {
|
|
rinfo.replication_status = ReplicationStatusType::Failed;
|
|
rinfo.error = Some(err.to_string());
|
|
warn!(
|
|
"replication put_object failed src_bucket={} dest_bucket={} object={} err={:?}",
|
|
bucket, tgt_client.bucket, object, err
|
|
);
|
|
|
|
// TODO: check offline
|
|
return rinfo;
|
|
}
|
|
|
|
rinfo.replication_status = ReplicationStatusType::Completed;
|
|
|
|
rinfo
|
|
}
|
|
|
|
async fn replicate_all<S: StorageAPI>(&self, storage: Arc<S>, tgt_client: Arc<TargetClient>) -> ReplicatedTargetInfo {
|
|
let start_time = OffsetDateTime::now_utc();
|
|
|
|
let bucket = self.bucket.clone();
|
|
let object = self.name.clone();
|
|
|
|
let mut replication_action = ReplicationAction::Metadata;
|
|
let mut rinfo = ReplicatedTargetInfo {
|
|
arn: tgt_client.arn.clone(),
|
|
size: self.actual_size,
|
|
replication_action,
|
|
op_type: self.op_type,
|
|
replication_status: ReplicationStatusType::Failed,
|
|
prev_replication_status: self.target_replication_status(&tgt_client.arn),
|
|
endpoint: tgt_client.endpoint.clone(),
|
|
secure: tgt_client.secure,
|
|
..Default::default()
|
|
};
|
|
|
|
if BucketTargetSys::get().is_offline(&tgt_client.to_url()).await {
|
|
warn!("target is offline: {}", tgt_client.to_url());
|
|
send_event(EventArgs {
|
|
event_name: EventName::ObjectReplicationNotTracked.as_ref().to_string(),
|
|
bucket_name: bucket.clone(),
|
|
object: self.to_object_info(),
|
|
host: GLOBAL_LocalNodeName.to_string(),
|
|
user_agent: "Internal: [Replication]".to_string(),
|
|
..Default::default()
|
|
});
|
|
return rinfo;
|
|
}
|
|
|
|
let versioned = BucketVersioningSys::prefix_enabled(&bucket, &object).await;
|
|
let version_suspended = BucketVersioningSys::prefix_suspended(&bucket, &object).await;
|
|
|
|
let obj_opts = ObjectOptions {
|
|
version_id: self.version_id.map(|v| v.to_string()),
|
|
version_suspended,
|
|
versioned,
|
|
replication_request: true,
|
|
..Default::default()
|
|
};
|
|
|
|
let mut gr = match storage
|
|
.get_object_reader(&bucket, &object, None, HeaderMap::new(), &obj_opts)
|
|
.await
|
|
{
|
|
Ok(gr) => gr,
|
|
Err(e) => {
|
|
if !is_err_object_not_found(&e) || is_err_version_not_found(&e) {
|
|
warn!("failed to get object reader for bucket:{} arn:{} error:{}", bucket, tgt_client.arn, e);
|
|
send_event(EventArgs {
|
|
event_name: EventName::ObjectReplicationNotTracked.as_ref().to_string(),
|
|
bucket_name: bucket.clone(),
|
|
object: self.to_object_info(),
|
|
host: GLOBAL_LocalNodeName.to_string(),
|
|
user_agent: "Internal: [Replication]".to_string(),
|
|
..Default::default()
|
|
});
|
|
}
|
|
|
|
return rinfo;
|
|
}
|
|
};
|
|
|
|
let object_info = gr.object_info.clone();
|
|
|
|
rinfo.prev_replication_status = object_info.target_replication_status(&tgt_client.arn);
|
|
|
|
if rinfo.prev_replication_status == ReplicationStatusType::Completed
|
|
&& !self.existing_obj_resync.is_empty()
|
|
&& self.existing_obj_resync.must_resync_target(&tgt_client.arn)
|
|
{
|
|
rinfo.replication_status = ReplicationStatusType::Completed;
|
|
rinfo.replication_resynced = true;
|
|
return rinfo;
|
|
}
|
|
|
|
let size = match object_info.get_actual_size() {
|
|
Ok(size) => size,
|
|
Err(e) => {
|
|
warn!("failed to get actual size for bucket:{} arn:{} error:{}", bucket, tgt_client.arn, e);
|
|
send_event(EventArgs {
|
|
event_name: EventName::ObjectReplicationNotTracked.as_ref().to_string(),
|
|
bucket_name: bucket.clone(),
|
|
object: object_info,
|
|
host: GLOBAL_LocalNodeName.to_string(),
|
|
user_agent: "Internal: [Replication]".to_string(),
|
|
..Default::default()
|
|
});
|
|
return rinfo;
|
|
}
|
|
};
|
|
|
|
// TODO: SSE
|
|
|
|
if tgt_client.bucket.is_empty() {
|
|
warn!("target bucket is empty: {}", tgt_client.bucket);
|
|
send_event(EventArgs {
|
|
event_name: EventName::ObjectReplicationNotTracked.as_ref().to_string(),
|
|
bucket_name: bucket.clone(),
|
|
object: object_info,
|
|
host: GLOBAL_LocalNodeName.to_string(),
|
|
user_agent: "Internal: [Replication]".to_string(),
|
|
..Default::default()
|
|
});
|
|
return rinfo;
|
|
}
|
|
|
|
let sopts = StatObjectOptions {
|
|
version_id: object_info.version_id.map(|v| v.to_string()).unwrap_or_default(),
|
|
internal: AdvancedGetOptions {
|
|
replication_proxy_request: "false".to_string(),
|
|
..Default::default()
|
|
},
|
|
..Default::default()
|
|
};
|
|
|
|
sopts.set(AMZ_TAGGING_DIRECTIVE, "ACCESS");
|
|
|
|
match tgt_client
|
|
.head_object(&tgt_client.bucket, &object, self.version_id.map(|v| v.to_string()))
|
|
.await
|
|
{
|
|
Ok(oi) => {
|
|
replication_action = get_replication_action(&object_info, &oi, self.op_type);
|
|
rinfo.replication_status = ReplicationStatusType::Completed;
|
|
if replication_action == ReplicationAction::None {
|
|
if self.op_type == ReplicationType::ExistingObject
|
|
&& object_info.mod_time
|
|
> oi.last_modified.map(|dt| {
|
|
time::OffsetDateTime::from_unix_timestamp(dt.secs()).unwrap_or(time::OffsetDateTime::UNIX_EPOCH)
|
|
})
|
|
&& object_info.version_id.is_none()
|
|
{
|
|
warn!(
|
|
"unable to replicate {}/{} Newer version exists on target {}",
|
|
bucket,
|
|
object,
|
|
tgt_client.to_url()
|
|
);
|
|
send_event(EventArgs {
|
|
event_name: EventName::ObjectReplicationNotTracked.as_ref().to_string(),
|
|
bucket_name: bucket.clone(),
|
|
object: object_info.clone(),
|
|
host: GLOBAL_LocalNodeName.to_string(),
|
|
user_agent: "Internal: [Replication]".to_string(),
|
|
..Default::default()
|
|
});
|
|
}
|
|
|
|
if object_info.target_replication_status(&tgt_client.arn) == ReplicationStatusType::Pending
|
|
|| object_info.target_replication_status(&tgt_client.arn) == ReplicationStatusType::Failed
|
|
|| self.op_type == ReplicationType::ExistingObject
|
|
{
|
|
rinfo.replication_action = replication_action;
|
|
rinfo.replication_status = ReplicationStatusType::Completed;
|
|
}
|
|
|
|
if rinfo.replication_status == ReplicationStatusType::Completed
|
|
&& self.op_type == ReplicationType::ExistingObject
|
|
&& !tgt_client.reset_id.is_empty()
|
|
{
|
|
rinfo.resync_timestamp =
|
|
format!("{};{}", OffsetDateTime::now_utc().format(&Rfc3339).unwrap(), tgt_client.reset_id);
|
|
rinfo.replication_resynced = true;
|
|
}
|
|
|
|
rinfo.duration = (OffsetDateTime::now_utc() - start_time).unsigned_abs();
|
|
|
|
return rinfo;
|
|
}
|
|
}
|
|
Err(e) => {
|
|
if let Some(se) = e.as_service_error() {
|
|
if se.is_not_found() {
|
|
replication_action = ReplicationAction::All;
|
|
} else {
|
|
rinfo.error = Some(e.to_string());
|
|
warn!("failed to head object for bucket:{} arn:{} error:{}", bucket, tgt_client.arn, e);
|
|
|
|
send_event(EventArgs {
|
|
event_name: EventName::ObjectReplicationNotTracked.as_ref().to_string(),
|
|
bucket_name: bucket.clone(),
|
|
object: object_info,
|
|
host: GLOBAL_LocalNodeName.to_string(),
|
|
user_agent: "Internal: [Replication]".to_string(),
|
|
..Default::default()
|
|
});
|
|
|
|
rinfo.duration = (OffsetDateTime::now_utc() - start_time).unsigned_abs();
|
|
return rinfo;
|
|
}
|
|
} else {
|
|
rinfo.error = Some(e.to_string());
|
|
warn!("failed to head object for bucket:{} arn:{} error:{}", bucket, tgt_client.arn, e);
|
|
|
|
send_event(EventArgs {
|
|
event_name: EventName::ObjectReplicationNotTracked.as_ref().to_string(),
|
|
bucket_name: bucket.clone(),
|
|
object: object_info,
|
|
host: GLOBAL_LocalNodeName.to_string(),
|
|
user_agent: "Internal: [Replication]".to_string(),
|
|
..Default::default()
|
|
});
|
|
|
|
rinfo.duration = (OffsetDateTime::now_utc() - start_time).unsigned_abs();
|
|
return rinfo;
|
|
}
|
|
}
|
|
};
|
|
|
|
rinfo.replication_status = ReplicationStatusType::Completed;
|
|
rinfo.size = size;
|
|
rinfo.replication_action = replication_action;
|
|
|
|
if replication_action != ReplicationAction::All {
|
|
// TODO: copy object
|
|
} else {
|
|
let (put_opts, is_multipart) = match put_replication_opts(&tgt_client.storage_class, &object_info) {
|
|
Ok((put_opts, is_mp)) => (put_opts, is_mp),
|
|
Err(e) => {
|
|
rinfo.error = Some(e.to_string());
|
|
warn!(
|
|
"failed to get put replication opts for bucket:{} arn:{} error:{}",
|
|
bucket, tgt_client.arn, e
|
|
);
|
|
send_event(EventArgs {
|
|
event_name: EventName::ObjectReplicationNotTracked.as_ref().to_string(),
|
|
bucket_name: bucket.clone(),
|
|
object: object_info,
|
|
host: GLOBAL_LocalNodeName.to_string(),
|
|
user_agent: "Internal: [Replication]".to_string(),
|
|
..Default::default()
|
|
});
|
|
|
|
rinfo.duration = (OffsetDateTime::now_utc() - start_time).unsigned_abs();
|
|
return rinfo;
|
|
}
|
|
};
|
|
|
|
if let Some(err) = if is_multipart {
|
|
drop(gr);
|
|
replicate_object_with_multipart(MultipartReplicationContext {
|
|
storage: storage.clone(),
|
|
cli: tgt_client.clone(),
|
|
src_bucket: &bucket,
|
|
dst_bucket: &tgt_client.bucket,
|
|
object: &object,
|
|
object_info: &object_info,
|
|
obj_opts: &obj_opts,
|
|
arn: &rinfo.arn,
|
|
put_opts,
|
|
})
|
|
.await
|
|
.err()
|
|
} else {
|
|
gr.stream = wrap_with_bandwidth_monitor(gr.stream, &put_opts, &bucket, &rinfo.arn);
|
|
let byte_stream = async_read_to_bytestream(gr.stream);
|
|
tgt_client
|
|
.put_object(&tgt_client.bucket, &object, size, byte_stream, &put_opts)
|
|
.await
|
|
.map_err(|e| std::io::Error::other(e.to_string()))
|
|
.err()
|
|
} {
|
|
rinfo.replication_status = ReplicationStatusType::Failed;
|
|
rinfo.error = Some(err.to_string());
|
|
rinfo.duration = (OffsetDateTime::now_utc() - start_time).unsigned_abs();
|
|
|
|
// TODO: check offline
|
|
return rinfo;
|
|
}
|
|
}
|
|
|
|
rinfo
|
|
}
|
|
|
|
fn to_object_info(&self) -> ObjectInfo {
|
|
ObjectInfo {
|
|
bucket: self.bucket.clone(),
|
|
name: self.name.clone(),
|
|
mod_time: self.mod_time,
|
|
version_id: self.version_id,
|
|
size: self.size,
|
|
user_tags: self.user_tags.clone(),
|
|
actual_size: self.actual_size,
|
|
replication_status_internal: self.replication_status_internal.clone(),
|
|
replication_status: self.replication_status.clone(),
|
|
version_purge_status_internal: self.version_purge_status_internal.clone(),
|
|
version_purge_status: self.version_purge_status.clone(),
|
|
delete_marker: true,
|
|
checksum: self.checksum.clone(),
|
|
..Default::default()
|
|
}
|
|
}
|
|
}
|
|
|
|
// Standard headers that needs to be extracted from User metadata.
|
|
static STANDARD_HEADERS: &[&str] = &[
|
|
headers::CONTENT_TYPE,
|
|
headers::CACHE_CONTROL,
|
|
headers::CONTENT_ENCODING,
|
|
headers::CONTENT_LANGUAGE,
|
|
headers::CONTENT_DISPOSITION,
|
|
headers::AMZ_STORAGE_CLASS,
|
|
headers::AMZ_OBJECT_TAGGING,
|
|
headers::AMZ_BUCKET_REPLICATION_STATUS,
|
|
headers::AMZ_OBJECT_LOCK_MODE,
|
|
headers::AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE,
|
|
headers::AMZ_OBJECT_LOCK_LEGAL_HOLD,
|
|
headers::AMZ_TAG_COUNT,
|
|
headers::AMZ_SERVER_SIDE_ENCRYPTION,
|
|
];
|
|
|
|
fn calc_put_object_header_size(put_opts: &PutObjectOptions) -> usize {
|
|
let mut header_size: usize = 0;
|
|
for (key, value) in put_opts.header().iter() {
|
|
header_size += key.as_str().len();
|
|
header_size += value.as_bytes().len();
|
|
// Account for HTTP header formatting: ": " (2 bytes) and "\r\n" (2 bytes)
|
|
header_size += 4;
|
|
}
|
|
header_size
|
|
}
|
|
|
|
fn wrap_with_bandwidth_monitor_with_header(
|
|
stream: Box<dyn AsyncRead + Unpin + Send + Sync>,
|
|
bucket: &str,
|
|
arn: &str,
|
|
header_size: usize,
|
|
) -> Box<dyn AsyncRead + Unpin + Send + Sync> {
|
|
if let Some(monitor) = get_global_bucket_monitor() {
|
|
Box::new(MonitoredReader::new(
|
|
monitor,
|
|
stream,
|
|
MonitorReaderOptions {
|
|
bucket_options: BucketOptions {
|
|
name: bucket.to_string(),
|
|
replication_arn: arn.to_string(),
|
|
},
|
|
header_size,
|
|
},
|
|
))
|
|
} else {
|
|
WARNED_MONITOR_UNINIT.call_once(|| {
|
|
warn!(
|
|
"Global bucket monitor uninitialized; proceeding with unthrottled replication (bandwidth limits will be ignored)"
|
|
)
|
|
});
|
|
stream
|
|
}
|
|
}
|
|
|
|
fn wrap_with_bandwidth_monitor(
|
|
stream: Box<dyn AsyncRead + Unpin + Send + Sync>,
|
|
put_opts: &PutObjectOptions,
|
|
bucket: &str,
|
|
arn: &str,
|
|
) -> Box<dyn AsyncRead + Unpin + Send + Sync> {
|
|
let header_size = calc_put_object_header_size(put_opts);
|
|
wrap_with_bandwidth_monitor_with_header(stream, bucket, arn, header_size)
|
|
}
|
|
|
|
fn async_read_to_bytestream(reader: impl AsyncRead + Send + Sync + Unpin + 'static) -> ByteStream {
|
|
// Non-retryable: SDK-level retries are not supported for streaming bodies.
|
|
// Replication-level retry handles failures at a higher layer.
|
|
let stream = ReaderStream::new(reader);
|
|
let body = StreamBody::new(stream.map(|r| r.map(Frame::data)));
|
|
ByteStream::new(SdkBody::from_body_1_x(body))
|
|
}
|
|
|
|
fn is_standard_header(k: &str) -> bool {
|
|
STANDARD_HEADERS.iter().any(|h| h.eq_ignore_ascii_case(k))
|
|
}
|
|
|
|
// Valid SSE replication headers mapping from internal to replication headers
|
|
static VALID_SSE_REPLICATION_HEADERS: &[(&str, &str)] = &[
|
|
(
|
|
"X-Rustfs-Internal-Server-Side-Encryption-Sealed-Key",
|
|
"X-Rustfs-Replication-Server-Side-Encryption-Sealed-Key",
|
|
),
|
|
(
|
|
"X-Rustfs-Internal-Server-Side-Encryption-Seal-Algorithm",
|
|
"X-Rustfs-Replication-Server-Side-Encryption-Seal-Algorithm",
|
|
),
|
|
(
|
|
"X-Rustfs-Internal-Server-Side-Encryption-Iv",
|
|
"X-Rustfs-Replication-Server-Side-Encryption-Iv",
|
|
),
|
|
("X-Rustfs-Internal-Encrypted-Multipart", "X-Rustfs-Replication-Encrypted-Multipart"),
|
|
("X-Rustfs-Internal-Actual-Object-Size", "X-Rustfs-Replication-Actual-Object-Size"),
|
|
];
|
|
|
|
const REPLICATION_SSEC_CHECKSUM_HEADER: &str = "X-Rustfs-Replication-Ssec-Crc";
|
|
|
|
fn is_valid_sse_header(k: &str) -> Option<&str> {
|
|
VALID_SSE_REPLICATION_HEADERS
|
|
.iter()
|
|
.find(|(internal, _)| k.eq_ignore_ascii_case(internal))
|
|
.map(|(_, replication)| *replication)
|
|
}
|
|
|
|
fn put_replication_opts(sc: &str, object_info: &ObjectInfo) -> Result<(PutObjectOptions, bool)> {
|
|
use crate::config::storageclass::{RRS, STANDARD};
|
|
use base64::{Engine, engine::general_purpose::STANDARD as BASE64_STANDARD};
|
|
use rustfs_utils::http::{
|
|
AMZ_CHECKSUM_TYPE, AMZ_CHECKSUM_TYPE_FULL_OBJECT, AMZ_SERVER_SIDE_ENCRYPTION, AMZ_SERVER_SIDE_ENCRYPTION_KMS_ID,
|
|
};
|
|
|
|
let mut meta = HashMap::new();
|
|
let is_ssec = is_ssec_encrypted(&object_info.user_defined);
|
|
|
|
// Process user-defined metadata
|
|
for (k, v) in object_info.user_defined.iter() {
|
|
let has_valid_sse_header = is_valid_sse_header(k).is_some();
|
|
|
|
// In case of SSE-C objects copy the allowed internal headers as well
|
|
if !is_ssec || !has_valid_sse_header {
|
|
if strings_has_prefix_fold(k, RESERVED_METADATA_PREFIX) {
|
|
continue;
|
|
}
|
|
if is_standard_header(k) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if let Some(replication_header) = is_valid_sse_header(k) {
|
|
meta.insert(replication_header.to_string(), v.to_string());
|
|
} else {
|
|
meta.insert(k.to_string(), v.to_string());
|
|
}
|
|
}
|
|
|
|
let mut is_multipart = object_info.is_multipart();
|
|
|
|
// Handle checksum
|
|
if let Some(checksum_data) = &object_info.checksum
|
|
&& !checksum_data.is_empty()
|
|
{
|
|
// Add encrypted CRC to metadata for SSE-C objects
|
|
if is_ssec {
|
|
let encoded = BASE64_STANDARD.encode(checksum_data);
|
|
meta.insert(REPLICATION_SSEC_CHECKSUM_HEADER.to_string(), encoded);
|
|
} else {
|
|
// Get checksum metadata for non-SSE-C objects
|
|
let (cs_meta, is_mp) = object_info.decrypt_checksums(0, &http::HeaderMap::new())?;
|
|
is_multipart = is_mp;
|
|
|
|
// Set object checksum metadata
|
|
for (k, v) in cs_meta.iter() {
|
|
if k != AMZ_CHECKSUM_TYPE {
|
|
meta.insert(k.clone(), v.clone());
|
|
}
|
|
}
|
|
|
|
// For objects where checksum is full object, use the cheaper PutObject replication
|
|
if !object_info.is_multipart()
|
|
&& cs_meta
|
|
.get(AMZ_CHECKSUM_TYPE)
|
|
.map(|v| v.as_str() == AMZ_CHECKSUM_TYPE_FULL_OBJECT)
|
|
.unwrap_or(false)
|
|
{
|
|
is_multipart = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle storage class default
|
|
let storage_class = if sc.is_empty() {
|
|
let obj_sc = object_info.storage_class.as_deref().unwrap_or_default();
|
|
if obj_sc == STANDARD || obj_sc == RRS {
|
|
obj_sc.to_string()
|
|
} else {
|
|
sc.to_string()
|
|
}
|
|
} else {
|
|
sc.to_string()
|
|
};
|
|
|
|
let mut put_op = PutObjectOptions {
|
|
user_metadata: meta,
|
|
content_type: object_info.content_type.clone().unwrap_or_default(),
|
|
content_encoding: object_info.content_encoding.clone().unwrap_or_default(),
|
|
expires: object_info.expires.unwrap_or(OffsetDateTime::UNIX_EPOCH),
|
|
storage_class,
|
|
internal: AdvancedPutOptions {
|
|
source_version_id: object_info.version_id.map(|v| v.to_string()).unwrap_or_default(),
|
|
source_etag: object_info.etag.clone().unwrap_or_default(),
|
|
source_mtime: object_info.mod_time.unwrap_or(OffsetDateTime::UNIX_EPOCH),
|
|
replication_status: ReplicationStatusType::Replica, // Changed from Pending to Replica
|
|
replication_request: true, // always set this to distinguish between replication and normal PUT operation
|
|
..Default::default()
|
|
},
|
|
..Default::default()
|
|
};
|
|
|
|
if !object_info.user_tags.is_empty() {
|
|
let tags = decode_tags_to_map(&object_info.user_tags);
|
|
|
|
if !tags.is_empty() {
|
|
put_op.user_tags = tags;
|
|
// set tag timestamp in opts
|
|
put_op.internal.tagging_timestamp = if let Some(ts) = object_info
|
|
.user_defined
|
|
.get(&format!("{RESERVED_METADATA_PREFIX_LOWER}tagging-timestamp"))
|
|
{
|
|
OffsetDateTime::parse(ts, &Rfc3339)
|
|
.map_err(|e| Error::other(format!("Failed to parse tagging timestamp: {}", e)))?
|
|
} else {
|
|
object_info.mod_time.unwrap_or(OffsetDateTime::UNIX_EPOCH)
|
|
};
|
|
}
|
|
}
|
|
|
|
// Use case-insensitive lookup for headers
|
|
let lk_map = object_info.user_defined.clone();
|
|
|
|
if let Some(lang) = lk_map.lookup(headers::CONTENT_LANGUAGE) {
|
|
put_op.content_language = lang.to_string();
|
|
}
|
|
|
|
if let Some(cd) = lk_map.lookup(headers::CONTENT_DISPOSITION) {
|
|
put_op.content_disposition = cd.to_string();
|
|
}
|
|
|
|
if let Some(v) = lk_map.lookup(headers::CACHE_CONTROL) {
|
|
put_op.cache_control = v.to_string();
|
|
}
|
|
|
|
if let Some(v) = lk_map.lookup(headers::AMZ_OBJECT_LOCK_MODE) {
|
|
let mode = v.to_string().to_uppercase();
|
|
put_op.mode = Some(aws_sdk_s3::types::ObjectLockRetentionMode::from(mode.as_str()));
|
|
}
|
|
|
|
if let Some(v) = lk_map.lookup(headers::AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE) {
|
|
put_op.retain_until_date =
|
|
OffsetDateTime::parse(v, &Rfc3339).map_err(|e| Error::other(format!("Failed to parse retain until date: {}", e)))?;
|
|
// set retention timestamp in opts
|
|
put_op.internal.retention_timestamp = if let Some(v) = object_info
|
|
.user_defined
|
|
.get(&format!("{RESERVED_METADATA_PREFIX_LOWER}objectlock-retention-timestamp"))
|
|
{
|
|
OffsetDateTime::parse(v, &Rfc3339).unwrap_or(OffsetDateTime::UNIX_EPOCH)
|
|
} else {
|
|
object_info.mod_time.unwrap_or(OffsetDateTime::UNIX_EPOCH)
|
|
};
|
|
}
|
|
|
|
if let Some(v) = lk_map.lookup(headers::AMZ_OBJECT_LOCK_LEGAL_HOLD) {
|
|
let hold = v.to_uppercase();
|
|
put_op.legalhold = Some(ObjectLockLegalHoldStatus::from(hold.as_str()));
|
|
// set legalhold timestamp in opts
|
|
put_op.internal.legalhold_timestamp = if let Some(v) = object_info
|
|
.user_defined
|
|
.get(&format!("{RESERVED_METADATA_PREFIX_LOWER}objectlock-legalhold-timestamp"))
|
|
{
|
|
OffsetDateTime::parse(v, &Rfc3339).unwrap_or(OffsetDateTime::UNIX_EPOCH)
|
|
} else {
|
|
object_info.mod_time.unwrap_or(OffsetDateTime::UNIX_EPOCH)
|
|
};
|
|
}
|
|
|
|
// Handle SSE-S3 encryption
|
|
if object_info
|
|
.user_defined
|
|
.get(AMZ_SERVER_SIDE_ENCRYPTION)
|
|
.map(|v| v.eq_ignore_ascii_case("AES256"))
|
|
.unwrap_or(false)
|
|
{
|
|
// SSE-S3 detected - set ServerSideEncryption
|
|
// Note: This requires the PutObjectOptions to support SSE
|
|
// TODO: Implement SSE-S3 support in PutObjectOptions if not already present
|
|
}
|
|
|
|
// Handle SSE-KMS encryption
|
|
if object_info.user_defined.contains_key(AMZ_SERVER_SIDE_ENCRYPTION_KMS_ID) {
|
|
// SSE-KMS detected
|
|
// If KMS key ID replication is enabled (as by default)
|
|
// we include the object's KMS key ID. In any case, we
|
|
// always set the SSE-KMS header. If no KMS key ID is
|
|
// specified, MinIO is supposed to use whatever default
|
|
// config applies on the site or bucket.
|
|
// TODO: Implement SSE-KMS support with key ID replication
|
|
// let key_id = if kms::replicate_key_id() {
|
|
// object_info.kms_key_id()
|
|
// } else {
|
|
// None
|
|
// };
|
|
// TODO: Set SSE-KMS encryption in put_op
|
|
}
|
|
|
|
Ok((put_op, is_multipart))
|
|
}
|
|
|
|
fn part_range_spec_from_actual_size(offset: i64, part_size: i64) -> std::io::Result<(HTTPRangeSpec, i64)> {
|
|
if offset < 0 {
|
|
return Err(std::io::Error::other("invalid part offset"));
|
|
}
|
|
if part_size <= 0 {
|
|
return Err(std::io::Error::other(format!("invalid part size {part_size}")));
|
|
}
|
|
let end = offset
|
|
.checked_add(part_size - 1)
|
|
.ok_or_else(|| std::io::Error::other("part range overflow"))?;
|
|
let next_offset = end
|
|
.checked_add(1)
|
|
.ok_or_else(|| std::io::Error::other("part offset overflow"))?;
|
|
Ok((
|
|
HTTPRangeSpec {
|
|
is_suffix_length: false,
|
|
start: offset,
|
|
end,
|
|
},
|
|
next_offset,
|
|
))
|
|
}
|
|
|
|
struct MultipartReplicationContext<'a, S: StorageAPI> {
|
|
storage: Arc<S>,
|
|
cli: Arc<TargetClient>,
|
|
src_bucket: &'a str,
|
|
dst_bucket: &'a str,
|
|
object: &'a str,
|
|
object_info: &'a ObjectInfo,
|
|
obj_opts: &'a ObjectOptions,
|
|
arn: &'a str,
|
|
put_opts: PutObjectOptions,
|
|
}
|
|
|
|
async fn replicate_object_with_multipart<S: StorageAPI>(ctx: MultipartReplicationContext<'_, S>) -> std::io::Result<()> {
|
|
let MultipartReplicationContext {
|
|
storage,
|
|
cli,
|
|
src_bucket,
|
|
dst_bucket,
|
|
object,
|
|
object_info,
|
|
obj_opts,
|
|
arn,
|
|
put_opts,
|
|
} = ctx;
|
|
let mut attempts = 1;
|
|
let upload_id = loop {
|
|
match cli.create_multipart_upload(dst_bucket, object, &put_opts).await {
|
|
Ok(id) => {
|
|
break id;
|
|
}
|
|
Err(e) => {
|
|
attempts += 1;
|
|
if attempts > 3 {
|
|
return Err(std::io::Error::other(e.to_string()));
|
|
}
|
|
|
|
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
|
|
|
continue;
|
|
}
|
|
}
|
|
};
|
|
|
|
let mut uploaded_parts: Vec<CompletedPart> = Vec::new();
|
|
|
|
let mut header_size = calc_put_object_header_size(&put_opts);
|
|
let mut offset: i64 = 0;
|
|
for part_info in object_info.parts.iter() {
|
|
let part_size = part_info.actual_size;
|
|
let (range_spec, next_offset) = part_range_spec_from_actual_size(offset, part_size)?;
|
|
offset = next_offset;
|
|
|
|
let part_reader = storage
|
|
.get_object_reader(src_bucket, object, Some(range_spec), HeaderMap::new(), obj_opts)
|
|
.await
|
|
.map_err(|e| std::io::Error::other(e.to_string()))?;
|
|
|
|
let part_stream = wrap_with_bandwidth_monitor_with_header(part_reader.stream, src_bucket, arn, header_size);
|
|
header_size = 0;
|
|
let byte_stream = async_read_to_bytestream(part_stream);
|
|
|
|
let object_part = cli
|
|
.put_object_part(
|
|
dst_bucket,
|
|
object,
|
|
&upload_id,
|
|
part_info.number as i32,
|
|
part_size,
|
|
byte_stream,
|
|
&PutObjectPartOptions { ..Default::default() },
|
|
)
|
|
.await
|
|
.map_err(|e| std::io::Error::other(e.to_string()))?;
|
|
|
|
let etag = object_part.e_tag.unwrap_or_default();
|
|
|
|
uploaded_parts.push(
|
|
CompletedPart::builder()
|
|
.part_number(part_info.number as i32)
|
|
.e_tag(etag)
|
|
.build(),
|
|
);
|
|
}
|
|
|
|
let mut user_metadata = HashMap::new();
|
|
|
|
user_metadata.insert(
|
|
RUSTFS_REPLICATION_ACTUAL_OBJECT_SIZE.to_string(),
|
|
object_info
|
|
.user_defined
|
|
.get(&format!("{RESERVED_METADATA_PREFIX}actual-size"))
|
|
.map(|v| v.to_string())
|
|
.unwrap_or_default(),
|
|
);
|
|
|
|
cli.complete_multipart_upload(
|
|
dst_bucket,
|
|
object,
|
|
&upload_id,
|
|
uploaded_parts,
|
|
&PutObjectOptions {
|
|
user_metadata,
|
|
..Default::default()
|
|
},
|
|
)
|
|
.await
|
|
.map_err(|e| std::io::Error::other(e.to_string()))?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn get_replication_action(oi1: &ObjectInfo, oi2: &HeadObjectOutput, op_type: ReplicationType) -> ReplicationAction {
|
|
if op_type == ReplicationType::ExistingObject
|
|
&& oi1.mod_time
|
|
> oi2
|
|
.last_modified
|
|
.map(|dt| time::OffsetDateTime::from_unix_timestamp(dt.secs()).unwrap_or(time::OffsetDateTime::UNIX_EPOCH))
|
|
&& oi1.version_id.is_none()
|
|
{
|
|
return ReplicationAction::None;
|
|
}
|
|
|
|
let size = oi1.get_actual_size().unwrap_or_default();
|
|
|
|
// Normalize ETags by removing quotes before comparison (PR #592 compatibility)
|
|
let oi1_etag = oi1.etag.as_ref().map(|e| rustfs_utils::path::trim_etag(e));
|
|
let oi2_etag = oi2.e_tag.as_ref().map(|e| rustfs_utils::path::trim_etag(e));
|
|
|
|
if oi1_etag != oi2_etag
|
|
|| oi1.version_id.map(|v| v.to_string()) != oi2.version_id
|
|
|| size != oi2.content_length.unwrap_or_default()
|
|
|| oi1.delete_marker != oi2.delete_marker.unwrap_or_default()
|
|
|| oi1.mod_time
|
|
!= oi2
|
|
.last_modified
|
|
.map(|dt| time::OffsetDateTime::from_unix_timestamp(dt.secs()).unwrap_or(time::OffsetDateTime::UNIX_EPOCH))
|
|
{
|
|
return ReplicationAction::All;
|
|
}
|
|
|
|
if oi1.content_type != oi2.content_type {
|
|
return ReplicationAction::Metadata;
|
|
}
|
|
|
|
let empty_metadata = HashMap::new();
|
|
let metadata = oi2.metadata.as_ref().unwrap_or(&empty_metadata);
|
|
|
|
if let Some(content_encoding) = &oi1.content_encoding {
|
|
if let Some(enc) = metadata
|
|
.get(CONTENT_ENCODING)
|
|
.or_else(|| metadata.get(&CONTENT_ENCODING.to_lowercase()))
|
|
{
|
|
if enc != content_encoding {
|
|
return ReplicationAction::Metadata;
|
|
}
|
|
} else {
|
|
return ReplicationAction::Metadata;
|
|
}
|
|
}
|
|
|
|
let oi1_tags = decode_tags_to_map(&oi1.user_tags);
|
|
let oi2_tags = decode_tags_to_map(metadata.get(AMZ_OBJECT_TAGGING).cloned().unwrap_or_default().as_str());
|
|
|
|
if (oi2.tag_count.unwrap_or_default() > 0 && oi1_tags != oi2_tags)
|
|
|| oi2.tag_count.unwrap_or_default() != oi1_tags.len() as i32
|
|
{
|
|
return ReplicationAction::Metadata;
|
|
}
|
|
|
|
// Compare only necessary headers
|
|
let compare_keys = vec![
|
|
"Expires",
|
|
"Cache-Control",
|
|
"Content-Language",
|
|
"Content-Disposition",
|
|
"X-Amz-Object-Lock-Mode",
|
|
"X-Amz-Object-Lock-Retain-Until-Date",
|
|
"X-Amz-Object-Lock-Legal-Hold",
|
|
"X-Amz-Website-Redirect-Location",
|
|
"X-Amz-Meta-",
|
|
];
|
|
|
|
// compare metadata on both maps to see if meta is identical
|
|
let mut compare_meta1 = HashMap::new();
|
|
for (k, v) in &oi1.user_defined {
|
|
let mut found = false;
|
|
for prefix in &compare_keys {
|
|
if strings_has_prefix_fold(k, prefix) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if found {
|
|
compare_meta1.insert(k.to_lowercase(), v.clone());
|
|
}
|
|
}
|
|
|
|
let mut compare_meta2 = HashMap::new();
|
|
for (k, v) in metadata {
|
|
let mut found = false;
|
|
for prefix in &compare_keys {
|
|
if strings_has_prefix_fold(k.to_string().as_str(), prefix) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if found {
|
|
compare_meta2.insert(k.to_lowercase(), v.clone());
|
|
}
|
|
}
|
|
|
|
if compare_meta1 != compare_meta2 {
|
|
return ReplicationAction::Metadata;
|
|
}
|
|
|
|
ReplicationAction::None
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_part_range_spec_from_actual_size() {
|
|
let (rs, next) = part_range_spec_from_actual_size(0, 10).unwrap();
|
|
assert_eq!(rs.start, 0);
|
|
assert_eq!(rs.end, 9);
|
|
assert_eq!(next, 10);
|
|
}
|
|
|
|
#[test]
|
|
fn test_part_range_spec_rejects_non_positive() {
|
|
assert!(part_range_spec_from_actual_size(0, 0).is_err());
|
|
assert!(part_range_spec_from_actual_size(0, -1).is_err());
|
|
}
|
|
}
|