mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-16 09:58:21 +00:00
f17ea7f146
* fix(heal): gate auto replacement formatting Co-Authored-By: heihutu <heihutu@gmail.com> * fix(heal): require replacement target outcomes Co-Authored-By: heihutu <heihutu@gmail.com> * fix(heal): bind resumes to replacement targets Co-Authored-By: heihutu <heihutu@gmail.com> * fix(heal): fence healing marker ownership Co-Authored-By: heihutu <heihutu@gmail.com> * test(heal): cover replacement target completion Co-Authored-By: heihutu <heihutu@gmail.com> * docs(heal): clarify replacement recovery status Co-Authored-By: heihutu <heihutu@gmail.com> * fix(heal): canonicalize replacement target checks Co-Authored-By: heihutu <heihutu@gmail.com> * fix(heal): satisfy marker test module lint Co-Authored-By: heihutu <heihutu@gmail.com> * fix(heal): scope automatic replacement format Co-Authored-By: heihutu <heihutu@gmail.com> * fix(heal): require a mounted replacement target Co-Authored-By: heihutu <heihutu@gmail.com> * fix(ecstore): avoid cloned ref slice in test Co-Authored-By: heihutu <heihutu@gmail.com> * fix(heal): revalidate replacement before scanning Co-Authored-By: heihutu <heihutu@gmail.com> * fix(heal): reset stale resume checkpoints Co-Authored-By: heihutu <heihutu@gmail.com> * fix(heal): release scanner disk map before probing Co-Authored-By: heihutu <heihutu@gmail.com> * fix(heal): persist replacement intent before format Co-Authored-By: heihutu <heihutu@gmail.com> * fix(heal): fail closed on mountinfo read errors Co-Authored-By: heihutu <heihutu@gmail.com> * fix(heal): fence replacement target identity Co-Authored-By: heihutu <heihutu@gmail.com> * fix(heal): order replacement completion cleanup Co-Authored-By: heihutu <heihutu@gmail.com> * fix(heal): atomically seal replacement completion Co-Authored-By: heihutu <heihutu@gmail.com> * test(heal): census replacement target shards Co-Authored-By: heihutu <heihutu@gmail.com> * fix(heal): fence replacement recovery ownership Co-Authored-By: heihutu <heihutu@gmail.com> * fix(heal): preserve replacement recovery anchors Co-Authored-By: heihutu <heihutu@gmail.com> * fix(heal): satisfy replacement recovery lint gates Co-Authored-By: heihutu <heihutu@gmail.com> * fix(ecstore): bind replacement identity to mount lease Co-Authored-By: heihutu <heihutu@gmail.com> * test(heal): cover durable replacement recovery states Co-Authored-By: heihutu <heihutu@gmail.com> * fix(heal): validate persisted resume task identifiers Co-Authored-By: heihutu <heihutu@gmail.com> * fix(ecstore): avoid blocking replacement marker CAS Co-Authored-By: heihutu <heihutu@gmail.com> * fix(heal): report failed marker rollback Co-Authored-By: heihutu <heihutu@gmail.com> * test(heal): pin replacement resume schema compatibility Co-Authored-By: heihutu <heihutu@gmail.com> * fix(heal): preserve durable recovery anchors Co-Authored-By: heihutu <heihutu@gmail.com> * fix(ecstore): preserve public disk path semantics Co-Authored-By: heihutu <heihutu@gmail.com> * test(heal): use canonical replacement task ids Co-Authored-By: heihutu <heihutu@gmail.com> * test(heal): cover automatic replacement in 3x4 cluster Co-Authored-By: heihutu <heihutu@gmail.com> * fix(heal): verify replacement target commits Co-Authored-By: heihutu <heihutu@gmail.com> * fix(heal): persist replacement completion proof Co-Authored-By: heihutu <heihutu@gmail.com> * feat(heal): expose durable replacement status Co-Authored-By: heihutu <heihutu@gmail.com> * fix(heal): bound durable replacement discovery Co-Authored-By: heihutu <heihutu@gmail.com> * fix(heal): remove replacement readiness bypass Co-Authored-By: heihutu <heihutu@gmail.com> * fix(heal): retry terminal replacement cleanup Co-Authored-By: heihutu <heihutu@gmail.com> * fix(heal): isolate replacement intents from legacy resume Co-Authored-By: heihutu <heihutu@gmail.com> * fix(heal): migrate legacy replacement intents at startup Co-Authored-By: heihutu <heihutu@gmail.com> * style(heal): apply strict clippy fix Co-Authored-By: heihutu <heihutu@gmail.com> * fix(heal): prioritize active replacement recovery state Co-Authored-By: heihutu <heihutu@gmail.com> * fix(heal): bind readiness to the admitted mount lease Co-Authored-By: heihutu <heihutu@gmail.com> * fix(heal): atomically publish replacement intents Co-Authored-By: heihutu <heihutu@gmail.com> * fix(heal): isolate replacement recovery directory Co-Authored-By: heihutu <heihutu@gmail.com> * fix(heal): tolerate an empty recovery directory Co-Authored-By: heihutu <heihutu@gmail.com> * style(heal): remove redundant disk bytes conversion Co-Authored-By: heihutu <heihutu@gmail.com> * fix(heal): reconcile proof-first replacement recovery Co-Authored-By: heihutu <heihutu@gmail.com> * fix(heal): fence torn intent recovery Co-Authored-By: heihutu <heihutu@gmail.com> * test(heal): cover replacement migration conflicts Co-Authored-By: heihutu <heihutu@gmail.com> * fix(ecstore): fence replacement lease mount identity Co-Authored-By: heihutu <heihutu@gmail.com> * test(heal): cover missing replacement path admission Co-Authored-By: heihutu <heihutu@gmail.com> * fix(heal): reject conflicting legacy completion proof Co-Authored-By: heihutu <heihutu@gmail.com> * fix(ecstore): fall back to proc mount identity Co-Authored-By: heihutu <heihutu@gmail.com> * feat(admin): expose replacement recovery status Surface the local durable replacement recovery snapshot in the background heal status response so operators can tell whether replacement cleanup is definitive or still pending. Co-Authored-By: heihutu <heihutu@gmail.com> * fix(heal): keep replacement status compatible Keep the existing background heal status response wire-compatible while retaining the Linux mount lease cleanup needed for the replacement recovery branch. Co-Authored-By: heihutu <heihutu@gmail.com> * style(ecstore): match linux mount lease formatting Keep Linux rustfmt output stable for the replacement mount lease comparison. Co-Authored-By: heihutu <heihutu@gmail.com> * fix(ecstore): qualify mount lease test constant Use the disk module path for the format config constant in the Linux mount lease regression test. Co-Authored-By: heihutu <heihutu@gmail.com> * fix(ecstore): keep procfd mount roots directory-safe Use a procfd path with an explicit directory component so Unix directory guards can open the replacement mount lease root with O_NOFOLLOW while preserving handle-relative I/O semantics. Co-Authored-By: heihutu <heihutu@gmail.com> * fix(ecstore): delete empty leased buckets via dirfd Use the held mount lease fd as the parent for non-force empty bucket deletion on Linux so procfd-rooted paths do not get rejected as BucketNotEmpty. Also make the download-part OpenOptions truncate behavior explicit and keep fsync test recording stable across procfd canonicalization. Co-Authored-By: heihutu <heihutu@gmail.com> * fix(ecstore): scan leased bucket paths for emptiness Use the local disk I/O root for bucket emptiness probes before non-force bucket deletion and table-bucket metadata checks. This keeps validation on the same mount instance as the subsequent local disk delete path. Co-Authored-By: heihutu <heihutu@gmail.com> * test(ecstore): align lease path test probes Co-Authored-By: heihutu <heihutu@gmail.com> * fix(heal): block unsafe replacement recovery restarts Co-Authored-By: heihutu <heihutu@gmail.com> * fix(heal): defer blocked replacement candidates Co-Authored-By: heihutu <heihutu@gmail.com> * fix(heal): retry transient replacement discovery Co-Authored-By: heihutu <heihutu@gmail.com> * fix(heal): keep transient recovery errors retryable Co-Authored-By: heihutu <heihutu@gmail.com> * fix(heal): block corrupt legacy replacement state Co-Authored-By: heihutu <heihutu@gmail.com> * fix(heal): classify flat replacement intent corruption Co-Authored-By: heihutu <heihutu@gmail.com> * fix(heal): keep transient resume loads retryable Classify malformed legacy replacement state as blocking corruption while preserving disk and transient load failures for retry. This avoids permanently blocking replacement recovery on temporary storage errors. Co-Authored-By: heihutu <heihutu@gmail.com> * fix(heal): avoid latching transient legacy publishes Co-Authored-By: heihutu <heihutu@gmail.com> * fix(heal): retry blocked legacy migrations Co-Authored-By: heihutu <heihutu@gmail.com> * fix(heal): defer blocked startup recoveries Co-Authored-By: heihutu <heihutu@gmail.com> * fix(ecstore): preserve disk sync limiter across lease roots Co-Authored-By: heihutu <heihutu@gmail.com> --------- Co-authored-by: heihutu <heihutu@gmail.com> Co-authored-by: zhi22915 <qiuzgang@gmail.com>
405 lines
17 KiB
Rust
405 lines
17 KiB
Rust
// Copyright 2024 RustFS Team
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
//! Reliability tests driven by in-process fault injection (see `chaos.rs`):
|
|
//! degraded reads/writes with an offline disk, bitrot read-through, and
|
|
//! fresh-disk replacement heal after a SIGKILL restart.
|
|
//!
|
|
//! All tests use a single-node 4-disk topology (default erasure coding for
|
|
//! 4 drives is 2 data + 2 parity) and verify object content via sha256
|
|
//! manifests recorded at write time.
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::chaos::{DiskFaultHarness, VersionShardCensus, signed_admin_post};
|
|
use crate::common::init_logging;
|
|
use aws_sdk_s3::Client;
|
|
use aws_sdk_s3::primitives::ByteStream;
|
|
use aws_sdk_s3::types::{BucketVersioningStatus, CompletedMultipartUpload, CompletedPart, VersioningConfiguration};
|
|
use serial_test::serial;
|
|
use sha2::{Digest, Sha256};
|
|
use std::collections::HashSet;
|
|
use std::error::Error;
|
|
use tokio::time::{Duration, Instant, interval, timeout};
|
|
use tracing::info;
|
|
|
|
const GET_TIMEOUT: Duration = Duration::from_secs(60);
|
|
const PUT_TIMEOUT: Duration = Duration::from_secs(60);
|
|
|
|
fn sha256_hex(data: &[u8]) -> String {
|
|
let digest = Sha256::digest(data);
|
|
digest.iter().map(|byte| format!("{byte:02x}")).collect()
|
|
}
|
|
|
|
/// Deterministic pseudo-random payload so tests stay reproducible.
|
|
fn payload(len: usize, seed: u8) -> Vec<u8> {
|
|
(0..len)
|
|
.map(|i| (i as u64).wrapping_mul(2654435761).wrapping_add(seed as u64) as u8)
|
|
.collect()
|
|
}
|
|
|
|
async fn put_and_record(
|
|
client: &Client,
|
|
bucket: &str,
|
|
key: &str,
|
|
body: Vec<u8>,
|
|
manifest: &mut Vec<(String, String)>,
|
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
let digest = sha256_hex(&body);
|
|
timeout(
|
|
PUT_TIMEOUT,
|
|
client
|
|
.put_object()
|
|
.bucket(bucket)
|
|
.key(key)
|
|
.body(ByteStream::from(body))
|
|
.send(),
|
|
)
|
|
.await
|
|
.map_err(|_| format!("PUT {key} timed out"))??;
|
|
manifest.push((key.to_string(), digest));
|
|
Ok(())
|
|
}
|
|
|
|
async fn multipart_put_and_record(
|
|
client: &Client,
|
|
bucket: &str,
|
|
key: &str,
|
|
parts: Vec<Vec<u8>>,
|
|
manifest: &mut Vec<(String, String)>,
|
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
let full_body: Vec<u8> = parts.iter().flatten().copied().collect();
|
|
let digest = sha256_hex(&full_body);
|
|
|
|
let create = client.create_multipart_upload().bucket(bucket).key(key).send().await?;
|
|
let upload_id = create.upload_id().ok_or("missing upload id")?.to_string();
|
|
|
|
let mut completed_parts = Vec::with_capacity(parts.len());
|
|
for (index, part_body) in parts.into_iter().enumerate() {
|
|
let part_number = (index + 1) as i32;
|
|
let uploaded = timeout(
|
|
PUT_TIMEOUT,
|
|
client
|
|
.upload_part()
|
|
.bucket(bucket)
|
|
.key(key)
|
|
.upload_id(&upload_id)
|
|
.part_number(part_number)
|
|
.body(ByteStream::from(part_body))
|
|
.send(),
|
|
)
|
|
.await
|
|
.map_err(|_| format!("upload_part {part_number} for {key} timed out"))??;
|
|
completed_parts.push(
|
|
CompletedPart::builder()
|
|
.part_number(part_number)
|
|
.e_tag(uploaded.e_tag().ok_or("missing part etag")?)
|
|
.build(),
|
|
);
|
|
}
|
|
|
|
client
|
|
.complete_multipart_upload()
|
|
.bucket(bucket)
|
|
.key(key)
|
|
.upload_id(&upload_id)
|
|
.multipart_upload(CompletedMultipartUpload::builder().set_parts(Some(completed_parts)).build())
|
|
.send()
|
|
.await?;
|
|
|
|
manifest.push((key.to_string(), digest));
|
|
Ok(())
|
|
}
|
|
|
|
async fn verify_manifest(
|
|
client: &Client,
|
|
bucket: &str,
|
|
manifest: &[(String, String)],
|
|
phase: &str,
|
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
for (key, expected_sha256) in manifest {
|
|
let response = timeout(GET_TIMEOUT, client.get_object().bucket(bucket).key(key).send())
|
|
.await
|
|
.map_err(|_| format!("GET {key} timed out during {phase}"))?
|
|
.map_err(|err| format!("GET {key} failed during {phase}: {err}"))?;
|
|
let body = response
|
|
.body
|
|
.collect()
|
|
.await
|
|
.map_err(|err| format!("GET {key} body collect failed during {phase}: {err}"))?
|
|
.into_bytes();
|
|
let actual_sha256 = sha256_hex(&body);
|
|
if actual_sha256 != *expected_sha256 {
|
|
return Err(format!(
|
|
"sha256 mismatch for {key} during {phase}: expected {expected_sha256}, got {actual_sha256} ({} bytes)",
|
|
body.len()
|
|
)
|
|
.into());
|
|
}
|
|
}
|
|
info!("Verified {} objects during {}", manifest.len(), phase);
|
|
Ok(())
|
|
}
|
|
|
|
/// One disk goes offline at runtime: all previously written objects (from
|
|
/// inline-small to multipart-sized) must stay readable with intact
|
|
/// content, degraded writes must succeed, and everything must still
|
|
/// verify after the disk returns.
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn test_degraded_read_write_with_one_disk_offline() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
init_logging();
|
|
info!("Reliability: degraded read/write with one of four disks offline");
|
|
|
|
let mut harness = DiskFaultHarness::new(4).await?;
|
|
harness.start_server().await?;
|
|
let client = harness.env.create_s3_client();
|
|
|
|
let bucket = "reliability-degraded-rw";
|
|
client.create_bucket().bucket(bucket).send().await?;
|
|
|
|
let mut manifest: Vec<(String, String)> = Vec::new();
|
|
put_and_record(&client, bucket, "degraded/small.bin", payload(4 * 1024, 1), &mut manifest).await?;
|
|
put_and_record(&client, bucket, "degraded/medium.bin", payload(1024 * 1024, 2), &mut manifest).await?;
|
|
put_and_record(&client, bucket, "degraded/big.bin", payload(3 * 1024 * 1024, 3), &mut manifest).await?;
|
|
multipart_put_and_record(
|
|
&client,
|
|
bucket,
|
|
"degraded/multipart.bin",
|
|
vec![payload(5 * 1024 * 1024, 4), payload(1024 * 1024, 5)],
|
|
&mut manifest,
|
|
)
|
|
.await?;
|
|
|
|
verify_manifest(&client, bucket, &manifest, "baseline with all disks online").await?;
|
|
|
|
harness.take_disk_offline(0)?;
|
|
|
|
verify_manifest(&client, bucket, &manifest, "degraded read with disk0 offline").await?;
|
|
|
|
// Degraded writes: 3 of 4 disks still satisfy the write quorum for
|
|
// an EC 2+2 set.
|
|
put_and_record(
|
|
&client,
|
|
bucket,
|
|
"degraded/written-while-offline.bin",
|
|
payload(1024 * 1024, 9),
|
|
&mut manifest,
|
|
)
|
|
.await?;
|
|
verify_manifest(&client, bucket, &manifest, "read-back of degraded write").await?;
|
|
|
|
harness.bring_disk_online(0)?;
|
|
|
|
verify_manifest(&client, bucket, &manifest, "after disk0 came back online").await?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Silent bitrot in a single erasure shard must never surface corrupted
|
|
/// bytes to a reader: per-shard bitrot checksums reject the bad shard and
|
|
/// the object is reconstructed from the remaining shards.
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn test_bitrot_corrupted_shard_read_returns_correct_data() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
init_logging();
|
|
info!("Reliability: GET must read through a bitrot-corrupted shard");
|
|
|
|
let mut harness = DiskFaultHarness::new(4).await?;
|
|
harness.start_server().await?;
|
|
let client = harness.env.create_s3_client();
|
|
|
|
let bucket = "reliability-bitrot";
|
|
client.create_bucket().bucket(bucket).send().await?;
|
|
|
|
let key_a = "bitrot/object-a.bin";
|
|
let key_b = "bitrot/object-b.bin";
|
|
let mut manifest: Vec<(String, String)> = Vec::new();
|
|
// 2 MiB objects are far above the 128 KiB inline threshold, so every
|
|
// disk holds a real part.1 shard file to corrupt.
|
|
put_and_record(&client, bucket, key_a, payload(2 * 1024 * 1024, 21), &mut manifest).await?;
|
|
put_and_record(&client, bucket, key_b, payload(2 * 1024 * 1024, 22), &mut manifest).await?;
|
|
|
|
verify_manifest(&client, bucket, &manifest, "baseline before corruption").await?;
|
|
|
|
// Corrupt two shards per object (the maximum an EC 2+2 set can lose)
|
|
// on different disk pairs. Which disks hold data vs parity depends on
|
|
// the per-object distribution, so corrupting a pair makes it very
|
|
// likely that at least one data shard is hit; either way the read
|
|
// must return intact content reconstructed from the clean shards.
|
|
harness.corrupt_object_shard(0, bucket, key_a)?;
|
|
harness.corrupt_object_shard(1, bucket, key_a)?;
|
|
harness.corrupt_object_shard(2, bucket, key_b)?;
|
|
harness.corrupt_object_shard(3, bucket, key_b)?;
|
|
|
|
verify_manifest(&client, bucket, &manifest, "first read after shard corruption").await?;
|
|
// A second pass ensures repeated reads stay correct as well.
|
|
verify_manifest(&client, bucket, &manifest, "second read after shard corruption").await?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Fresh-disk replacement: SIGKILL the server, swap one disk for an empty
|
|
/// directory, restart with the same volumes/port, trigger an admin deep
|
|
/// heal, and require the replaced disk to be rebuilt and all content to
|
|
/// verify against the sha256 manifest.
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn test_fresh_disk_replacement_heals_after_sigkill_restart() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
init_logging();
|
|
info!("Reliability: fresh-disk replacement heals after SIGKILL restart");
|
|
|
|
let mut harness = DiskFaultHarness::new(4).await?;
|
|
harness.start_server().await?;
|
|
let client = harness.env.create_s3_client();
|
|
|
|
let bucket = "reliability-fresh-disk";
|
|
client.create_bucket().bucket(bucket).send().await?;
|
|
|
|
let mut manifest: Vec<(String, String)> = Vec::new();
|
|
put_and_record(&client, bucket, "heal/tiny.bin", payload(4 * 1024, 31), &mut manifest).await?;
|
|
put_and_record(&client, bucket, "heal/small.bin", payload(256 * 1024, 32), &mut manifest).await?;
|
|
put_and_record(&client, bucket, "heal/medium.bin", payload(1024 * 1024, 33), &mut manifest).await?;
|
|
put_and_record(&client, bucket, "heal/nested/large.bin", payload(2 * 1024 * 1024, 34), &mut manifest).await?;
|
|
|
|
verify_manifest(&client, bucket, &manifest, "baseline before disk replacement").await?;
|
|
let manifest_keys = manifest.iter().map(|(key, _)| key.clone()).collect::<Vec<_>>();
|
|
let target_manifest: Vec<(String, VersionShardCensus)> = manifest_keys
|
|
.iter()
|
|
.map(|key| {
|
|
let census = harness.census_object_version(0, bucket, key, None)?;
|
|
if !census.is_complete() {
|
|
return Err(format!("disk 0 has incomplete physical census for {key}: {census:?}").into());
|
|
}
|
|
Ok((key.clone(), census))
|
|
})
|
|
.collect::<Result<_, Box<dyn Error + Send + Sync>>>()?;
|
|
|
|
harness.kill_server();
|
|
harness.replace_disk_with_empty(0)?;
|
|
harness.restart_server().await?;
|
|
|
|
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/{}?forceStart=true", harness.env.url, bucket);
|
|
signed_admin_post(&heal_url, Some(heal_body), &harness.env.access_key, &harness.env.secret_key).await?;
|
|
|
|
let client = harness.env.create_s3_client();
|
|
let mut remaining: HashSet<String> = manifest_keys.iter().cloned().collect();
|
|
let heal_timeout_secs = std::env::var("RUSTFS_RELIABILITY_HEAL_TIMEOUT_SECS")
|
|
.ok()
|
|
.and_then(|value| value.parse::<u64>().ok())
|
|
.unwrap_or(120);
|
|
let deadline = Instant::now() + Duration::from_secs(heal_timeout_secs);
|
|
let mut retry = interval(Duration::from_secs(1));
|
|
|
|
loop {
|
|
remaining.retain(|key| {
|
|
let expected = target_manifest
|
|
.iter()
|
|
.find(|(manifest_key, _)| manifest_key == key)
|
|
.map(|(_, manifest)| manifest)
|
|
.expect("every key has a physical manifest");
|
|
harness
|
|
.census_object_version(0, bucket, key, None)
|
|
.map(|census| !census.matches_manifest(expected))
|
|
.unwrap_or(true)
|
|
});
|
|
if remaining.is_empty() {
|
|
verify_manifest(&client, bucket, &manifest, "after fresh-disk heal completed").await?;
|
|
return Ok(());
|
|
}
|
|
if Instant::now() >= deadline {
|
|
break;
|
|
}
|
|
retry.tick().await;
|
|
}
|
|
|
|
Err(format!("fresh-disk heal did not rebuild {remaining:?} on the replaced disk within {heal_timeout_secs}s").into())
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn test_versioned_shard_census_selects_each_version_data_dir() -> Result<(), Box<dyn Error + Send + Sync>> {
|
|
init_logging();
|
|
info!("Reliability: physical shard census selects the requested object version");
|
|
|
|
let mut harness = DiskFaultHarness::new(4).await?;
|
|
harness.start_server().await?;
|
|
let client = harness.env.create_s3_client();
|
|
let bucket = "reliability-versioned-census";
|
|
let key = "versions/large.bin";
|
|
client.create_bucket().bucket(bucket).send().await?;
|
|
client
|
|
.put_bucket_versioning()
|
|
.bucket(bucket)
|
|
.versioning_configuration(
|
|
VersioningConfiguration::builder()
|
|
.status(BucketVersioningStatus::Enabled)
|
|
.build(),
|
|
)
|
|
.send()
|
|
.await?;
|
|
|
|
let first = client
|
|
.put_object()
|
|
.bucket(bucket)
|
|
.key(key)
|
|
.body(ByteStream::from(payload(256 * 1024, 41)))
|
|
.send()
|
|
.await?;
|
|
let first_version = first.version_id().ok_or("first PUT did not return a version ID")?;
|
|
let second = client
|
|
.put_object()
|
|
.bucket(bucket)
|
|
.key(key)
|
|
.body(ByteStream::from(payload(256 * 1024, 42)))
|
|
.send()
|
|
.await?;
|
|
let second_version = second.version_id().ok_or("second PUT did not return a version ID")?;
|
|
let delete = client.delete_object().bucket(bucket).key(key).send().await?;
|
|
let delete_version = delete.version_id().ok_or("delete marker did not return a version ID")?;
|
|
|
|
let first_census = harness.census_object_version(0, bucket, key, Some(first_version))?;
|
|
let second_census = harness.census_object_version(0, bucket, key, Some(second_version))?;
|
|
let delete_census = harness.census_object_version(0, bucket, key, Some(delete_version))?;
|
|
assert!(
|
|
first_census.is_complete(),
|
|
"first version physical census is incomplete: {first_census:?}"
|
|
);
|
|
assert!(
|
|
second_census.is_complete(),
|
|
"second version physical census is incomplete: {second_census:?}"
|
|
);
|
|
assert_ne!(
|
|
first_census.data_dir, second_census.data_dir,
|
|
"distinct object versions must select distinct physical data directories"
|
|
);
|
|
assert_eq!(
|
|
first_census.expected_part_numbers, second_census.expected_part_numbers,
|
|
"same single-part shape should expose the same part numbers"
|
|
);
|
|
assert!(
|
|
delete_census.is_complete(),
|
|
"delete marker physical census is incomplete: {delete_census:?}"
|
|
);
|
|
assert!(
|
|
delete_census.expected_part_numbers.is_empty(),
|
|
"delete marker must not declare object shards: {delete_census:?}"
|
|
);
|
|
assert!(
|
|
delete_census.present_part_numbers.is_empty(),
|
|
"delete marker must not select stale object shards: {delete_census:?}"
|
|
);
|
|
Ok(())
|
|
}
|
|
}
|