mirror of
https://github.com/rustfs/rustfs.git
synced 2026-09-02 18:28:11 +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:
@@ -58,7 +58,7 @@ use crate::{
|
||||
core::sets::Sets,
|
||||
disk::{BUCKET_META_PREFIX, DiskOption, DiskStore, RUSTFS_META_BUCKET},
|
||||
layout::endpoints::EndpointServerPools,
|
||||
object_api::{GetObjectReader, ObjectInfo, ObjectOptions, PutObjReader},
|
||||
object_api::{GetObjectReader, ObjectInfo, ObjectOptions, PutObjReader, ScannerPublicationCommitScope},
|
||||
};
|
||||
use futures::future::join_all;
|
||||
use http::HeaderMap;
|
||||
@@ -522,6 +522,48 @@ impl ECStore {
|
||||
Some((operation_guard, self.ctx.data_movement_operation_epoch()))
|
||||
}
|
||||
|
||||
/// Acquire a storage-owned scanner publication scope. Unlike the legacy
|
||||
/// admission helper, the movement permit is owned by the returned scope
|
||||
/// and therefore survives cancellation of the scanner coordinator while
|
||||
/// the actual metadata mutation drains.
|
||||
pub 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> {
|
||||
let (movement_permit, epoch) = self.scanner_data_usage_publication_admission_guard().await?;
|
||||
if epoch != expected_movement_epoch {
|
||||
return None;
|
||||
}
|
||||
Some(ScannerPublicationCommitScope::new_storage_owned(
|
||||
epoch,
|
||||
safe_deadline,
|
||||
remote_lease_tokens,
|
||||
movement_permit,
|
||||
))
|
||||
}
|
||||
|
||||
pub 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> {
|
||||
let (movement_permit, epoch) = self.scanner_data_usage_publication_admission_guard().await?;
|
||||
if epoch != expected_movement_epoch {
|
||||
return None;
|
||||
}
|
||||
Some(ScannerPublicationCommitScope::new_storage_owned_with_release_flag(
|
||||
epoch,
|
||||
safe_deadline,
|
||||
remote_lease_tokens,
|
||||
movement_permit,
|
||||
lease_release_safe,
|
||||
))
|
||||
}
|
||||
|
||||
/// Capture the current publication epoch without holding the movement
|
||||
/// gate across backend I/O. Callers must re-admit the same epoch before a
|
||||
/// mutation commits.
|
||||
@@ -1409,9 +1451,29 @@ mod tests {
|
||||
.await
|
||||
.expect("movement writer should proceed after lease expiry")
|
||||
.expect("expiry writer task should not panic");
|
||||
assert!(
|
||||
store.validate_scanner_publication_lease(expiring_token, 0).await.is_err(),
|
||||
"an expired lease must not validate after its read guard is released"
|
||||
);
|
||||
assert!(!store.release_scanner_publication_lease(expiring_token).await);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn scanner_publication_lease_rejects_a_new_movement_generation() {
|
||||
let store = build_store_with_ctx(Arc::new(InstanceContext::new()));
|
||||
let (token, generation) = store
|
||||
.acquire_scanner_publication_lease(0, crate::runtime::instance::SCANNER_PUBLICATION_LEASE_TTL)
|
||||
.await
|
||||
.expect("an idle store should grant a publication lease");
|
||||
|
||||
assert_eq!(store.ctx.advance_data_movement_generation(), Some(1));
|
||||
assert!(
|
||||
store.validate_scanner_publication_lease(token, generation).await.is_err(),
|
||||
"a lease from the prior movement generation must fail closed"
|
||||
);
|
||||
assert!(store.release_scanner_publication_lease(token).await);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn scanner_publication_lease_rejects_stale_generation_before_install() {
|
||||
let store = build_store_with_ctx(Arc::new(InstanceContext::new()));
|
||||
@@ -1422,6 +1484,102 @@ mod tests {
|
||||
assert!(error.to_string().contains("generation is stale"));
|
||||
}
|
||||
|
||||
#[tokio::test(start_paused = true)]
|
||||
async fn scanner_publication_commit_scope_owns_permit_until_terminal_drain() {
|
||||
let store = build_store_with_ctx(Arc::new(InstanceContext::new()));
|
||||
let scope = store
|
||||
.scanner_data_usage_publication_commit_scope(
|
||||
0,
|
||||
tokio::time::Instant::now() + Duration::from_secs(30),
|
||||
vec![Uuid::new_v4()],
|
||||
)
|
||||
.await
|
||||
.expect("idle storage should grant a publication scope");
|
||||
assert_eq!(scope.state(), crate::object_api::ScannerPublicationCommitState::Admitted);
|
||||
assert_eq!(scope.remote_lease_tokens().len(), 1);
|
||||
|
||||
let gate = store.ctx.data_movement_operation_gate();
|
||||
let writer = tokio::spawn(async move { gate.write_owned().await });
|
||||
tokio::task::yield_now().await;
|
||||
assert!(!writer.is_finished(), "the scope must own its movement permit after the caller returns");
|
||||
|
||||
scope.cancel();
|
||||
assert!(scope.mark_aborted_before_commit());
|
||||
assert_eq!(
|
||||
scope.wait_for_completion().await,
|
||||
crate::object_api::ScannerPublicationCommitState::AbortedBeforeCommit
|
||||
);
|
||||
assert!(scope.release_movement_permit().await);
|
||||
tokio::time::timeout(Duration::from_secs(1), writer)
|
||||
.await
|
||||
.expect("movement writer should proceed after the scope drains")
|
||||
.expect("movement writer task should not panic");
|
||||
}
|
||||
|
||||
#[tokio::test(start_paused = true)]
|
||||
async fn scanner_publication_commit_scope_rejects_late_start_and_keeps_indeterminate_permit() {
|
||||
let store = build_store_with_ctx(Arc::new(InstanceContext::new()));
|
||||
let scope = store
|
||||
.scanner_data_usage_publication_commit_scope(0, tokio::time::Instant::now() + Duration::from_secs(1), Vec::new())
|
||||
.await
|
||||
.expect("idle storage should grant a publication scope");
|
||||
tokio::time::advance(Duration::from_secs(1)).await;
|
||||
assert_eq!(
|
||||
scope.try_begin(),
|
||||
Err(crate::object_api::ScannerPublicationCommitStartError::DeadlineExceeded)
|
||||
);
|
||||
assert!(
|
||||
!scope.release_movement_permit().await,
|
||||
"an admitted scope is not safe to release before owner resolution"
|
||||
);
|
||||
assert!(scope.mark_aborted_before_commit());
|
||||
assert!(scope.release_movement_permit().await);
|
||||
|
||||
let scope = store
|
||||
.scanner_data_usage_publication_commit_scope(0, tokio::time::Instant::now() + Duration::from_secs(30), Vec::new())
|
||||
.await
|
||||
.expect("a second idle publication scope should be granted");
|
||||
scope.try_begin().expect("scope should enter the mutation state");
|
||||
scope.cancel();
|
||||
assert!(scope.mark_indeterminate());
|
||||
assert_eq!(
|
||||
scope.wait_for_completion().await,
|
||||
crate::object_api::ScannerPublicationCommitState::Indeterminate
|
||||
);
|
||||
assert!(!scope.release_movement_permit().await, "indeterminate mutation must retain the permit");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn scanner_publication_scope_guard_classifies_early_returns_conservatively() {
|
||||
let store = build_store_with_ctx(Arc::new(InstanceContext::new()));
|
||||
let permit = store.ctx.data_movement_operation_gate().read_owned().await;
|
||||
let scope = ScannerPublicationCommitScope::new_storage_owned(
|
||||
0,
|
||||
tokio::time::Instant::now() + Duration::from_secs(30),
|
||||
Vec::new(),
|
||||
permit,
|
||||
);
|
||||
{
|
||||
let _guard = crate::object_api::ScannerPublicationCommitScopeGuard::new(scope.clone());
|
||||
}
|
||||
assert_eq!(scope.state(), crate::object_api::ScannerPublicationCommitState::AbortedBeforeCommit);
|
||||
assert!(scope.release_movement_permit().await);
|
||||
|
||||
let permit = store.ctx.data_movement_operation_gate().read_owned().await;
|
||||
let scope = ScannerPublicationCommitScope::new_storage_owned(
|
||||
0,
|
||||
tokio::time::Instant::now() + Duration::from_secs(30),
|
||||
Vec::new(),
|
||||
permit,
|
||||
);
|
||||
scope.try_begin().expect("scope should enter the mutation state");
|
||||
{
|
||||
let _guard = crate::object_api::ScannerPublicationCommitScopeGuard::new(scope.clone());
|
||||
}
|
||||
assert_eq!(scope.state(), crate::object_api::ScannerPublicationCommitState::Indeterminate);
|
||||
assert!(!scope.release_movement_permit().await);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn scanner_target_guard_keeps_movement_writer_fenced_after_lease_release() {
|
||||
let store = build_store_with_ctx(Arc::new(InstanceContext::new()));
|
||||
|
||||
Reference in New Issue
Block a user