mirror of
https://github.com/rustfs/rustfs.git
synced 2026-09-06 20:19:14 +00:00
07833379b4
* test(e2e): add distributed 4x4 validation * test(e2e): prove operations overlap data movement --------- Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
189 lines
6.8 KiB
Rust
189 lines
6.8 KiB
Rust
// Copyright 2026 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/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.
|
|
|
|
use super::harness::{DistCluster, DistLayout, TestResult, enable_versioning, get_object_bytes, put_object, unique_bucket};
|
|
use crate::common::init_logging;
|
|
use aws_sdk_s3::error::ProvideErrorMetadata;
|
|
use aws_sdk_s3::types::{BucketVersioningStatus, VersioningConfiguration};
|
|
|
|
#[tokio::test]
|
|
async fn four_node_four_drive_versioning_put_list_get_delete_marker() -> TestResult {
|
|
init_logging();
|
|
let dist = DistCluster::start(DistLayout::FourByFour).await?;
|
|
let bucket = unique_bucket("version");
|
|
dist.create_bucket(&bucket).await?;
|
|
let writer = dist.client(0)?;
|
|
let reader = dist.client(3)?;
|
|
enable_versioning(&writer, &bucket).await?;
|
|
|
|
let key = "versioned.txt";
|
|
let v1_id = writer
|
|
.put_object()
|
|
.bucket(&bucket)
|
|
.key(key)
|
|
.body(b"v1".to_vec().into())
|
|
.send()
|
|
.await?
|
|
.version_id()
|
|
.ok_or("v1 PUT omitted version ID")?
|
|
.to_string();
|
|
let v2_id = writer
|
|
.put_object()
|
|
.bucket(&bucket)
|
|
.key(key)
|
|
.body(b"v2".to_vec().into())
|
|
.send()
|
|
.await?
|
|
.version_id()
|
|
.ok_or("v2 PUT omitted version ID")?
|
|
.to_string();
|
|
|
|
let versions = reader.list_object_versions().bucket(&bucket).prefix(key).send().await?;
|
|
let matching_versions: Vec<_> = versions
|
|
.versions()
|
|
.iter()
|
|
.filter(|version| version.key() == Some(key))
|
|
.collect();
|
|
assert_eq!(matching_versions.len(), 2, "fresh key must have exactly two versions: {versions:?}");
|
|
assert!(versions.delete_markers().is_empty(), "fresh key unexpectedly has a delete marker");
|
|
assert!(
|
|
matching_versions
|
|
.iter()
|
|
.any(|version| version.version_id() == Some(v1_id.as_str()) && version.is_latest() != Some(true)),
|
|
"v1 was not the historical version: {versions:?}"
|
|
);
|
|
assert!(
|
|
matching_versions
|
|
.iter()
|
|
.any(|version| version.version_id() == Some(v2_id.as_str()) && version.is_latest() == Some(true)),
|
|
"v2 was not the latest version: {versions:?}"
|
|
);
|
|
|
|
let latest = get_object_bytes(&reader, &bucket, key).await?;
|
|
assert_eq!(latest, b"v2");
|
|
|
|
let older = reader.get_object().bucket(&bucket).key(key).version_id(&v1_id).send().await?;
|
|
let older_body = older.body.collect().await?.into_bytes();
|
|
assert_eq!(older_body.as_ref(), b"v1");
|
|
|
|
let deleted = writer.delete_object().bucket(&bucket).key(key).send().await?;
|
|
assert_eq!(deleted.delete_marker(), Some(true));
|
|
let marker_id = deleted.version_id().ok_or("DeleteObject omitted delete-marker version ID")?;
|
|
let after_delete = reader.list_object_versions().bucket(&bucket).prefix(key).send().await?;
|
|
let matching_markers: Vec<_> = after_delete
|
|
.delete_markers()
|
|
.iter()
|
|
.filter(|marker| marker.key() == Some(key))
|
|
.collect();
|
|
assert_eq!(
|
|
matching_markers.len(),
|
|
1,
|
|
"delete marker missing or duplicated after current-version delete: {after_delete:?}"
|
|
);
|
|
assert!(
|
|
matching_markers[0].version_id() == Some(marker_id) && matching_markers[0].is_latest() == Some(true),
|
|
"DeleteObject response and ListObjectVersions disagree about the marker: {after_delete:?}"
|
|
);
|
|
|
|
let latest_after_delete = reader.get_object().bucket(&bucket).key(key).send().await;
|
|
match latest_after_delete {
|
|
Ok(_) => return Err("current version should be a delete marker".into()),
|
|
Err(error)
|
|
if error
|
|
.as_service_error()
|
|
.and_then(ProvideErrorMetadata::code)
|
|
.is_some_and(|code| code == "NoSuchKey" || code == "NotFound") => {}
|
|
Err(error) => return Err(error.into()),
|
|
}
|
|
|
|
let restored = reader.get_object().bucket(&bucket).key(key).version_id(&v1_id).send().await?;
|
|
let restored_body = restored.body.collect().await?.into_bytes();
|
|
assert_eq!(restored_body.as_ref(), b"v1");
|
|
|
|
writer
|
|
.delete_object()
|
|
.bucket(&bucket)
|
|
.key(key)
|
|
.version_id(marker_id)
|
|
.send()
|
|
.await?;
|
|
assert_eq!(get_object_bytes(&reader, &bucket, key).await?, b"v2");
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn four_node_versioning_suspension_keeps_one_null_version_and_history() -> TestResult {
|
|
init_logging();
|
|
let dist = DistCluster::start(DistLayout::FourByFour).await?;
|
|
let bucket = unique_bucket("suspend");
|
|
dist.create_bucket(&bucket).await?;
|
|
let writer = dist.client(0)?;
|
|
let reader = dist.client(3)?;
|
|
enable_versioning(&writer, &bucket).await?;
|
|
|
|
let key = "suspended.txt";
|
|
let original = writer
|
|
.put_object()
|
|
.bucket(&bucket)
|
|
.key(key)
|
|
.body(b"enabled-history".to_vec().into())
|
|
.send()
|
|
.await?
|
|
.version_id()
|
|
.ok_or("enabled PUT omitted version ID")?
|
|
.to_string();
|
|
writer
|
|
.put_bucket_versioning()
|
|
.bucket(&bucket)
|
|
.versioning_configuration(
|
|
VersioningConfiguration::builder()
|
|
.status(BucketVersioningStatus::Suspended)
|
|
.build(),
|
|
)
|
|
.send()
|
|
.await?;
|
|
|
|
put_object(&writer, &bucket, key, b"null-one".to_vec()).await?;
|
|
put_object(&writer, &bucket, key, b"null-two".to_vec()).await?;
|
|
assert_eq!(get_object_bytes(&reader, &bucket, key).await?, b"null-two");
|
|
|
|
let versions = reader.list_object_versions().bucket(&bucket).prefix(key).send().await?;
|
|
let matching: Vec<_> = versions
|
|
.versions()
|
|
.iter()
|
|
.filter(|version| version.key() == Some(key))
|
|
.collect();
|
|
assert!(matching.iter().any(|version| version.version_id() == Some(original.as_str())));
|
|
let null_version_count = matching
|
|
.iter()
|
|
.filter(|version| {
|
|
matches!(
|
|
version.version_id(),
|
|
None | Some("") | Some("null") | Some("00000000-0000-0000-0000-000000000000")
|
|
)
|
|
})
|
|
.count();
|
|
assert_eq!(null_version_count, 1, "suspended overwrites must keep one null version: {versions:?}");
|
|
|
|
let historical = reader
|
|
.get_object()
|
|
.bucket(&bucket)
|
|
.key(key)
|
|
.version_id(&original)
|
|
.send()
|
|
.await?;
|
|
assert_eq!(historical.body.collect().await?.into_bytes().as_ref(), b"enabled-history");
|
|
Ok(())
|
|
}
|