mirror of
https://github.com/rustfs/rustfs.git
synced 2026-09-01 09:48:20 +00:00
fix(scanner): own publication mutations through storage drain (#6867)
* fix(scanner): own publication mutations through storage drain Co-Authored-By: heihutu <heihutu@gmail.com> * fix(storage): remove unused rename data shim Co-Authored-By: heihutu <heihutu@gmail.com> --------- Co-authored-by: heihutu <heihutu@gmail.com>
This commit is contained in:
+109
-10
@@ -39,11 +39,11 @@ use storage_api::owner::{
|
||||
EcstoreListPathRawOptions, EcstoreNsScannerOpenRequest, EcstoreObjectOpts, EcstoreReplicationConfigurationExt,
|
||||
EcstoreReplicationScannerBridge, EcstoreResultType, EcstoreScanGuard, EcstoreSetDisks, EcstoreStorageError, EcstoreStore,
|
||||
EcstoreVersioningApi, HTTPPreconditions, HTTPRangeSpec, ObjectIO, ObjectOperations, ObjectToDelete,
|
||||
ScannerReplicationHealObject, ScannerReplicationHealResult, ScannerReplicationQueueAdmission, ecstore_apply_expiry_rule,
|
||||
ecstore_apply_transition_rule, ecstore_expiry_state_handle, ecstore_get_global_tier_config_mgr, ecstore_get_lifecycle_config,
|
||||
ecstore_get_object_lock_config, ecstore_get_replication_config, ecstore_invalidate_admin_data_usage_snapshot_cache,
|
||||
ecstore_invalidate_data_usage_snapshot_cache, ecstore_is_erasure, ecstore_is_erasure_sd,
|
||||
ecstore_is_reserved_or_invalid_bucket, ecstore_list_path_raw, ecstore_object_opts_from_object_info,
|
||||
ScannerPublicationCommitScope, ScannerReplicationHealObject, ScannerReplicationHealResult, ScannerReplicationQueueAdmission,
|
||||
ecstore_apply_expiry_rule, ecstore_apply_transition_rule, ecstore_expiry_state_handle, ecstore_get_global_tier_config_mgr,
|
||||
ecstore_get_lifecycle_config, ecstore_get_object_lock_config, ecstore_get_replication_config,
|
||||
ecstore_invalidate_admin_data_usage_snapshot_cache, ecstore_invalidate_data_usage_snapshot_cache, ecstore_is_erasure,
|
||||
ecstore_is_erasure_sd, ecstore_is_reserved_or_invalid_bucket, ecstore_list_path_raw, ecstore_object_opts_from_object_info,
|
||||
ecstore_path2_bucket_object, ecstore_path2_bucket_object_with_base_path, ecstore_read_config,
|
||||
ecstore_replace_bucket_usage_memory_from_info, ecstore_resolve_object_store_handle, ecstore_save_config,
|
||||
scanner_replication_config_for_lifecycle_eval,
|
||||
@@ -55,6 +55,7 @@ use storage_api::owner::{
|
||||
ecstore_new_disk,
|
||||
};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub mod data_usage_define;
|
||||
pub mod error;
|
||||
@@ -752,6 +753,32 @@ pub(crate) fn scanner_publication_epoch_changed(error: &EcstoreError) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_config_with_publication_scope_for_epoch<S>(
|
||||
api: Arc<S>,
|
||||
bucket: &str,
|
||||
object: &str,
|
||||
mut opts: ScannerObjectOptions,
|
||||
expected_epoch: u64,
|
||||
scanner_publication_commit_scope: Option<ScannerPublicationCommitScope>,
|
||||
) -> EcstoreResult<ScannerObjectInfo>
|
||||
where
|
||||
S: ScannerObjectIO + ScannerConfigObjectDelete,
|
||||
{
|
||||
let legacy_admission = if scanner_publication_commit_scope.is_none() {
|
||||
Some(
|
||||
scanner_publication_admission_for_epoch(api.clone(), expected_epoch)
|
||||
.await
|
||||
.ok_or_else(|| EcstoreError::other(SCANNER_PUBLICATION_EPOCH_CHANGED))?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
opts.scanner_publication_commit_scope = scanner_publication_commit_scope;
|
||||
let result = api.delete_config_object(bucket, object, opts).await;
|
||||
drop(legacy_admission);
|
||||
result
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_config_with_publication_admission_for_epoch<S>(
|
||||
api: Arc<S>,
|
||||
bucket: &str,
|
||||
@@ -762,10 +789,7 @@ pub(crate) async fn delete_config_with_publication_admission_for_epoch<S>(
|
||||
where
|
||||
S: ScannerObjectIO + ScannerConfigObjectDelete,
|
||||
{
|
||||
let Some(_admission) = scanner_publication_admission_for_epoch(api.clone(), expected_epoch).await else {
|
||||
return Err(EcstoreError::other(SCANNER_PUBLICATION_EPOCH_CHANGED));
|
||||
};
|
||||
api.delete_config_object(bucket, object, opts).await
|
||||
delete_config_with_publication_scope_for_epoch(api, bucket, object, opts, expected_epoch, None).await
|
||||
}
|
||||
|
||||
/// Capture the storage-owned publication epoch without retaining the read
|
||||
@@ -796,13 +820,14 @@ where
|
||||
Some(admission)
|
||||
}
|
||||
|
||||
pub(crate) async fn save_config_shared_with_preconditions_and_lease_fence<S>(
|
||||
pub(crate) async fn save_config_shared_with_preconditions_and_lease_fence_and_scope<S>(
|
||||
api: Arc<S>,
|
||||
file: &str,
|
||||
data: Bytes,
|
||||
sha256hex: Option<String>,
|
||||
preconditions: HTTPPreconditions,
|
||||
scanner_publication_lease_fence: Option<&str>,
|
||||
scanner_publication_commit_scope: Option<ScannerPublicationCommitScope>,
|
||||
) -> EcstoreResult<ScannerObjectInfo>
|
||||
where
|
||||
S: ScannerObjectIO,
|
||||
@@ -822,6 +847,7 @@ where
|
||||
&ScannerObjectOptions {
|
||||
max_parity: true,
|
||||
http_preconditions: Some(preconditions),
|
||||
scanner_publication_commit_scope,
|
||||
user_defined,
|
||||
..Default::default()
|
||||
},
|
||||
@@ -886,6 +912,27 @@ pub trait ScannerConfigObjectDelete: Send + Sync + std::fmt::Debug + 'static {
|
||||
async fn scanner_data_usage_publication_admission(&self) -> Option<ScannerDataUsagePublicationAdmission> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Acquire a storage-owned scope for a fenced scanner metadata mutation.
|
||||
/// Implementations without a storage movement owner fail closed.
|
||||
async fn scanner_data_usage_publication_commit_scope(
|
||||
&self,
|
||||
_expected_movement_epoch: u64,
|
||||
_safe_deadline: tokio::time::Instant,
|
||||
_remote_lease_tokens: Vec<Uuid>,
|
||||
) -> Option<ScannerPublicationCommitScope> {
|
||||
None
|
||||
}
|
||||
|
||||
async fn scanner_data_usage_publication_commit_scope_with_release_flag(
|
||||
&self,
|
||||
_expected_movement_epoch: u64,
|
||||
_safe_deadline: tokio::time::Instant,
|
||||
_remote_lease_tokens: Vec<Uuid>,
|
||||
_lease_release_safe: Arc<std::sync::atomic::AtomicBool>,
|
||||
) -> Option<ScannerPublicationCommitScope> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ScannerDataUsagePublicationAdmission {
|
||||
@@ -929,6 +976,32 @@ impl ScannerConfigObjectDelete for ECStore {
|
||||
let (read_guard, epoch) = self.scanner_data_usage_publication_admission_guard().await?;
|
||||
Some(ScannerDataUsagePublicationAdmission::fenced(read_guard, epoch))
|
||||
}
|
||||
|
||||
async fn scanner_data_usage_publication_commit_scope(
|
||||
&self,
|
||||
expected_movement_epoch: u64,
|
||||
safe_deadline: tokio::time::Instant,
|
||||
remote_lease_tokens: Vec<Uuid>,
|
||||
) -> Option<ScannerPublicationCommitScope> {
|
||||
self.scanner_data_usage_publication_commit_scope(expected_movement_epoch, safe_deadline, remote_lease_tokens)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn scanner_data_usage_publication_commit_scope_with_release_flag(
|
||||
&self,
|
||||
expected_movement_epoch: u64,
|
||||
safe_deadline: tokio::time::Instant,
|
||||
remote_lease_tokens: Vec<Uuid>,
|
||||
lease_release_safe: Arc<std::sync::atomic::AtomicBool>,
|
||||
) -> Option<ScannerPublicationCommitScope> {
|
||||
self.scanner_data_usage_publication_commit_scope_with_release_flag(
|
||||
expected_movement_epoch,
|
||||
safe_deadline,
|
||||
remote_lease_tokens,
|
||||
lease_release_safe,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
@@ -946,6 +1019,32 @@ impl ScannerConfigObjectDelete for SetDisks {
|
||||
let (read_guard, epoch) = self.scanner_data_usage_publication_admission_guard().await?;
|
||||
Some(ScannerDataUsagePublicationAdmission::fenced(read_guard, epoch))
|
||||
}
|
||||
|
||||
async fn scanner_data_usage_publication_commit_scope(
|
||||
&self,
|
||||
expected_movement_epoch: u64,
|
||||
safe_deadline: tokio::time::Instant,
|
||||
remote_lease_tokens: Vec<Uuid>,
|
||||
) -> Option<ScannerPublicationCommitScope> {
|
||||
self.scanner_data_usage_publication_commit_scope(expected_movement_epoch, safe_deadline, remote_lease_tokens)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn scanner_data_usage_publication_commit_scope_with_release_flag(
|
||||
&self,
|
||||
expected_movement_epoch: u64,
|
||||
safe_deadline: tokio::time::Instant,
|
||||
remote_lease_tokens: Vec<Uuid>,
|
||||
lease_release_safe: Arc<std::sync::atomic::AtomicBool>,
|
||||
) -> Option<ScannerPublicationCommitScope> {
|
||||
self.scanner_data_usage_publication_commit_scope_with_release_flag(
|
||||
expected_movement_epoch,
|
||||
safe_deadline,
|
||||
remote_lease_tokens,
|
||||
lease_release_safe,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -16,6 +16,7 @@ use std::collections::BTreeMap;
|
||||
use std::future::Future;
|
||||
#[cfg(test)]
|
||||
use std::sync::Mutex as StdMutex;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, LazyLock, RwLock};
|
||||
|
||||
use self::heal_info::{BackgroundHealInfoReadStatus, read_background_heal_info_with_epoch, save_background_heal_info_for_epoch};
|
||||
@@ -62,6 +63,7 @@ use tokio::time::{Duration, Instant};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tokio_util::task::AbortOnDropHandle;
|
||||
use tracing::{debug, error, info, instrument, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::storage_api::scan::{
|
||||
BucketOperations, BucketOptions, NamespaceLocking as _, SCANNER_ACTIVITY_LEGACY_PROTOCOL_VERSION,
|
||||
@@ -71,7 +73,7 @@ use crate::{
|
||||
ECStore, EcstoreError, RUSTFS_META_BUCKET, SCANNER_PUBLICATION_EPOCH_CHANGED, ScannerLifecycleConfigExt as _,
|
||||
ScannerReplicationConfigExt as _, delete_config_with_publication_admission_for_epoch, get_lifecycle_config,
|
||||
get_replication_config, invalidate_admin_data_usage_snapshot_cache, invalidate_data_usage_snapshot_cache, read_config,
|
||||
replace_bucket_usage_memory_from_info, save_config, save_config_shared_with_preconditions_and_lease_fence,
|
||||
replace_bucket_usage_memory_from_info, save_config, save_config_shared_with_preconditions_and_lease_fence_and_scope,
|
||||
save_config_with_preconditions, save_config_with_publication_admission_for_epoch, scanner_is_erasure_sd,
|
||||
scanner_publication_admission_for_epoch, scanner_publication_epoch, scanner_publication_epoch_changed,
|
||||
};
|
||||
@@ -453,6 +455,7 @@ fn data_usage_backup_due(data_usage_info: &DataUsageInfo) -> bool {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(dead_code)]
|
||||
async fn sync_data_usage_backup_from_primary(
|
||||
ctx: &CancellationToken,
|
||||
storeapi: Arc<impl ScannerObjectIO + ScannerConfigObjectDelete>,
|
||||
@@ -460,12 +463,34 @@ async fn sync_data_usage_backup_from_primary(
|
||||
sync_data_usage_backup_from_primary_for_epoch_and_lease_and_fence(ctx, storeapi, None, None, None).await
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
async fn sync_data_usage_backup_from_primary_for_epoch_and_lease_and_fence(
|
||||
ctx: &CancellationToken,
|
||||
storeapi: Arc<impl ScannerObjectIO + ScannerConfigObjectDelete>,
|
||||
expected_publication_epoch: Option<u64>,
|
||||
remote_lease_deadline: Option<std::time::Instant>,
|
||||
scanner_publication_lease_fence: Option<&str>,
|
||||
) -> Result<(), EcstoreError> {
|
||||
sync_data_usage_backup_from_primary_for_epoch_and_lease_and_fence_and_scope(
|
||||
ctx,
|
||||
storeapi,
|
||||
expected_publication_epoch,
|
||||
remote_lease_deadline,
|
||||
scanner_publication_lease_fence,
|
||||
Vec::new(),
|
||||
Arc::new(AtomicBool::new(true)),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn sync_data_usage_backup_from_primary_for_epoch_and_lease_and_fence_and_scope(
|
||||
ctx: &CancellationToken,
|
||||
storeapi: Arc<impl ScannerObjectIO + ScannerConfigObjectDelete>,
|
||||
expected_publication_epoch: Option<u64>,
|
||||
remote_lease_deadline: Option<std::time::Instant>,
|
||||
scanner_publication_lease_fence: Option<&str>,
|
||||
remote_lease_tokens: Vec<Uuid>,
|
||||
lease_release_safe: Arc<AtomicBool>,
|
||||
) -> Result<(), EcstoreError> {
|
||||
let backup_path = format!("{}.bkp", DATA_USAGE_OBJ_NAME_PATH.as_str());
|
||||
for retry in 0..=SCANNER_PERSIST_CAS_RETRIES {
|
||||
@@ -530,15 +555,48 @@ async fn sync_data_usage_backup_from_primary_for_epoch_and_lease_and_fence(
|
||||
}
|
||||
return Err(EcstoreError::other(SCANNER_PUBLICATION_EPOCH_CHANGED));
|
||||
};
|
||||
save_config_shared_with_preconditions_and_lease_fence(
|
||||
let publication_scope = match expected_publication_epoch {
|
||||
Some(expected_epoch) => {
|
||||
storeapi
|
||||
.scanner_data_usage_publication_commit_scope_with_release_flag(
|
||||
expected_epoch,
|
||||
usage_store::scanner_publication_scope_deadline(data_usage_persist_timeout(), remote_lease_deadline),
|
||||
remote_lease_tokens.clone(),
|
||||
Arc::clone(&lease_release_safe),
|
||||
)
|
||||
.await
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
if expected_publication_epoch.is_some() && publication_scope.is_none() {
|
||||
if retry < SCANNER_PERSIST_CAS_RETRIES {
|
||||
continue;
|
||||
}
|
||||
return Err(EcstoreError::other(SCANNER_PUBLICATION_EPOCH_CHANGED));
|
||||
}
|
||||
let save_result = save_config_shared_with_preconditions_and_lease_fence_and_scope(
|
||||
storeapi.clone(),
|
||||
&backup_path,
|
||||
primary.clone(),
|
||||
sha256hex,
|
||||
revision.preconditions(),
|
||||
scanner_publication_lease_fence,
|
||||
publication_scope.clone(),
|
||||
)
|
||||
.await
|
||||
.await;
|
||||
if let Some(scope) = publication_scope {
|
||||
match scope.wait_for_completion().await {
|
||||
crate::storage_api::owner::ScannerPublicationCommitState::Committed
|
||||
| crate::storage_api::owner::ScannerPublicationCommitState::AbortedBeforeCommit => save_result,
|
||||
crate::storage_api::owner::ScannerPublicationCommitState::Indeterminate
|
||||
| crate::storage_api::owner::ScannerPublicationCommitState::Admitted
|
||||
| crate::storage_api::owner::ScannerPublicationCommitState::InFlight => Err(EcstoreError::other(
|
||||
"scanner backup publication commit scope did not reach a safe terminal state",
|
||||
)),
|
||||
}
|
||||
} else {
|
||||
save_result
|
||||
}
|
||||
};
|
||||
|
||||
match save_result {
|
||||
@@ -1546,26 +1604,16 @@ async fn run_data_scanner_cycle_with_budget(
|
||||
remote_lease_fence.is_some(),
|
||||
))
|
||||
.then_some(ScannerCycleDeferReason::ActivityBaselineUnavailable);
|
||||
let remote_lease_covers_persistence = remote_lease_deadline.is_none_or(|deadline| {
|
||||
std::time::Instant::now()
|
||||
.checked_add(usage_persist_timeout)
|
||||
.is_some_and(|latest_finish| latest_finish < deadline)
|
||||
});
|
||||
let publication_defer_reason = publication_defer_reason
|
||||
.or(remote_lease_defer_reason)
|
||||
.or(remote_lease_fence_defer_reason);
|
||||
let publication_defer_reason = (!remote_lease_covers_persistence)
|
||||
.then_some(ScannerCycleDeferReason::PublicationLeaseDeadlineExceeded)
|
||||
.or(publication_defer_reason);
|
||||
// Include reasons discovered while acquiring or validating remote leases.
|
||||
// In particular, the static budget gate above is reached after the scan
|
||||
// result is classified, so computing this flag earlier would suppress its
|
||||
// deferred metric.
|
||||
let publication_deferred = publication_defer_reason.is_some();
|
||||
let budget_elapsed = cycle_budget.budget_elapsed() && !ctx.is_cancelled();
|
||||
let remote_lease_probe = remote_publication_leases
|
||||
.as_ref()
|
||||
.map(|(notification_system, grants)| (Arc::clone(notification_system), grants.clone()));
|
||||
let remote_lease_release_safe = Arc::new(AtomicBool::new(true));
|
||||
let mut usage_persist_outcome = match publication_defer_reason {
|
||||
Some(reason) => {
|
||||
drop(receiver);
|
||||
@@ -1579,6 +1627,11 @@ async fn run_data_scanner_cycle_with_budget(
|
||||
let ctx_clone = ctx.clone();
|
||||
let route_probe_store = storeapi.clone();
|
||||
let remote_lease_fence = remote_lease_fence.clone();
|
||||
let remote_lease_release_safe_for_task = Arc::clone(&remote_lease_release_safe);
|
||||
let remote_lease_tokens = remote_publication_leases
|
||||
.as_ref()
|
||||
.map(|(_, grants)| grants.iter().map(|grant| grant.lease.token).collect())
|
||||
.unwrap_or_default();
|
||||
let mut usage_persist_task = AbortOnDropHandle::new(tokio::spawn(async move {
|
||||
store_data_usage_in_backend_with_outcome_for_epoch_and_baseline_and_route_probe_for_publication_epoch_and_lease_fence(
|
||||
ctx_clone,
|
||||
@@ -1590,7 +1643,9 @@ async fn run_data_scanner_cycle_with_budget(
|
||||
publication_epoch,
|
||||
remote_lease_deadline,
|
||||
remote_lease_fence,
|
||||
),
|
||||
)
|
||||
.with_remote_lease_tokens(remote_lease_tokens)
|
||||
.with_lease_release_flag(remote_lease_release_safe_for_task),
|
||||
move || {
|
||||
let storeapi = route_probe_store.clone();
|
||||
let remote_lease_probe = remote_lease_probe.clone();
|
||||
@@ -1655,7 +1710,16 @@ async fn run_data_scanner_cycle_with_budget(
|
||||
let lease_expired = remote_publication_leases
|
||||
.as_ref()
|
||||
.is_some_and(|(_, grants)| grants.iter().any(|grant| !grant.lease.is_valid()));
|
||||
if let Some((notification_system, grants)) = remote_publication_leases.take() {
|
||||
if !remote_lease_release_safe.load(Ordering::Acquire) {
|
||||
// A cancelled or detached storage mutation did not report a safe
|
||||
// terminal state. Keep remote grants until their own expiry rather
|
||||
// than releasing movement admission while a commit may be unknown.
|
||||
usage_persist_outcome = if usage_persist_outcome == DataUsagePersistOutcome::Failed {
|
||||
DataUsagePersistOutcome::Failed
|
||||
} else {
|
||||
DataUsagePersistOutcome::Deferred(ScannerCycleDeferReason::PublicationLeaseDeadlineExceeded)
|
||||
};
|
||||
} else if let Some((notification_system, grants)) = remote_publication_leases.take() {
|
||||
let release_result = notification_system.release_scanner_publication_leases(grants).await;
|
||||
let lease_release_failed = release_result.is_err();
|
||||
if lease_expired || lease_release_failed {
|
||||
|
||||
@@ -352,6 +352,7 @@ struct MemoryConfigStore {
|
||||
objects: Mutex<HashMap<String, Vec<u8>>>,
|
||||
revisions: Mutex<HashMap<String, u64>>,
|
||||
insert_after_gets: Mutex<HashMap<String, Vec<u8>>>,
|
||||
delayed_gets: Mutex<HashMap<String, Duration>>,
|
||||
non_regular_objects: Mutex<HashSet<String>>,
|
||||
fail_put_number: Mutex<HashMap<String, usize>>,
|
||||
object_not_found_put_number: Mutex<HashMap<String, usize>>,
|
||||
@@ -399,6 +400,9 @@ impl crate::storage_api::scanner_io::ObjectIO for MemoryConfigStore {
|
||||
_opts: &ObjectOptions,
|
||||
) -> EcstoreResult<GetObjectReader> {
|
||||
let key = memory_config_key(bucket, object);
|
||||
if let Some(delay) = self.delayed_gets.lock().await.remove(&key) {
|
||||
tokio::time::sleep(delay).await;
|
||||
}
|
||||
let inserted_data = self.insert_after_gets.lock().await.remove(&key);
|
||||
let data = {
|
||||
let mut objects = self.objects.lock().await;
|
||||
@@ -3532,6 +3536,47 @@ async fn coordinator_classifies_an_expired_publication_lease() {
|
||||
assert!(store.put_counts.lock().await.is_empty(), "expired lease must prevent a PUT");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn backup_sync_checks_the_lease_deadline_after_a_slow_backup_read() {
|
||||
let store = Arc::new(MemoryConfigStore::default());
|
||||
let primary_path = DATA_USAGE_OBJ_NAME_PATH.as_str();
|
||||
let backup_path = format!("{primary_path}.bkp");
|
||||
let primary_key = memory_config_key(RUSTFS_META_BUCKET, primary_path);
|
||||
let backup_key = memory_config_key(RUSTFS_META_BUCKET, &backup_path);
|
||||
let primary = complete_usage_with_bucket_count(Some(std::time::SystemTime::UNIX_EPOCH), 0);
|
||||
store
|
||||
.objects
|
||||
.lock()
|
||||
.await
|
||||
.insert(primary_key, serde_json::to_vec(&primary).expect("primary usage snapshot should encode"));
|
||||
store
|
||||
.delayed_gets
|
||||
.lock()
|
||||
.await
|
||||
.insert(backup_key.clone(), Duration::from_millis(20));
|
||||
|
||||
// The primary read is allowed to start, but the backup read consumes the
|
||||
// remaining lease window. The second deadline check must prevent a stale
|
||||
// backup PUT after that window has elapsed.
|
||||
let deadline = std::time::Instant::now()
|
||||
.checked_add(std::time::Duration::from_millis(5))
|
||||
.expect("test deadline should support a five-millisecond window");
|
||||
let result = sync_data_usage_backup_from_primary_for_epoch_and_lease_and_fence(
|
||||
&CancellationToken::new(),
|
||||
store.clone(),
|
||||
None,
|
||||
Some(deadline),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(scanner_publication_epoch_changed(
|
||||
&result.expect_err("an expired backup lease must defer publication")
|
||||
));
|
||||
assert!(!store.objects.lock().await.contains_key(&backup_key));
|
||||
assert_eq!(store.put_counts.lock().await.get(&backup_key), None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_deferred_usage_save_keeps_last_real_save_metric() {
|
||||
@@ -4783,6 +4828,29 @@ async fn data_usage_persist_wait_aborts_after_timeout() {
|
||||
assert!(task.is_finished());
|
||||
}
|
||||
|
||||
#[tokio::test(start_paused = true)]
|
||||
async fn data_usage_persist_timeout_drops_owned_task_without_a_late_commit() {
|
||||
let ctx = CancellationToken::new();
|
||||
let commit_started = Arc::new(AtomicBool::new(false));
|
||||
let commit_started_by_task = commit_started.clone();
|
||||
let task_ready = Arc::new(tokio::sync::Notify::new());
|
||||
let task_ready_by_task = task_ready.clone();
|
||||
let mut task = AbortOnDropHandle::new(tokio::spawn(async move {
|
||||
task_ready_by_task.notify_one();
|
||||
std::future::pending::<()>().await;
|
||||
commit_started_by_task.store(true, Ordering::Release);
|
||||
DataUsagePersistOutcome::Saved
|
||||
}));
|
||||
task_ready.notified().await;
|
||||
|
||||
let result = wait_for_data_usage_persist_task(&ctx, &mut task, Duration::from_secs(1)).await;
|
||||
|
||||
assert!(matches!(result, DataUsagePersistTaskResult::TimedOut));
|
||||
assert!(task.is_finished(), "the timed-out persistence task must be drained before return");
|
||||
tokio::task::yield_now().await;
|
||||
assert!(!commit_started.load(Ordering::Acquire), "an owned task must not commit after its timeout");
|
||||
}
|
||||
|
||||
#[tokio::test(start_paused = true)]
|
||||
async fn maintenance_feature_inspection_preserves_base_cycle_after_timeout() {
|
||||
let ctx = CancellationToken::new();
|
||||
|
||||
@@ -13,7 +13,10 @@
|
||||
// limitations under the License.
|
||||
/// Data-usage snapshot persistence: CAS store pipeline, epoch baselines, and observed-snapshot cleanup.
|
||||
use super::*;
|
||||
use crate::storage_api::owner::ScannerPublicationCommitState;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub(super) enum DataUsagePersistOutcome {
|
||||
@@ -34,6 +37,16 @@ fn remote_lease_expired(deadline: Option<std::time::Instant>) -> bool {
|
||||
deadline.is_some_and(|deadline| std::time::Instant::now() >= deadline)
|
||||
}
|
||||
|
||||
pub(super) fn scanner_publication_scope_deadline(
|
||||
persist_timeout: Duration,
|
||||
remote_lease_deadline: Option<std::time::Instant>,
|
||||
) -> tokio::time::Instant {
|
||||
let configured_deadline = tokio::time::Instant::now() + persist_timeout;
|
||||
remote_lease_deadline
|
||||
.map(tokio::time::Instant::from_std)
|
||||
.map_or(configured_deadline, |lease_deadline| configured_deadline.min(lease_deadline))
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(super) struct DataUsagePersistBaseline {
|
||||
pub(super) data: Option<Bytes>,
|
||||
@@ -126,6 +139,8 @@ pub(super) struct ScannerPublicationFence {
|
||||
pub(super) expected_publication_epoch: Option<u64>,
|
||||
pub(super) remote_lease_deadline: Option<std::time::Instant>,
|
||||
pub(super) scanner_publication_lease_fence: Option<String>,
|
||||
pub(super) remote_lease_tokens: Vec<Uuid>,
|
||||
pub(super) lease_release_safe: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl ScannerPublicationFence {
|
||||
@@ -138,8 +153,20 @@ impl ScannerPublicationFence {
|
||||
expected_publication_epoch,
|
||||
remote_lease_deadline,
|
||||
scanner_publication_lease_fence,
|
||||
remote_lease_tokens: Vec::new(),
|
||||
lease_release_safe: Arc::new(AtomicBool::new(true)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn with_remote_lease_tokens(mut self, remote_lease_tokens: Vec<Uuid>) -> Self {
|
||||
self.remote_lease_tokens = remote_lease_tokens;
|
||||
self
|
||||
}
|
||||
|
||||
pub(super) fn with_lease_release_flag(mut self, lease_release_safe: Arc<AtomicBool>) -> Self {
|
||||
self.lease_release_safe = lease_release_safe;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -290,6 +317,8 @@ where
|
||||
expected_publication_epoch,
|
||||
remote_lease_deadline,
|
||||
scanner_publication_lease_fence,
|
||||
remote_lease_tokens,
|
||||
lease_release_safe,
|
||||
} = publication_fence;
|
||||
let mut outcome = DataUsagePersistOutcome::NoUpdate;
|
||||
let mut next_baseline = initial_baseline;
|
||||
@@ -580,25 +609,54 @@ where
|
||||
|
||||
let done_save = Metrics::time(Metric::SaveUsage);
|
||||
let save_result = {
|
||||
let Some(_publication_admission) =
|
||||
scanner_publication_admission_for_epoch(storeapi.clone(), publication_epoch_for_save).await
|
||||
else {
|
||||
done_save();
|
||||
break DataUsagePersistOutcome::Deferred(ScannerCycleDeferReason::DataMovement);
|
||||
let publication_scope = storeapi
|
||||
.scanner_data_usage_publication_commit_scope_with_release_flag(
|
||||
publication_epoch_for_save,
|
||||
scanner_publication_scope_deadline(data_usage_persist_timeout(), remote_lease_deadline),
|
||||
remote_lease_tokens.clone(),
|
||||
Arc::clone(&lease_release_safe),
|
||||
)
|
||||
.await;
|
||||
let legacy_publication_admission = if publication_scope.is_none() {
|
||||
let Some(admission) =
|
||||
scanner_publication_admission_for_epoch(storeapi.clone(), publication_epoch_for_save).await
|
||||
else {
|
||||
done_save();
|
||||
break DataUsagePersistOutcome::Deferred(ScannerCycleDeferReason::DataMovement);
|
||||
};
|
||||
Some(admission)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if remote_lease_expired(remote_lease_deadline) {
|
||||
done_save();
|
||||
break DataUsagePersistOutcome::Deferred(ScannerCycleDeferReason::PublicationLeaseDeadlineExceeded);
|
||||
}
|
||||
save_config_shared_with_preconditions_and_lease_fence(
|
||||
let save_result = crate::save_config_shared_with_preconditions_and_lease_fence_and_scope(
|
||||
storeapi.clone(),
|
||||
target_path,
|
||||
data.clone(),
|
||||
sha256hex.clone(),
|
||||
revision.preconditions(),
|
||||
scanner_publication_lease_fence.as_deref(),
|
||||
publication_scope.clone(),
|
||||
)
|
||||
.await
|
||||
.await;
|
||||
drop(legacy_publication_admission);
|
||||
if let Some(scope) = publication_scope {
|
||||
match scope.wait_for_completion().await {
|
||||
ScannerPublicationCommitState::Committed | ScannerPublicationCommitState::AbortedBeforeCommit => {
|
||||
save_result
|
||||
}
|
||||
ScannerPublicationCommitState::Indeterminate
|
||||
| ScannerPublicationCommitState::Admitted
|
||||
| ScannerPublicationCommitState::InFlight => Err(EcstoreError::other(
|
||||
"scanner publication commit scope did not reach a safe terminal state",
|
||||
)),
|
||||
}
|
||||
} else {
|
||||
save_result
|
||||
}
|
||||
};
|
||||
done_save();
|
||||
|
||||
@@ -696,6 +754,8 @@ where
|
||||
expected_publication_epoch,
|
||||
remote_lease_deadline,
|
||||
scanner_publication_lease_fence.as_deref(),
|
||||
&remote_lease_tokens,
|
||||
Arc::clone(&lease_release_safe),
|
||||
)
|
||||
.await;
|
||||
if expected_publication_epoch.is_some() && !cleanup_ok {
|
||||
@@ -719,6 +779,8 @@ where
|
||||
expected_publication_epoch,
|
||||
remote_lease_deadline,
|
||||
scanner_publication_lease_fence.as_deref(),
|
||||
&remote_lease_tokens,
|
||||
Arc::clone(&lease_release_safe),
|
||||
)
|
||||
.await;
|
||||
if expected_publication_epoch.is_some() && !cleanup_ok {
|
||||
@@ -761,6 +823,8 @@ where
|
||||
expected_publication_epoch,
|
||||
remote_lease_deadline,
|
||||
scanner_publication_lease_fence.as_deref(),
|
||||
&remote_lease_tokens,
|
||||
Arc::clone(&lease_release_safe),
|
||||
)
|
||||
.await;
|
||||
if expected_publication_epoch.is_some() && !cleanup_ok {
|
||||
@@ -778,12 +842,14 @@ where
|
||||
|
||||
if backup_due {
|
||||
let done_save = Metrics::time(Metric::SaveUsage);
|
||||
let backup_result = sync_data_usage_backup_from_primary_for_epoch_and_lease_and_fence(
|
||||
let backup_result = sync_data_usage_backup_from_primary_for_epoch_and_lease_and_fence_and_scope(
|
||||
&ctx,
|
||||
storeapi.clone(),
|
||||
expected_publication_epoch,
|
||||
remote_lease_deadline,
|
||||
scanner_publication_lease_fence.as_deref(),
|
||||
remote_lease_tokens.clone(),
|
||||
Arc::clone(&lease_release_safe),
|
||||
)
|
||||
.await;
|
||||
done_save();
|
||||
@@ -817,6 +883,8 @@ async fn cleanup_observed_data_usage_snapshot_for_epoch_and_lease(
|
||||
expected_publication_epoch: Option<u64>,
|
||||
remote_lease_deadline: Option<std::time::Instant>,
|
||||
scanner_publication_lease_fence: Option<&str>,
|
||||
remote_lease_tokens: &[Uuid],
|
||||
lease_release_safe: Arc<AtomicBool>,
|
||||
) -> bool {
|
||||
if remote_lease_expired(remote_lease_deadline) {
|
||||
return false;
|
||||
@@ -885,7 +953,15 @@ async fn cleanup_observed_data_usage_snapshot_for_epoch_and_lease(
|
||||
return false;
|
||||
}
|
||||
|
||||
let result = delete_config_with_publication_admission_for_epoch(
|
||||
let publication_scope = storeapi
|
||||
.scanner_data_usage_publication_commit_scope_with_release_flag(
|
||||
read_epoch,
|
||||
scanner_publication_scope_deadline(data_usage_persist_timeout(), remote_lease_deadline),
|
||||
remote_lease_tokens.to_vec(),
|
||||
Arc::clone(&lease_release_safe),
|
||||
)
|
||||
.await;
|
||||
let result = crate::delete_config_with_publication_scope_for_epoch(
|
||||
storeapi,
|
||||
RUSTFS_META_BUCKET,
|
||||
DATA_USAGE_OBSERVED_OBJ_NAME_PATH.as_str(),
|
||||
@@ -904,9 +980,23 @@ async fn cleanup_observed_data_usage_snapshot_for_epoch_and_lease(
|
||||
..Default::default()
|
||||
},
|
||||
read_epoch,
|
||||
publication_scope.clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let result = if let Some(scope) = publication_scope {
|
||||
match scope.wait_for_completion().await {
|
||||
ScannerPublicationCommitState::Committed | ScannerPublicationCommitState::AbortedBeforeCommit => result,
|
||||
ScannerPublicationCommitState::Indeterminate
|
||||
| ScannerPublicationCommitState::Admitted
|
||||
| ScannerPublicationCommitState::InFlight => Err(EcstoreError::other(
|
||||
"scanner publication cleanup scope did not reach a safe terminal state",
|
||||
)),
|
||||
}
|
||||
} else {
|
||||
result
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(_)
|
||||
| Err(
|
||||
|
||||
@@ -93,7 +93,9 @@ pub(crate) use rustfs_ecstore::api::layout::{
|
||||
EndpointServerPools as EcstoreEndpointServerPools, Endpoints as EcstoreEndpoints, PoolEndpoints as EcstorePoolEndpoints,
|
||||
};
|
||||
pub(crate) use rustfs_ecstore::api::notification::scanner_peer_transport_error_message_is_retryable;
|
||||
pub(crate) use rustfs_ecstore::api::object::SCANNER_PUBLICATION_LEASE_FENCE_METADATA_KEY;
|
||||
pub(crate) use rustfs_ecstore::api::object::{
|
||||
SCANNER_PUBLICATION_LEASE_FENCE_METADATA_KEY, ScannerPublicationCommitScope, ScannerPublicationCommitState,
|
||||
};
|
||||
#[cfg(test)]
|
||||
pub(crate) use rustfs_ecstore::api::rebalance::{
|
||||
RebalStatus as EcstoreRebalStatus, RebalanceInfo as EcstoreRebalanceInfo, RebalanceMeta as EcstoreRebalanceMeta,
|
||||
@@ -126,14 +128,15 @@ pub(crate) mod owner {
|
||||
EcstoreNsScannerOpenRequest, EcstoreObjectLockConfiguration, EcstoreObjectOpts, EcstoreReplicationConfigurationExt,
|
||||
EcstoreReplicationScannerBridge, EcstoreResultType, EcstoreScanGuard, EcstoreSetDisks, EcstoreStorageError, EcstoreStore,
|
||||
EcstoreVersioningApi, EcstoreVersioningConfiguration, SCANNER_PUBLICATION_LEASE_FENCE_METADATA_KEY,
|
||||
ScannerReplicationHealObject, ScannerReplicationHealResult, ScannerReplicationQueueAdmission, ecstore_apply_expiry_rule,
|
||||
ecstore_apply_transition_rule, ecstore_expiry_state_handle, ecstore_get_global_tier_config_mgr,
|
||||
ecstore_get_lifecycle_config, ecstore_get_object_lock_config, ecstore_get_replication_config,
|
||||
ecstore_invalidate_admin_data_usage_snapshot_cache, ecstore_invalidate_data_usage_snapshot_cache, ecstore_is_erasure,
|
||||
ecstore_is_erasure_sd, ecstore_is_reserved_or_invalid_bucket, ecstore_list_path_raw,
|
||||
ecstore_object_opts_from_object_info, ecstore_path2_bucket_object, ecstore_path2_bucket_object_with_base_path,
|
||||
ecstore_read_config, ecstore_replace_bucket_usage_memory_from_info, ecstore_resolve_object_store_handle,
|
||||
ecstore_save_config, ecstore_send_event, scanner_replication_config_for_lifecycle_eval,
|
||||
ScannerPublicationCommitScope, ScannerPublicationCommitState, ScannerReplicationHealObject, ScannerReplicationHealResult,
|
||||
ScannerReplicationQueueAdmission, ecstore_apply_expiry_rule, ecstore_apply_transition_rule, ecstore_expiry_state_handle,
|
||||
ecstore_get_global_tier_config_mgr, ecstore_get_lifecycle_config, ecstore_get_object_lock_config,
|
||||
ecstore_get_replication_config, ecstore_invalidate_admin_data_usage_snapshot_cache,
|
||||
ecstore_invalidate_data_usage_snapshot_cache, ecstore_is_erasure, ecstore_is_erasure_sd,
|
||||
ecstore_is_reserved_or_invalid_bucket, ecstore_list_path_raw, ecstore_object_opts_from_object_info,
|
||||
ecstore_path2_bucket_object, ecstore_path2_bucket_object_with_base_path, ecstore_read_config,
|
||||
ecstore_replace_bucket_usage_memory_from_info, ecstore_resolve_object_store_handle, ecstore_save_config,
|
||||
ecstore_send_event, scanner_replication_config_for_lifecycle_eval,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in New Issue
Block a user