From a974e50b1d9fd98452e39f91b6495e592413f158 Mon Sep 17 00:00:00 2001 From: houseme Date: Wed, 9 Sep 2026 05:17:13 +0800 Subject: [PATCH] test(scanner): wait for EC8+4 heal control readiness (#7540) --- crates/e2e_test/src/distributed/heal_test.rs | 48 ++++++++++++++++++- .../on_demand_migration/interaction_test.rs | 7 ++- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/crates/e2e_test/src/distributed/heal_test.rs b/crates/e2e_test/src/distributed/heal_test.rs index 6f216bcda..d09c0a32e 100644 --- a/crates/e2e_test/src/distributed/heal_test.rs +++ b/crates/e2e_test/src/distributed/heal_test.rs @@ -26,6 +26,7 @@ use std::collections::{BTreeMap, HashSet}; use std::io::{Read, Write}; use std::path::{Path, PathBuf}; use std::time::Duration; +use tokio::time::{Instant, sleep}; const EC84_NODE_COUNT: usize = 3; const EC84_DRIVES_PER_NODE: usize = 4; @@ -33,6 +34,8 @@ const EC84_DATA_BLOCKS: usize = 8; const EC84_PARITY_BLOCKS: usize = 4; const EC84_TARGET_DRIVE_RESTART_CASE: &str = "ec84-target-drive-restart"; const EC84_TARGET_DRIVE_RESTART_ORACLE: &str = "ec84-target-drive-restart.json"; +const EC84_HEAL_CONTROL_READY_TIMEOUT: Duration = Duration::from_secs(45); +const EC84_HEAL_CONTROL_RETRY_DELAY: Duration = Duration::from_millis(250); #[derive(Clone)] struct ExpectedShard { @@ -211,6 +214,29 @@ fn assert_replaced_drive_empty(drive: &Path, bucket: &str, keys: &[String]) -> T Ok(()) } +fn is_cluster_heal_coordination_unavailable(error: &(dyn std::error::Error + Send + Sync)) -> bool { + let message = error.to_string(); + message.contains("500 Internal Server Error") && message.contains("cluster heal coordination unavailable") +} + +async fn start_ec84_root_heal_when_control_ready( + heal_url: &str, + heal_body: &str, + access_key: &str, + secret_key: &str, +) -> TestResult { + let deadline = Instant::now() + EC84_HEAL_CONTROL_READY_TIMEOUT; + loop { + match signed_admin_post(heal_url, Some(heal_body), access_key, secret_key).await { + Ok(_) => return Ok(()), + Err(error) if is_cluster_heal_coordination_unavailable(error.as_ref()) && Instant::now() < deadline => { + sleep(EC84_HEAL_CONTROL_RETRY_DELAY).await; + } + Err(error) => return Err(error), + } + } +} + async fn put_large_inventory(client: &Client, bucket: &str) -> TestResult> { let mut expected = Vec::new(); for index in 0..4 { @@ -304,7 +330,7 @@ async fn three_node_four_drive_ec8_4_root_heal_rebuilds_replaced_drive_after_res let heal_body = r#"{"recursive":true,"dryRun":false,"remove":false,"recreate":true,"scanMode":2,"updateParity":false,"nolock":false}"#; let heal_url = format!("{}/rustfs/admin/v3/heal/{bucket}?forceStart=true", dist.cluster.nodes[0].url); - signed_admin_post(&heal_url, Some(heal_body), &dist.cluster.access_key, &dist.cluster.secret_key).await?; + start_ec84_root_heal_when_control_ready(&heal_url, heal_body, &dist.cluster.access_key, &dist.cluster.secret_key).await?; wait_until( Duration::from_secs(120), @@ -365,3 +391,23 @@ async fn three_node_four_drive_ec8_4_root_heal_rebuilds_replaced_drive_after_res Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn cluster_heal_coordination_retry_is_exact() { + let retryable: Box = + "admin POST failed: 500 Internal Server Error cluster heal coordination unavailable".into(); + assert!(is_cluster_heal_coordination_unavailable(retryable.as_ref())); + + let other_internal: Box = + "admin POST failed: 500 Internal Server Error unrelated".into(); + assert!(!is_cluster_heal_coordination_unavailable(other_internal.as_ref())); + + let wrong_status: Box = + "admin POST failed: 503 Service Unavailable cluster heal coordination unavailable".into(); + assert!(!is_cluster_heal_coordination_unavailable(wrong_status.as_ref())); + } +} diff --git a/crates/e2e_test/src/on_demand_migration/interaction_test.rs b/crates/e2e_test/src/on_demand_migration/interaction_test.rs index 465888b44..f22c5e3b9 100644 --- a/crates/e2e_test/src/on_demand_migration/interaction_test.rs +++ b/crates/e2e_test/src/on_demand_migration/interaction_test.rs @@ -1137,7 +1137,12 @@ async fn test_odm_admin_config_is_redacted_and_status_counts_match_the_source() let miss = env.raw_get(bucket, miss_key).await?; assert_eq!(miss.status, 404, "{}", String::from_utf8_lossy(&miss.body)); } - assert!(env.wait_local_listed(bucket, hit_key, SETTLE).await?); + let (listed, _, _) = tokio::try_join!( + env.wait_local_listed(bucket, hit_key, SETTLE), + env.wait_for_status_counter(bucket, "/counters/pulled_objects_total/inline", 1, SETTLE), + env.wait_for_status_counter(bucket, "/counters/pulled_bytes_total", body.len() as u64, SETTLE), + )?; + assert!(listed); let status = env.status_json(bucket).await?; assert_eq!(status.pointer("/configured").and_then(Value::as_bool), Some(true), "{status}");