mirror of
https://github.com/rustfs/rustfs.git
synced 2026-10-13 08:02:03 +00:00
fix(ci): preserve heal quorum retries and synchronize background tests (#8422)
This commit is contained in:
@@ -407,11 +407,15 @@ async fn wait_for_restore_completion(
|
||||
// runtime (`worker_threads = 1`), so no concurrent test can mutate process environment during the
|
||||
// `env::set_var` / `env::remove_var` window.
|
||||
#[allow(unsafe_code)]
|
||||
async fn with_forced_immediate_enqueue_timeout<F, Fut>(test_fn: F)
|
||||
async fn with_forced_immediate_enqueue_timeout<F, Fut>(backend: &MockWarmBackend, test_fn: F)
|
||||
where
|
||||
F: FnOnce() -> Fut,
|
||||
Fut: std::future::Future<Output = ()>,
|
||||
{
|
||||
let transition_state = get_global_transition_state();
|
||||
let missed_before = transition_state.missed_immediate_tasks();
|
||||
let compensation_before = transition_state.compensation_scheduled_tasks();
|
||||
let put_barrier = backend.arm_put_barrier().await;
|
||||
let original = env::var_os(ENV_TEST_FORCE_IMMEDIATE_TRANSITION_ENQUEUE_TIMEOUT);
|
||||
unsafe {
|
||||
env::set_var(ENV_TEST_FORCE_IMMEDIATE_TRANSITION_ENQUEUE_TIMEOUT, "1");
|
||||
@@ -428,6 +432,20 @@ where
|
||||
if let Err(err) = result {
|
||||
std::panic::resume_unwind(err);
|
||||
}
|
||||
|
||||
assert!(
|
||||
transition_state.missed_immediate_tasks() > missed_before,
|
||||
"the operation should exercise the forced immediate enqueue failure"
|
||||
);
|
||||
assert!(
|
||||
transition_state.compensation_scheduled_tasks() > compensation_before,
|
||||
"the operation should schedule automatic compensation backfill"
|
||||
);
|
||||
|
||||
// Separate automatic discovery and queue latency from the metadata commit deadline.
|
||||
put_barrier.wait_until_paused().await;
|
||||
assert_eq!(backend.object_count().await, 1, "compensation should store the body before committing");
|
||||
put_barrier.release();
|
||||
}
|
||||
|
||||
async fn with_get_codec_streaming_remote_probe_env<F, Fut>(test_fn: F)
|
||||
@@ -1641,7 +1659,7 @@ async fn immediate_transition_timeout_eventually_completes_via_compensation() {
|
||||
.await
|
||||
.expect("Failed to set lifecycle configuration");
|
||||
|
||||
with_forced_immediate_enqueue_timeout(|| async {
|
||||
with_forced_immediate_enqueue_timeout(&backend, || async {
|
||||
let _ = upload_test_object(&ecstore, bucket.as_str(), object, payload).await;
|
||||
})
|
||||
.await;
|
||||
@@ -1650,6 +1668,7 @@ async fn immediate_transition_timeout_eventually_completes_via_compensation() {
|
||||
.await
|
||||
.expect("object should eventually transition after compensation backfill");
|
||||
|
||||
assert_eq!(backend.put_count().await, 1, "compensation should upload the version once");
|
||||
assert_eq!(info.transitioned_object.status, "complete");
|
||||
assert_eq!(info.transitioned_object.tier, tier_name);
|
||||
assert!(backend.contains(&info.transitioned_object.name).await);
|
||||
@@ -1689,37 +1708,13 @@ async fn compensation_driven_copy_still_completes_transition() {
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let transition_state = get_global_transition_state();
|
||||
let missed_before = transition_state.missed_immediate_tasks();
|
||||
let compensation_before = transition_state.compensation_scheduled_tasks();
|
||||
let put_barrier = backend.arm_put_barrier().await;
|
||||
|
||||
with_forced_immediate_enqueue_timeout(|| async {
|
||||
with_forced_immediate_enqueue_timeout(&backend, || async {
|
||||
Box::pin(usecase.execute_copy_object(build_request(copy_input, Method::PUT)))
|
||||
.await
|
||||
.expect("Failed to copy object through usecase");
|
||||
})
|
||||
.await;
|
||||
|
||||
assert!(
|
||||
transition_state.missed_immediate_tasks() > missed_before,
|
||||
"copy should exercise the forced immediate enqueue failure"
|
||||
);
|
||||
assert!(
|
||||
transition_state.compensation_scheduled_tasks() > compensation_before,
|
||||
"copy should schedule automatic compensation backfill"
|
||||
);
|
||||
|
||||
// Observe automatic compensation reaching the tier before timing its final
|
||||
// metadata commit; discovery and queue latency are separate from that commit.
|
||||
put_barrier.wait_until_paused().await;
|
||||
assert_eq!(
|
||||
backend.object_count().await,
|
||||
1,
|
||||
"compensation should store the copied body before committing"
|
||||
);
|
||||
put_barrier.release();
|
||||
|
||||
let info = wait_for_transition(&ecstore, dst_bucket.as_str(), dst_object, TRANSITION_WAIT_TIMEOUT)
|
||||
.await
|
||||
.expect("copied object should eventually transition after compensation backfill");
|
||||
@@ -1792,7 +1787,7 @@ async fn compensation_driven_complete_multipart_upload_still_transitions_inner()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
with_forced_immediate_enqueue_timeout(|| async {
|
||||
with_forced_immediate_enqueue_timeout(&backend, || async {
|
||||
Box::pin(usecase.execute_complete_multipart_upload(build_request(complete_input, Method::POST)))
|
||||
.await
|
||||
.expect("Failed to complete multipart upload through usecase");
|
||||
@@ -1803,6 +1798,7 @@ async fn compensation_driven_complete_multipart_upload_still_transitions_inner()
|
||||
.await
|
||||
.expect("multipart object should eventually transition after compensation backfill");
|
||||
|
||||
assert_eq!(backend.put_count().await, 1, "compensation should upload the version once");
|
||||
assert_eq!(info.transitioned_object.status, "complete");
|
||||
assert_eq!(info.transitioned_object.tier, tier_name);
|
||||
assert!(backend.contains(&info.transitioned_object.name).await);
|
||||
@@ -1829,7 +1825,7 @@ async fn compensation_driven_transition_still_cleans_remote_tier_on_delete() {
|
||||
.await
|
||||
.expect("Failed to set lifecycle configuration");
|
||||
|
||||
with_forced_immediate_enqueue_timeout(|| async {
|
||||
with_forced_immediate_enqueue_timeout(&backend, || async {
|
||||
let _ = upload_test_object(&ecstore, bucket.as_str(), object, payload).await;
|
||||
})
|
||||
.await;
|
||||
@@ -1837,6 +1833,7 @@ async fn compensation_driven_transition_still_cleans_remote_tier_on_delete() {
|
||||
let transitioned = wait_for_transition(&ecstore, bucket.as_str(), object, TRANSITION_WAIT_TIMEOUT)
|
||||
.await
|
||||
.expect("object should eventually transition after compensation backfill");
|
||||
assert_eq!(backend.put_count().await, 1, "compensation should upload the version once");
|
||||
let remote_object = transitioned.transitioned_object.name.clone();
|
||||
|
||||
assert!(backend.contains(&remote_object).await);
|
||||
@@ -1890,7 +1887,7 @@ async fn compensation_driven_versioned_delete_still_creates_delete_marker() {
|
||||
.await
|
||||
.expect("Failed to set lifecycle configuration");
|
||||
|
||||
with_forced_immediate_enqueue_timeout(|| async {
|
||||
with_forced_immediate_enqueue_timeout(&backend, || async {
|
||||
let _ = upload_test_object(&ecstore, bucket.as_str(), object, payload).await;
|
||||
})
|
||||
.await;
|
||||
@@ -1898,6 +1895,7 @@ async fn compensation_driven_versioned_delete_still_creates_delete_marker() {
|
||||
let transitioned = wait_for_transition(&ecstore, bucket.as_str(), object, TRANSITION_WAIT_TIMEOUT)
|
||||
.await
|
||||
.expect("object should eventually transition after compensation backfill");
|
||||
assert_eq!(backend.put_count().await, 1, "compensation should upload the version once");
|
||||
let remote_object = transitioned.transitioned_object.name.clone();
|
||||
|
||||
assert!(backend.contains(&remote_object).await);
|
||||
@@ -1945,7 +1943,7 @@ async fn compensation_driven_delete_marker_still_honors_lifecycle_cleanup() {
|
||||
.await
|
||||
.expect("Failed to set transition lifecycle configuration");
|
||||
|
||||
with_forced_immediate_enqueue_timeout(|| async {
|
||||
with_forced_immediate_enqueue_timeout(&backend, || async {
|
||||
let _ = upload_test_object(&ecstore, bucket.as_str(), object, payload).await;
|
||||
})
|
||||
.await;
|
||||
@@ -1953,6 +1951,7 @@ async fn compensation_driven_delete_marker_still_honors_lifecycle_cleanup() {
|
||||
let transitioned = wait_for_transition(&ecstore, bucket.as_str(), object, TRANSITION_WAIT_TIMEOUT)
|
||||
.await
|
||||
.expect("object should eventually transition after compensation backfill");
|
||||
assert_eq!(backend.put_count().await, 1, "compensation should upload the version once");
|
||||
let remote_object = transitioned.transitioned_object.name.clone();
|
||||
|
||||
assert!(backend.contains(&remote_object).await);
|
||||
|
||||
@@ -701,6 +701,65 @@ mod tests {
|
||||
})
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn consent_revoke_cancels_retry_and_persists_receipt() {
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use tokio::sync::Notify;
|
||||
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = StateStore::new(temp.path());
|
||||
let (policy_tx, policy_rx) = watch::channel(policy(4, true));
|
||||
let shutdown = CancellationToken::new();
|
||||
let attempts = Arc::new(AtomicUsize::new(0));
|
||||
let release = Arc::new(Notify::new());
|
||||
let failed = Arc::new(Notify::new());
|
||||
let runner: Runner = Arc::new({
|
||||
let attempts = Arc::clone(&attempts);
|
||||
let release = Arc::clone(&release);
|
||||
let failed = Arc::clone(&failed);
|
||||
move |_| {
|
||||
let attempts = Arc::clone(&attempts);
|
||||
let release = Arc::clone(&release);
|
||||
let failed = Arc::clone(&failed);
|
||||
Box::pin(async move {
|
||||
attempts.fetch_add(1, Ordering::SeqCst);
|
||||
release.notified().await;
|
||||
failed.notify_one();
|
||||
Err(DiagnosticScheduleError::InventoryUnavailable)
|
||||
})
|
||||
}
|
||||
});
|
||||
let mut runtime = spawn_schedule(store.clone(), policy_rx, shutdown.clone(), runner);
|
||||
let mut receipts = runtime.receipts();
|
||||
wait_running(&mut runtime.status).await;
|
||||
|
||||
// Pause only after real state IO completes, then revoke within the retry backoff.
|
||||
tokio::time::pause();
|
||||
release.notify_one();
|
||||
tokio::time::timeout(Duration::from_secs(10), failed.notified())
|
||||
.await
|
||||
.expect("first attempt should fail before revocation");
|
||||
let mut revoked = policy(5, false);
|
||||
revoked.reason_code = Some(ReasonCode::ConsentInactive);
|
||||
policy_tx.send(revoked).expect("revoke consent");
|
||||
tokio::time::resume();
|
||||
|
||||
tokio::time::timeout(Duration::from_secs(10), receipts.changed())
|
||||
.await
|
||||
.expect("cancel receipt timeout")
|
||||
.expect("cancel receipt should be published");
|
||||
let cancelled = receipts.borrow().clone().expect("cancel receipt");
|
||||
assert_eq!(cancelled.outcome, ReceiptOutcome::Cancelled);
|
||||
assert_eq!(cancelled.policy_revision, 4);
|
||||
assert_eq!(cancelled.attempt_count, 1);
|
||||
assert_eq!(attempts.load(Ordering::SeqCst), 1, "revocation must prevent another attempt");
|
||||
shutdown.cancel();
|
||||
runtime.shutdown().await;
|
||||
let persisted = store.read().await.expect("durable cancelled state");
|
||||
assert_eq!(persisted.last_receipt, Some(cancelled));
|
||||
assert!(persisted.active_interval_started_at.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn bootstrap_stopped_policy_preserves_a_due_interval_until_fresh_policy_arrives() {
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
|
||||
@@ -112,31 +112,3 @@ async fn missing_inventory_has_bounded_retries_and_durable_failure_receipt() {
|
||||
shutdown.cancel();
|
||||
runtime.shutdown().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn consent_revoke_cancels_retry_and_persists_receipt() {
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let (policy_tx, policy_rx) = watch::channel(policy(4, "RUNNING", None));
|
||||
let shutdown = CancellationToken::new();
|
||||
let runtime = spawn_environment_schedule(temp.path(), policy_rx, shutdown.clone()).expect("scheduler");
|
||||
let mut status = runtime.status();
|
||||
tokio::time::timeout(Duration::from_secs(2), async {
|
||||
while !matches!(&*status.borrow(), DiagnosticScheduleStatus::Running { .. }) {
|
||||
status.changed().await.expect("schedule remains active");
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("first attempt");
|
||||
|
||||
policy_tx
|
||||
.send(policy(5, "STOPPED", Some("CONSENT_INACTIVE")))
|
||||
.expect("revoke policy");
|
||||
let cancelled = receipt(&mut status).await;
|
||||
assert_eq!(cancelled.outcome, ReceiptOutcome::Cancelled);
|
||||
let state: serde_json::Value =
|
||||
serde_json::from_slice(&std::fs::read(temp.path().join("diagnostics/schedule.json")).expect("durable state"))
|
||||
.expect("state JSON");
|
||||
assert_eq!(state["lastReceipt"]["outcome"], "CANCELLED");
|
||||
shutdown.cancel();
|
||||
runtime.shutdown().await;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user