mirror of
https://github.com/rustfs/rustfs.git
synced 2026-10-30 15:55:55 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b2cc71e5b6 | |||
| a106585826 | |||
| 9a1479a706 | |||
| 351bd12031 | |||
| a2381c8421 | |||
| 5eee702e88 |
@@ -93,6 +93,7 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
ref: 3c2b54aeb5a4641e413c2ee67dd909ecb9ac172f
|
||||
persist-credentials: false
|
||||
|
||||
- name: Determine build strategy
|
||||
@@ -189,7 +190,7 @@ jobs:
|
||||
# pins the glibc 2.31 floor on the same architecture; musl stays a
|
||||
# native cargo build.
|
||||
all='{"include":[
|
||||
{"target_id":"linux-x86_64-musl","os":"sm-standard-4","target":"x86_64-unknown-linux-musl","cross":false,"platform":"linux","rustflags":"","features":"","glibc":""},
|
||||
{"target_id":"linux-x86_64-musl","os":"ubuntu-latest","target":"x86_64-unknown-linux-musl","cross":false,"platform":"linux","rustflags":"","features":"","glibc":""},
|
||||
{"target_id":"linux-aarch64-musl","os":"ubicloud-standard-16-arm","target":"aarch64-unknown-linux-musl","cross":false,"platform":"linux","rustflags":"","features":"","glibc":""},
|
||||
{"target_id":"linux-x86_64-gnu","os":"sm-standard-4","target":"x86_64-unknown-linux-gnu","cross":true,"platform":"linux","rustflags":"","features":"pyroscope","glibc":"2.31"},
|
||||
{"target_id":"linux-aarch64-gnu","os":"ubicloud-standard-16-arm","target":"aarch64-unknown-linux-gnu","cross":true,"platform":"linux","rustflags":"","features":"pyroscope","glibc":"2.31"},
|
||||
@@ -235,6 +236,7 @@ jobs:
|
||||
# `make build-profiling`); crates/obs/build.rs fails the compile if the
|
||||
# `dial9` feature is enabled without the flag.
|
||||
RUSTFLAGS: "${{ matrix.rustflags }}"
|
||||
CARGO_BUILD_JOBS: 1
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJson(needs.prepare-platform-matrix.outputs.matrix) }}
|
||||
@@ -242,6 +244,7 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
ref: 3c2b54aeb5a4641e413c2ee67dd909ecb9ac172f
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
@@ -1700,6 +1700,15 @@ mod tests {
|
||||
);
|
||||
|
||||
let mut cluster = RustFSTestClusterEnvironment::with_topology(topology.cluster_topology()).await?;
|
||||
let bucket = "heal-restart-during-rebuild";
|
||||
let mrf_fallback_scope = if !background_enabled {
|
||||
let path = PathBuf::from(&cluster.temp_dir).join("skip-outage-mrf-fallback");
|
||||
std::fs::write(&path, format!("{bucket}/cluster/written-while-node-down-"))?;
|
||||
cluster.set_env("RUSTFS_E2E_SKIP_MRF_FALLBACK_PATH", path.to_string_lossy().into_owned());
|
||||
Some(path)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
cluster.set_env("RUSTFS_UNSAFE_BYPASS_DISK_CHECK", "true");
|
||||
cluster.set_env("RUSTFS_HEAL_ENABLED", "true");
|
||||
if let Some(storage_class) = evidence_case.and_then(|case| case.storage_class_standard) {
|
||||
@@ -1748,7 +1757,6 @@ mod tests {
|
||||
cluster.start_with_binary(&server_binary).await?;
|
||||
let clients = cluster.create_all_clients()?;
|
||||
|
||||
let bucket = "heal-restart-during-rebuild";
|
||||
clients[0].create_bucket().bucket(bucket).send().await?;
|
||||
|
||||
let ReplacementDriveSelection {
|
||||
@@ -1944,6 +1952,21 @@ mod tests {
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(path) = &mrf_fallback_scope {
|
||||
let observed = path.with_extension("observed");
|
||||
let expected = format!("{bucket}/{outage_key}");
|
||||
let deadline = Instant::now() + Duration::from_secs(30);
|
||||
loop {
|
||||
if tokio::fs::read_to_string(&observed).await.ok().as_deref() == Some(expected.as_str()) {
|
||||
break;
|
||||
}
|
||||
if Instant::now() >= deadline {
|
||||
return Err("outage repair did not reach the isolated best-effort fallback before target rejoin".into());
|
||||
}
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
}
|
||||
}
|
||||
|
||||
assert!(
|
||||
!outage_target_manifest_required
|
||||
|| target_set_peer_drives
|
||||
@@ -2056,7 +2079,12 @@ mod tests {
|
||||
} else {
|
||||
cluster_heal_is_idle(&recovered)
|
||||
};
|
||||
if ready {
|
||||
let pool_ready = if ready && background_enabled && !outage_write_deferred_until_rejoin {
|
||||
all_nodes_pool_metadata_readiness(&cluster, recovery_deadline).await.0
|
||||
} else {
|
||||
true
|
||||
};
|
||||
if ready && pool_ready {
|
||||
break;
|
||||
}
|
||||
if Instant::now() >= recovery_deadline {
|
||||
@@ -2553,6 +2581,10 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(path) = &mrf_fallback_scope {
|
||||
std::fs::remove_file(path)?;
|
||||
}
|
||||
|
||||
if let Some(cycle_end) = scanner_cycle_floor {
|
||||
wait_for_scanner_cycle_after(&cluster, cycle_end).await?;
|
||||
}
|
||||
|
||||
@@ -5120,6 +5120,12 @@ impl SetDisks {
|
||||
if request.expected_bucket_incarnation_id.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
#[cfg(feature = "e2e-test-hooks")]
|
||||
if let Some(object) = request.object_prefix.as_deref()
|
||||
&& skip_e2e_mrf_fallback(&request.bucket, object).await
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
request.source = rustfs_heal_contracts::heal_channel::HealRequestSource::Mrf;
|
||||
#[cfg(test)]
|
||||
{
|
||||
@@ -5822,6 +5828,25 @@ impl SetDisks {
|
||||
// }
|
||||
}
|
||||
|
||||
// The isolated root-heal test suppresses only the direct best-effort fallback.
|
||||
// Persisted MRF responsibilities and their replay keep their normal behavior.
|
||||
#[cfg(feature = "e2e-test-hooks")]
|
||||
pub(in crate::set_disk) async fn skip_e2e_mrf_fallback(bucket: &str, object: &str) -> bool {
|
||||
let Some(path) = std::env::var_os("RUSTFS_E2E_SKIP_MRF_FALLBACK_PATH") else {
|
||||
return false;
|
||||
};
|
||||
let path = std::path::PathBuf::from(path);
|
||||
let Ok(prefix) = tokio::fs::read_to_string(&path).await else {
|
||||
return false;
|
||||
};
|
||||
let key = format!("{bucket}/{object}");
|
||||
if prefix.is_empty() || !key.starts_with(&prefix) {
|
||||
return false;
|
||||
}
|
||||
let _ = tokio::fs::write(path.with_extension("observed"), key).await;
|
||||
true
|
||||
}
|
||||
|
||||
fn is_explicit_null_version(version_id: Option<Uuid>) -> bool {
|
||||
version_id == Some(Uuid::nil())
|
||||
}
|
||||
|
||||
@@ -9150,6 +9150,10 @@ impl SetDisks {
|
||||
// cannot safely target the object name after bucket recreation.
|
||||
return Ok(());
|
||||
};
|
||||
#[cfg(feature = "e2e-test-hooks")]
|
||||
if crate::set_disk::skip_e2e_mrf_fallback(bucket, object).await {
|
||||
return Ok(());
|
||||
}
|
||||
let mut request = rustfs_heal_contracts::heal_channel::create_heal_request_with_options(
|
||||
bucket.to_string(),
|
||||
Some(object.to_string()),
|
||||
|
||||
@@ -886,7 +886,7 @@ impl DistributedLock {
|
||||
}
|
||||
|
||||
async fn acquire_lock_quorum_with_retry(&self, request: &LockRequest) -> Result<LockAcquireQuorumResult> {
|
||||
let start = std::time::Instant::now();
|
||||
let start = tokio::time::Instant::now();
|
||||
let mut attempt = 1;
|
||||
let mut last_result = None;
|
||||
|
||||
@@ -907,11 +907,13 @@ impl DistributedLock {
|
||||
}
|
||||
|
||||
last_result = Some(result);
|
||||
let backoff = Self::lock_acquire_retry_backoff(attempt, &mut rand::rng());
|
||||
if start.elapsed().saturating_add(backoff) >= request.acquire_timeout {
|
||||
let remaining = request.acquire_timeout.saturating_sub(start.elapsed());
|
||||
if remaining.is_zero() {
|
||||
break;
|
||||
}
|
||||
|
||||
// Keep time for another acquire attempt instead of abandoning the
|
||||
// caller's remaining budget when the jittered backoff grows large.
|
||||
let backoff = Self::lock_acquire_retry_backoff(attempt, &mut rand::rng()).min(remaining / 2);
|
||||
tokio::time::sleep(backoff).await;
|
||||
attempt += 1;
|
||||
}
|
||||
|
||||
@@ -979,6 +979,30 @@ async fn test_namespace_lock_distributed_retries_transient_acquire_timeout() {
|
||||
drop(guard);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_namespace_lock_retries_when_backoff_exceeds_remaining_budget() {
|
||||
let managers = (0..3).map(|_| Arc::new(GlobalLockManager::new())).collect::<Vec<_>>();
|
||||
let flaky_clients = managers
|
||||
.iter()
|
||||
.map(|manager| Arc::new(FlakyAcquireClient::new(manager.clone(), 1)))
|
||||
.collect::<Vec<_>>();
|
||||
let clients = flaky_clients
|
||||
.iter()
|
||||
.map(|client| client.clone() as Arc<dyn LockClient>)
|
||||
.collect::<Vec<_>>();
|
||||
let lock = NamespaceLock::with_clients("short-retry-budget".to_string(), clients);
|
||||
let resource = create_test_object_key("bucket", "short-retry-budget");
|
||||
|
||||
// The first jittered backoff is at least 187 ms, but a second attempt can
|
||||
// still succeed within this budget once the transient failure has cleared.
|
||||
let guard = lock
|
||||
.get_write_lock(resource, "owner", Duration::from_millis(180))
|
||||
.await
|
||||
.expect("a backoff longer than the remaining budget must not discard the remaining attempt");
|
||||
assert!(flaky_clients.iter().any(|client| client.acquire_attempts() >= 2));
|
||||
drop(guard);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_namespace_lock_distributed_waits_full_timeout_for_late_release() {
|
||||
let managers = (0..3).map(|_| Arc::new(GlobalLockManager::new())).collect::<Vec<_>>();
|
||||
@@ -1136,10 +1160,16 @@ async fn test_namespace_lock_distributed_contention_quorum_miss_times_out() {
|
||||
let lock = NamespaceLock::with_clients("contention-timeout".to_string(), clients);
|
||||
let resource = create_test_object_key("bucket", "object-contention-timeout");
|
||||
|
||||
let err = lock
|
||||
.get_write_lock(resource, "owner-a", Duration::from_millis(40))
|
||||
let timeout = Duration::from_millis(40);
|
||||
let started = std::time::Instant::now();
|
||||
let err = tokio::time::timeout(Duration::from_secs(2), lock.get_write_lock(resource, "owner-a", timeout))
|
||||
.await
|
||||
.expect("contended acquisition must remain bounded")
|
||||
.expect_err("ordinary contention should exhaust acquire timeout");
|
||||
assert!(
|
||||
started.elapsed() >= timeout,
|
||||
"a contended acquisition must not return before its configured deadline"
|
||||
);
|
||||
|
||||
let err_str = err.to_string().to_lowercase();
|
||||
assert!(err_str.contains("timeout"), "expected timeout after contention retries, got: {err}");
|
||||
|
||||
Reference in New Issue
Block a user