test(e2e): require quota admission on over-limit 4x4 PUT

Do not treat SlowDown or AccessDenied as a hard-quota rejection.
Match InvalidRequest/QuotaExceeded only with a Bucket quota exceeded
message, matching the single-node quota e2e contract.

Co-authored-by: RustFS <hello@rustfs.com>
This commit is contained in:
Cursor Agent
2026-09-04 16:22:07 +00:00
parent 58c350b615
commit f597cdf505
2 changed files with 36 additions and 8 deletions
+2 -2
View File
@@ -1,2 +1,2 @@
sha256-linux=47c690dc23add078813d44a78b6ab4a4347a5ecef9cf072bb71f08bd5ce1efe6
sha256-darwin=47c690dc23add078813d44a78b6ab4a4347a5ecef9cf072bb71f08bd5ce1efe6
sha256-linux=4988bad7f5929152e0744f07393bc5d24aba2726e5daafa0eeca9a8c6a1f5683
sha256-darwin=4988bad7f5929152e0744f07393bc5d24aba2726e5daafa0eeca9a8c6a1f5683
@@ -21,6 +21,20 @@ use aws_sdk_s3::error::ProvideErrorMetadata;
use http::Method;
use std::time::Duration;
/// `Ok(true)` quota admission rejected the PUT, `Ok(false)` retry, `Err` not quota.
fn quota_over_limit_put_outcome(code: Option<&str>, message: Option<&str>) -> Result<bool, String> {
let quota_message = message.is_some_and(|text| text.starts_with("Bucket quota exceeded"));
match code {
Some("InvalidRequest" | "QuotaExceeded") if quota_message => Ok(true),
Some("SlowDown" | "ServiceUnavailable") => Ok(false),
Some("AccessDenied") => Err("AccessDenied is not a quota admission rejection".to_string()),
Some("InvalidRequest" | "QuotaExceeded") => {
Err(format!("InvalidRequest/QuotaExceeded without quota admission message: {message:?}"))
}
other => Err(format!("unexpected over-quota error code {other:?} message {message:?}")),
}
}
#[tokio::test]
async fn four_node_bucket_replication_converges_to_peer_cluster() -> TestResult {
init_logging();
@@ -98,12 +112,10 @@ async fn four_node_four_drive_hard_quota_rejects_over_limit_put() -> TestResult
Ok(_) => Ok(false),
Err(error) => {
let code = error.as_service_error().and_then(ProvideErrorMetadata::code);
if matches!(code, Some("QuotaExceeded" | "SlowDown" | "AccessDenied" | "InvalidRequest")) {
Ok(true)
} else if matches!(code, Some("ServiceUnavailable")) {
Ok(false)
} else {
Err(format!("unexpected over-quota error: {error:?}").into())
let message = error.as_service_error().and_then(ProvideErrorMetadata::message);
match quota_over_limit_put_outcome(code, message) {
Ok(done) => Ok(done),
Err(detail) => Err(format!("{detail}: {error:?}").into()),
}
}
}
@@ -114,3 +126,19 @@ async fn four_node_four_drive_hard_quota_rejects_over_limit_put() -> TestResult
.await?;
Ok(())
}
#[test]
fn quota_over_limit_put_outcome_requires_quota_admission() {
assert_eq!(
quota_over_limit_put_outcome(Some("InvalidRequest"), Some("Bucket quota exceeded for bucket x")),
Ok(true)
);
assert_eq!(
quota_over_limit_put_outcome(Some("QuotaExceeded"), Some("Bucket quota exceeded")),
Ok(true)
);
assert_eq!(quota_over_limit_put_outcome(Some("SlowDown"), Some("slow down")), Ok(false));
assert_eq!(quota_over_limit_put_outcome(Some("ServiceUnavailable"), Some("unavailable")), Ok(false));
assert!(quota_over_limit_put_outcome(Some("AccessDenied"), Some("Access Denied")).is_err());
assert!(quota_over_limit_put_outcome(Some("InvalidRequest"), Some("invalid argument")).is_err());
}