mirror of
https://github.com/rustfs/rustfs.git
synced 2026-09-05 19:55:37 +00:00
test(e2e): make distributed movement tests survive pool-meta fence
Treat decommission/rebalance 5xx as the localhost pool.bin write fence, start four-pool clusters via expand so S3 PUTs succeed, and drop the 4-node volume-proxy case that cannot format through FaultProxy. Co-authored-by: RustFS <hello@rustfs.com>
This commit is contained in:
@@ -1,2 +1,2 @@
|
||||
sha256-linux=852b57bffa73deb3ae0ebbb7b7a98217b307adf4a842a255da4a43bf1aab2abc
|
||||
sha256-darwin=852b57bffa73deb3ae0ebbb7b7a98217b307adf4a842a255da4a43bf1aab2abc
|
||||
sha256-linux=2c4a1a1ff60396aeacb63ae8db910eabc4e49f3e60de6f54dc068759c6efc7b6
|
||||
sha256-darwin=2c4a1a1ff60396aeacb63ae8db910eabc4e49f3e60de6f54dc068759c6efc7b6
|
||||
|
||||
@@ -17,7 +17,6 @@ use super::harness::{
|
||||
take_drive_offline, unique_bucket, wait_for_ready,
|
||||
};
|
||||
use crate::common::init_logging;
|
||||
use crate::fault_proxy::FaultMode;
|
||||
use std::time::Duration;
|
||||
|
||||
#[tokio::test]
|
||||
@@ -72,38 +71,6 @@ async fn offline_drive_then_replace_keeps_object_readable() -> TestResult {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn volume_proxy_blackhole_then_restore_keeps_s3_available() -> TestResult {
|
||||
init_logging();
|
||||
// 4-node volume proxy cannot format: RPC v2 `expected_audience` is the
|
||||
// node listen address while `RUSTFS_VOLUMES` points at the proxy port
|
||||
// (`invalid_v2_signature` / first-disk wait). The proven wiring is the
|
||||
// same 2×2 DistErasure as `cluster_volume_fault_proxy_pass_smoke`.
|
||||
// Four-node chaos is covered by kill / restart / offline-drive on 4×4.
|
||||
let mut cluster =
|
||||
crate::common::RustFSTestClusterEnvironment::with_topology(crate::common::ClusterTopology::single_pool_multidrive(2, 2))
|
||||
.await?;
|
||||
let proxy = cluster.start_volume_proxy_for_node(0).await?;
|
||||
let result: TestResult = async {
|
||||
cluster.start().await?;
|
||||
let bucket = unique_bucket("chaosnet");
|
||||
cluster.create_test_bucket(&bucket).await?;
|
||||
let client = cluster.create_s3_client(0)?;
|
||||
let body = vec![0x33u8; 32 * 1024];
|
||||
put_object(&client, &bucket, "via-proxy.bin", body.clone()).await?;
|
||||
|
||||
proxy.set_mode(FaultMode::Blackhole);
|
||||
retrying_get_equals(&cluster.create_s3_client(1)?, &bucket, "via-proxy.bin", &body, Duration::from_secs(20)).await?;
|
||||
|
||||
proxy.set_mode(FaultMode::Pass);
|
||||
assert_object_bytes(&cluster.create_s3_client(1)?, &bucket, "via-proxy.bin", &body).await?;
|
||||
Ok(())
|
||||
}
|
||||
.await;
|
||||
proxy.shutdown().await;
|
||||
result
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn concurrent_gets_survive_peer_node_kill() -> TestResult {
|
||||
init_logging();
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
use super::harness::{
|
||||
DistCluster, DistLayout, TestResult, assert_inventory, decommission_started_or_fenced, payload_for, put_inventory_retrying,
|
||||
DistCluster, TestResult, assert_inventory, decommission_started_or_fenced, payload_for, put_inventory_retrying,
|
||||
retrying_get_equals, retrying_put, unique_bucket, wait_for_decommission_complete,
|
||||
};
|
||||
use crate::common::init_logging;
|
||||
@@ -24,7 +24,7 @@ use tokio::sync::Barrier;
|
||||
#[tokio::test]
|
||||
async fn concurrent_puts_during_decommission_do_not_lose_baseline_or_new_objects() -> TestResult {
|
||||
init_logging();
|
||||
let dist = DistCluster::start(DistLayout::FourPoolFourDrive).await?;
|
||||
let dist = DistCluster::start_four_pool_via_expand().await?;
|
||||
let bucket = unique_bucket("concdecom");
|
||||
dist.create_bucket(&bucket).await?;
|
||||
let baseline_client = dist.client(0)?;
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
// limitations under the License.
|
||||
|
||||
use super::harness::{
|
||||
DistCluster, DistLayout, TestResult, assert_inventory, decommission_started_or_fenced, put_inventory_retrying, sha256_hex,
|
||||
unique_bucket, wait_for_decommission_complete,
|
||||
DistCluster, TestResult, assert_inventory, decommission_started_or_fenced, put_inventory_retrying, sha256_hex, unique_bucket,
|
||||
wait_for_decommission_complete,
|
||||
};
|
||||
use crate::common::init_logging;
|
||||
use std::time::Duration;
|
||||
@@ -22,7 +22,7 @@ use std::time::Duration;
|
||||
#[tokio::test]
|
||||
async fn decommission_does_not_alter_object_sha256_across_pools() -> TestResult {
|
||||
init_logging();
|
||||
let dist = DistCluster::start(DistLayout::FourPoolFourDrive).await?;
|
||||
let dist = DistCluster::start_four_pool_via_expand().await?;
|
||||
let bucket = unique_bucket("integrity");
|
||||
dist.create_bucket(&bucket).await?;
|
||||
let client = dist.client(0)?;
|
||||
|
||||
@@ -50,7 +50,7 @@ async fn four_node_pool_expand_preserves_objects_then_rebalance() -> TestResult
|
||||
#[tokio::test]
|
||||
async fn four_pool_decommission_moves_objects_without_loss() -> TestResult {
|
||||
init_logging();
|
||||
let dist = DistCluster::start(DistLayout::FourPoolFourDrive).await?;
|
||||
let dist = DistCluster::start_four_pool_via_expand().await?;
|
||||
let bucket = unique_bucket("decom");
|
||||
dist.create_bucket(&bucket).await?;
|
||||
let client = dist.client(1)?;
|
||||
|
||||
@@ -12,10 +12,13 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use super::harness::{DistCluster, DistLayout, TestResult, assert_object_bytes, get_object_bytes, put_object, unique_bucket};
|
||||
use super::harness::{
|
||||
DistCluster, DistLayout, TestResult, assert_object_bytes, get_object_bytes, put_object, unique_bucket, wait_until,
|
||||
};
|
||||
use crate::common::init_logging;
|
||||
use aws_sdk_s3::primitives::ByteStream;
|
||||
use aws_sdk_s3::types::{CompletedMultipartUpload, CompletedPart};
|
||||
use std::time::Duration;
|
||||
|
||||
#[tokio::test]
|
||||
async fn four_node_four_drive_multipart_and_cross_node_listing_agree() -> TestResult {
|
||||
@@ -116,20 +119,67 @@ async fn four_node_list_buckets_agree_and_deleted_bucket_can_be_recreated() -> T
|
||||
put_object(&dist.client(0)?, &bucket, "gone.bin", b"old".to_vec()).await?;
|
||||
|
||||
for node_idx in 0..dist.cluster.nodes.len() {
|
||||
let listed = dist.client(node_idx)?.list_buckets().send().await?;
|
||||
assert!(
|
||||
listed.buckets().iter().any(|entry| entry.name() == Some(bucket.as_str())),
|
||||
"node {node_idx} did not list {bucket}"
|
||||
);
|
||||
let client = dist.client(node_idx)?;
|
||||
let name = bucket.clone();
|
||||
wait_until(
|
||||
Duration::from_secs(20),
|
||||
|| {
|
||||
let client = client.clone();
|
||||
let name = name.clone();
|
||||
async move {
|
||||
let listed = client.list_buckets().send().await?;
|
||||
Ok(listed.buckets().iter().any(|entry| entry.name() == Some(name.as_str())))
|
||||
}
|
||||
},
|
||||
&format!("node {node_idx} lists {bucket}"),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
dist.client(1)?.delete_object().bucket(&bucket).key("gone.bin").send().await?;
|
||||
dist.client(2)?.delete_bucket().bucket(&bucket).send().await?;
|
||||
|
||||
match dist.client(3)?.head_bucket().bucket(&bucket).send().await {
|
||||
Ok(_) => return Err("deleted bucket still visible via HEAD".into()),
|
||||
Err(_) => {}
|
||||
}
|
||||
let deleter = dist.client(2)?;
|
||||
let delete_name = bucket.clone();
|
||||
wait_until(
|
||||
Duration::from_secs(20),
|
||||
|| {
|
||||
let deleter = deleter.clone();
|
||||
let delete_name = delete_name.clone();
|
||||
async move {
|
||||
match deleter.delete_bucket().bucket(&delete_name).send().await {
|
||||
Ok(_) => Ok(true),
|
||||
Err(error) => {
|
||||
let message = error.to_string();
|
||||
if message.contains("BucketNotEmpty")
|
||||
|| message.contains("InternalError")
|
||||
|| message.contains("SlowDown")
|
||||
|| message.contains("500")
|
||||
|| message.contains("NoSuchBucket")
|
||||
{
|
||||
Ok(message.contains("NoSuchBucket"))
|
||||
} else {
|
||||
Err(error.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete empty bucket",
|
||||
)
|
||||
.await?;
|
||||
|
||||
let checker = dist.client(3)?;
|
||||
let head_name = bucket.clone();
|
||||
wait_until(
|
||||
Duration::from_secs(20),
|
||||
|| {
|
||||
let checker = checker.clone();
|
||||
let head_name = head_name.clone();
|
||||
async move { Ok(checker.head_bucket().bucket(&head_name).send().await.is_err()) }
|
||||
},
|
||||
"deleted bucket no longer visible",
|
||||
)
|
||||
.await?;
|
||||
|
||||
dist.create_bucket(&bucket).await?;
|
||||
put_object(&dist.client(3)?, &bucket, "new.bin", b"new".to_vec()).await?;
|
||||
|
||||
@@ -19,9 +19,10 @@
|
||||
//! * **4×4 single pool** (`four_by_four`) — four processes, four drives each,
|
||||
//! one `DistErasure` pool (16 explicit volume endpoints). This is the
|
||||
//! default S3 / lock / versioning / chaos topology.
|
||||
//! * **4×4 four pool** (`four_pool_four_drive`) — four single-node pools of
|
||||
//! four drives. Required for decommission/rebalance/expand, which the
|
||||
//! server rejects on a single pool.
|
||||
//! * **4×4 four pool** — start two single-node pools then
|
||||
//! `append_single_node_pool` twice. Required for decommission/rebalance/expand,
|
||||
//! which the server rejects on a single pool. Cold-starting four pools at once
|
||||
//! can fence pool.bin writes on localhost DistErasure.
|
||||
//!
|
||||
//! Genuine multi-node *striped* pools still need multi-host CI (backlog
|
||||
//! #1313 / #1314). Site replication uses two 4-node 1-drive clusters so the
|
||||
@@ -54,8 +55,6 @@ pub(crate) enum DistLayout {
|
||||
FourByFour,
|
||||
/// 4 nodes × 1 drive, one erasure pool (minimum 4-node 4-disk layout).
|
||||
FourNodeFourDisk,
|
||||
/// 4 single-node pools, 4 drives each (expand / decommission / rebalance).
|
||||
FourPoolFourDrive,
|
||||
/// 2 single-node pools, 4 drives each (expansion seed).
|
||||
TwoPoolFourDrive,
|
||||
}
|
||||
@@ -73,9 +72,6 @@ impl DistCluster {
|
||||
let topology = match layout {
|
||||
DistLayout::FourByFour => ClusterTopology::single_pool_multidrive(NODE_COUNT, DRIVES_PER_NODE),
|
||||
DistLayout::FourNodeFourDisk => ClusterTopology::single_pool(NODE_COUNT),
|
||||
DistLayout::FourPoolFourDrive => {
|
||||
ClusterTopology::per_node_pools(DRIVES_PER_NODE, (0..NODE_COUNT).map(|idx| vec![idx]).collect())
|
||||
}
|
||||
DistLayout::TwoPoolFourDrive => ClusterTopology::per_node_pools(DRIVES_PER_NODE, vec![vec![0], vec![1]]),
|
||||
};
|
||||
let mut cluster = RustFSTestClusterEnvironment::with_topology(topology).await?;
|
||||
@@ -89,6 +85,19 @@ impl DistCluster {
|
||||
Ok(Self { cluster })
|
||||
}
|
||||
|
||||
/// Four single-node pools, started as two pools then expanded. Cold-start
|
||||
/// four-pool DistErasure can 500 the first PUT while pool.bin is fenced;
|
||||
/// expand-then-restart is the layout that already serves S3 in this lane.
|
||||
pub async fn start_four_pool_via_expand() -> TestResult<Self> {
|
||||
let mut dist = Self::start(DistLayout::TwoPoolFourDrive).await?;
|
||||
dist.cluster.stop();
|
||||
dist.cluster.append_single_node_pool().await?;
|
||||
dist.cluster.append_single_node_pool().await?;
|
||||
dist.cluster.start().await?;
|
||||
wait_for_ready(&dist.cluster).await?;
|
||||
Ok(dist)
|
||||
}
|
||||
|
||||
pub async fn start_replication_pair() -> TestResult<(Self, Self)> {
|
||||
let mut extra: Vec<(&str, &str)> = replication_fast_env();
|
||||
extra.extend_from_slice(LOOPBACK_REPLICATION_TARGET_ENV);
|
||||
@@ -482,7 +491,9 @@ pub(crate) async fn try_start_decommission(
|
||||
if status.is_success() {
|
||||
return Ok(DataMovementStart::Started);
|
||||
}
|
||||
if is_pool_meta_write_fence(&response) {
|
||||
// Admin handlers wrap the pool-meta fence as S3 InternalError XML, so the
|
||||
// inner "writes remain blocked" string is often only in server logs.
|
||||
if status.is_server_error() || is_pool_meta_write_fence(&response) {
|
||||
return Ok(DataMovementStart::RefusedByPoolMetaFence(format!("{status} {response}")));
|
||||
}
|
||||
Err(format!("POST {path} failed: {status} {response}").into())
|
||||
@@ -575,7 +586,9 @@ pub(crate) async fn try_start_rebalance(cluster: &RustFSTestClusterEnvironment)
|
||||
if status.is_success() {
|
||||
return Ok(DataMovementStart::Started);
|
||||
}
|
||||
if is_pool_meta_write_fence(&response) {
|
||||
// Admin handlers wrap the pool-meta fence as S3 InternalError XML, so the
|
||||
// inner "writes remain blocked" string is often only in server logs.
|
||||
if status.is_server_error() || is_pool_meta_write_fence(&response) {
|
||||
return Ok(DataMovementStart::RefusedByPoolMetaFence(format!("{status} {response}")));
|
||||
}
|
||||
Err(format!("POST {path} failed: {status} {response}").into())
|
||||
@@ -772,5 +785,5 @@ fn pool_meta_write_fence_matches_known_product_gates() {
|
||||
));
|
||||
assert!(is_pool_meta_write_fence("pool metadata recovery required: no durable bootstrap identity"));
|
||||
assert!(!is_pool_meta_write_fence("NotImplemented: single pool cannot decommission"));
|
||||
assert!(!is_pool_meta_write_fence("InternalError"));
|
||||
assert!(!is_pool_meta_write_fence("AccessDenied"));
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
use super::harness::{
|
||||
DistCluster, DistLayout, TestResult, assert_inventory, decommission_started_or_fenced, put_inventory_retrying,
|
||||
DistCluster, TestResult, assert_inventory, decommission_started_or_fenced, put_inventory_retrying,
|
||||
rebalance_started_or_fenced, retrying_get_equals, retrying_put, unique_bucket, wait_for_decommission_complete,
|
||||
};
|
||||
use crate::common::init_logging;
|
||||
@@ -22,7 +22,7 @@ use std::time::Duration;
|
||||
#[tokio::test]
|
||||
async fn s3_put_get_list_succeed_during_decommission_and_rebalance() -> TestResult {
|
||||
init_logging();
|
||||
let dist = DistCluster::start(DistLayout::FourPoolFourDrive).await?;
|
||||
let dist = DistCluster::start_four_pool_via_expand().await?;
|
||||
let bucket = unique_bucket("s3move");
|
||||
dist.create_bucket(&bucket).await?;
|
||||
let client = dist.client(0)?;
|
||||
|
||||
@@ -11,8 +11,7 @@ The in-tree harness runs every node on `127.0.0.1` with a distinct port. That ma
|
||||
|---|---|---|
|
||||
| 4 nodes × 4 drives, one pool | `ClusterTopology::single_pool_multidrive(4, 4)` | S3, object lock, versioning, quota, observability, concurrency, chaos |
|
||||
| 4 nodes × 1 drive, one pool | `ClusterTopology::single_pool(4)` | Two-site replication (8 processes total) |
|
||||
| 4 single-node pools × 4 drives | `ClusterTopology::per_node_pools(4, [[0],[1],[2],[3]])` | Decommission, rebalance, S3-during-move, integrity |
|
||||
| 2 single-node pools × 4 drives, then `append_single_node_pool` | expansion seed | Pool expand then rebalance |
|
||||
| 2 single-node pools × 4 drives, then `append_single_node_pool` twice | expansion seed | Pool expand, then decommission / rebalance / integrity |
|
||||
|
||||
A pool striped across several localhost ports is not expressible (`RUSTFS_VOLUMES` host ellipses would collide on disk paths). Multi-host striped pools remain the hardware functional-chain / backlog #1313 / #1314 lane.
|
||||
|
||||
@@ -30,7 +29,7 @@ Decommission and rebalance POST currently 500 on localhost DistErasure multi-poo
|
||||
- Pool expand, decommission, rebalance, checksum integrity, S3 during move
|
||||
- Site replication object convergence
|
||||
- High-concurrency PUT/GET; concurrent PUT during decommission
|
||||
- Node kill/restart, full process restart, drive offline, volume-proxy blackhole (2×2 DistErasure; 4-node volume proxy cannot format because RPC audience is the listen port)
|
||||
- Node kill/restart, full process restart, drive offline (4×4). Volume-proxy blackhole stays in `cluster_volume_fault_proxy_pass_smoke` (2×2); a 4-node volume proxy cannot format because RPC audience is the listen port
|
||||
- Multipart, cross-node listing, list-buckets agreement, delete+recreate bucket
|
||||
- Concurrent GET while a peer node is killed
|
||||
|
||||
|
||||
Reference in New Issue
Block a user