fix(versioning): reject suspending versioning while a replication config exists (#6006)

PutBucketVersioning with Status=Suspended on a bucket that carries a replication configuration now fails with InvalidBucketState, matching AWS S3 and MinIO. Suspension would start minting null versions that the versioned replication engine can never converge — the state is unreachable on AWS and MinIO, and the nightly acceptance-matrix e2e that tried to exercise it failed every night since it landed (issue #5767).

The acceptance-matrix test tail now pins the rejection contract (InvalidBucketState) and verifies a fresh matched PUT still replicates with a real version id after the rejected suspension.
This commit is contained in:
Zhengchao An
2026-08-13 03:26:17 +08:00
committed by GitHub
parent ca4e66daab
commit f7df4fa62a
2 changed files with 55 additions and 27 deletions
@@ -4235,37 +4235,49 @@ async fn test_bucket_replication_acceptance_matrix_local_dual_targets() -> TestR
"tag rule with disabled delete-marker replication created a marker: {tagged_state:?}" "tag rule with disabled delete-marker replication created a marker: {tagged_state:?}"
); );
set_bucket_versioning(&source_env, source_bucket, BucketVersioningStatus::Suspended).await?; // AWS S3 and MinIO both reject suspending versioning on a bucket that
set_bucket_versioning(&target_env_a, target_bucket_a, BucketVersioningStatus::Suspended).await?; // carries a replication configuration (InvalidBucketState): suspension
let null_put = source_client // would mint null versions that versioned replication can never converge.
let suspend_err = source_client
.put_bucket_versioning()
.bucket(source_bucket)
.versioning_configuration(
VersioningConfiguration::builder()
.status(BucketVersioningStatus::Suspended)
.build(),
)
.send()
.await
.expect_err("suspending versioning on a replication source must be rejected");
assert_eq!(
suspend_err.as_service_error().and_then(|error| error.code()),
Some("InvalidBucketState"),
"suspension on a replication source must fail with InvalidBucketState: {suspend_err:?}"
);
// The rejected suspension must leave the versioning + replication state
// fully intact: a fresh matched PUT still replicates with a real version.
let post_reject_put = source_client
.put_object() .put_object()
.bucket(source_bucket) .bucket(source_bucket)
.key("prefix/null.txt") .key("prefix/after-rejected-suspend.txt")
.body(ByteStream::from_static(b"null version")) .body(ByteStream::from_static(b"still replicating"))
.send() .send()
.await?; .await?;
assert!(null_put.version_id().is_none(), "suspended source PUT must create a null version"); let post_reject_version_id = post_reject_put
wait_for_replication_state(&target_client_a, target_bucket_a, "null version did not replicate", |state| { .version_id()
state .ok_or("PUT after rejected suspension omitted version ID")?
.iter() .to_string();
.any(|entry| entry.key == "prefix/null.txt" && entry.version_id == "null" && !entry.delete_marker) wait_for_replication_state(
}) &target_client_a,
.await?; target_bucket_a,
let null_delete = source_client "replication stopped after rejected versioning suspension",
.delete_object() |state| {
.bucket(source_bucket) state
.key("prefix/null.txt") .iter()
.send() .any(|entry| entry.key == "prefix/after-rejected-suspend.txt" && entry.version_id == post_reject_version_id)
.await?; },
assert!( )
null_delete.version_id().is_none(),
"suspended source DELETE must create a null delete marker"
);
wait_for_replication_state(&target_client_a, target_bucket_a, "null delete marker did not replicate", |state| {
state
.iter()
.any(|entry| entry.key == "prefix/null.txt" && entry.version_id == "null" && entry.delete_marker)
})
.await?; .await?;
Ok(()) Ok(())
+16
View File
@@ -738,6 +738,22 @@ async fn validate_bucket_versioning_update(bucket: &str, config: &VersioningConf
Err(StorageError::ConfigNotFound) => {} Err(StorageError::ConfigNotFound) => {}
Err(err) => return Err(ApiError::from(err).into()), Err(err) => return Err(ApiError::from(err).into()),
} }
// AWS S3 and MinIO both refuse to suspend versioning while a replication
// configuration exists: suspension would start minting null versions that
// the replication engine (versioned by contract) can never converge.
if config.suspended() {
match metadata_sys::get_replication_config(bucket).await {
Ok(_) => {
return Err(S3Error::with_message(
S3ErrorCode::InvalidBucketState,
"A replication configuration is present on this bucket, bucket wide versioning cannot be suspended."
.to_string(),
));
}
Err(StorageError::ConfigNotFound) => {}
Err(err) => return Err(ApiError::from(err).into()),
}
}
Ok(()) Ok(())
} }