Skip to content

Support following S3 bucket region redirects when the CRT client is enabled - #10623

Open
ashovlin wants to merge 3 commits into
v2from
shovlia/crt-redirects
Open

Support following S3 bucket region redirects when the CRT client is enabled#10623
ashovlin wants to merge 3 commits into
v2from
shovlia/crt-redirects

Conversation

@ashovlin

@ashovlin ashovlin commented Sep 8, 2026

Copy link
Copy Markdown
Member

Issue #, if available: CLI-9212

Description of changes: This adds support for following S3 bucket region redirects for the high-level s3 commands when the CRT transfer manager is enabled.

  • CRTTransferCoordinator still coordinates the entire transfer for a single object. It now wraps the initial request and the optional retry, and exposes future-style cancel/result/etc. that apply to the either transfer, and act on either underlying request
  • CRTTransferManager - rather than containing a single CRT client, now caches a map of regions to clients
  • For the actual redirect logic, I extracted the guts of S3RegionRedirectorv2 so we can use them for CRT requests. I added CRTS3RegionRedirectPolicy on top which checks some CRT-specific conditions, and get_bucket_region transforms the CRT error into something S3RegionRedirectorv2 can work with
  • LazyHeadBucketClient - our existing Python redirect logic supports calling HeadBucket if it receives an error but cannot parse the region from the error response headers. I didn't see this case in testing, but to preserve the code path, we just a lazy boto client if needed.
  • Throughout, there is more initialization around clients, factories, etc. and some more bookkeeping of S3 request parameters that we need to adjust for the retry if needed.

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

@ashovlin
ashovlin requested a review from a team as a code owner September 8, 2026 15:10
@hssyoo
hssyoo self-requested a review September 8, 2026 15:12
@ashovlin

Copy link
Copy Markdown
Member Author

Added some fixes in c9feb20 after reviewing locally in a fresh Claude session:

  • Added try/except handling around redirect logic, to ensure a transfer finishes with an error
  • Moved the redirect region lookup (which possibly makes a boto HeadBucket call) to a new thread, to free up the CRT event loop
  • Cleaned up locking around the bucket -> redirect region cache
  • Avoided sending redirect requests if the region happens to match the original (not expected)

Comment thread awscli/botocore/utils.py
Comment on lines 1623 to 1629
if new_region is None:
logger.debug(
f"S3 client configured for region {client_region} but the bucket {bucket} is not "
"in that region and the proper region could not be "
"automatically determined."
)
return

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.

Previously, if error was not redirect-related, it would return early and this debug log would never fire. Now, if the error is not redirect-related, it returns early only for self.get_redirect_region(). Since new_region = None in this case, the debug log fires anyways. We should break out redirect check in get_redirect_region() to something like _is_redirect_response() and return from redirect_from_error early.

Comment thread awscli/s3transfer/crt.py
on_done_after_calls,
)
)
crt_client = self.get_crt_client(region)

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.

Can we move client creation outside of the lock?

Comment thread awscli/s3transfer/crt.py
Comment on lines +1173 to +1180
if error is not None and self._can_redirect(
is_region_redirect, bytes_transferred
):
# Discovering a region and serializing the retry can both
# block, and this runs on a CRT completion thread, where
# blocking stalls every other transfer sharing the event loop.
self._dispatch_redirect(redirect_and_finish, error, kwargs)
return

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.

Based on the logic of _can_redirect() and CRTS3RegionRedirectPolicy.is_error_redirect_candidate(), we're not actually checking if it's a redirect error before spawning a thread. So any error that doesn't get filtered out spawns a crt-s3-region-redirect. Can we check the instance of error and filter out non S3 redirect errors before spawning?

Comment thread awscli/s3transfer/crt.py
# request cannot be built for a region that changes while it is built.
# One lock covers every bucket because a command generally transfers
# to or from a single one.
self._region_lock = threading.Lock()

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.

It looks like this lock is acquired for both region discovery (get_retry_region()) and cache lookup (locked_bucket_region()). The issue is that the cache lookup occurs during CRT request serialization, so it's possible for it to be blocked while get_retry_region has a lock for HeadObject. In the worst case, a long-running HeadObject (retries, timeouts, etc) can block CRT transfer requests that may have correctly configured buckets. I think a workaround here is to use 2 locks.

Comment thread awscli/s3transfer/crt.py
bucket is resubmitted in the bucket's region, which makes a second
request for the same transfer.
"""
self._request_factory = request_factory

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.

Can we clear this on complete()? It looks like the source of a memory leak

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