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:
houseme
2026-08-30 10:39:07 +08:00
committed by GitHub
parent 90ab2e24c3
commit ee39e4fccb
15 changed files with 1360 additions and 114 deletions
+159 -1
View File
@@ -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()));