Skip to content

Fix flaky S3 races in test_cli put_object helper - #10765

Open
GarrettBeatty wants to merge 2 commits into
v2from
cli-8332-8387-test-cli-s3-races-v2
Open

GarrettBeatty wants to merge 2 commits into
v2from
cli-8332-8387-test-cli-s3-races-v2

Conversation

@GarrettBeatty

Copy link
Copy Markdown

Description of changes:

Fixes two intermittent failures in the TestBasicCommandFunctionality.put_object helper in tests/integration/test_cli.py, which is used by test_streaming_output_operation, test_no_sign_request, and test_no_paginate_arg.

  1. Public access block propagation (test_no_sign_request): replace the fixed 5s sleep with a bucket_exists waiter, poll GetPublicAccessBlock via ConsistencyWaiter until it consistently reports NoSuchPublicAccessBlockConfiguration, and retry PutObject on AccessDenied as a backstop for the public ACL being briefly rejected.
  2. DeleteBucket race during cleanup (test_streaming_output_operation): S3 can intermittently reject DeleteBucket with OperationAborted ("A conflicting conditional operation is currently in progress"). Cleanup now retries on that error code with exponential backoff (5/10/20/40s).

Test-only change; no customer-facing behavior changes.

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

Replace the fixed 5s sleep with a bucket_exists waiter, then poll
get_public_access_block until it consistently reports
NoSuchPublicAccessBlockConfiguration before uploading. Also retry
PutObject on AccessDenied as a backstop, since the public ACL can still
be rejected briefly after the block is reported deleted.

Fixes flaky test_no_sign_request (CLI-8332).
TestBasicCommandFunctionality.put_object registered a bare
client.delete_bucket cleanup, so a transient OperationAborted from S3
failed test_streaming_output_operation (and the other tests using the
helper). Retry with exponential backoff on that error code.

CLI-8387
@GarrettBeatty
GarrettBeatty marked this pull request as ready for review October 15, 2026 18:58
@GarrettBeatty
GarrettBeatty requested a review from a team as a code owner October 15, 2026 18:58
Comment on lines +55 to +59
# Deleting the public access block is eventually consistent, so
# wait until it is consistently reported as gone.
ConsistencyWaiter(min_successes=3, delay=2, max_attempts=30).wait(
self._public_access_block_deleted, client, bucket
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The consistency that you're claiming is not true as ConsistencyWaiter.wait succeeded even the success are not consistent (e.g. 0 0 0 0 1 0 1 0 0 1 still succeeded). This is because a failure doesn't reset the success count. Are there cases we get false positive so that we need consistent success? if not I suggest changing min_success to 1 and changing the comment.

Comment on lines +52 to +53
client.get_waiter('bucket_exists').wait(Bucket=bucket)
self.addCleanup(self._delete_bucket_with_retry, client, bucket)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that get_waiter is called before cleanup, if get_waiter raises, we have a bucket leak, meaning bucket won't be deleted.

Comment on lines +75 to +76

def _put_object_with_retry(self, client, call_args, attempts=5, delay=5):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: _put_object_with_retry and _delete_bucket_with_retry are essentially the same, consider creating a helper function to avoid duplicate codes

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants