Repository navigation
feat(io): refresh vended storage credentials before they expire - #892
plusplusjiajia wants to merge 1 commit into
Conversation
d92ef4d to
7088aed
Compare
|
Thanks for adding this feature! I think it is too large to review which may incur a long delay. Perhaps let's split it into smaller ones so we can review it one by one? |
@wgtmac Thanks — split into a stack, smallest first:
Each builds and passes the full suite standalone; the diff here narrows as each one merges. |
7088aed to
b06186d
Compare
afe009b to
c15c6e3
Compare
|
Is this ready for review? |
c15c6e3 to
27d9bab
Compare
27d9bab to
bf50711
Compare
f0f0f83 to
a1cf5ef
Compare
| /// \brief Replace the credentials once they are close to expiring. | ||
| /// | ||
| /// Called before each handle is created; a handle keeps the delegate it was | ||
| /// built from, so I/O on an open one is not re-checked. A failure keeps the |
There was a problem hiding this comment.
Can we separate credential refresh from filesystem replacement? Existing handles and streams keep the old token, even after FileIO refreshes. A refreshable provider on the S3 client, as in Java, would cover those requests and avoid rebuilding clients for token-only changes.
There was a problem hiding this comment.
Thanks! Done. Each vended S3 client now signs through a VendedCredentialsProvider set as Arrow's S3Options::credentials_provider. The AWS SDK asks it before every request: it returns the keys the FileIO currently vends for the client's prefix and refreshes them once due, so existing handles and streams pick up new tokens. A token-only refresh keeps the existing clients; only a change of prefixes or settings builds new ones. One caveat: Arrow holds its client registry while a request runs, and building a client locks it exclusively, so a refresh started from a request that needs new clients is left to the next file operation.
| } | ||
| // Expired credentials leave nothing to proceed with, so wait instead. | ||
| if (!refresh_lock.try_lock_for(kExpiredCredentialWait)) { | ||
| return; |
There was a problem hiding this comment.
If this wait times out, the caller still uses credentials we know are expired. A failed refresh does the same. Return an error after expiry instead of creating a handle with unusable credentials.
There was a problem hiding this comment.
Thanks! Done. Each prefix's delegate now tracks its own expiry. Once it has passed and the refresh failed or the wait timed out, creating a handle or deleting under that prefix returns TokenExpired. Locations outside vended prefixes keep the default credentials.
|
|
||
| // `delegates`/`*refreshed` take the retired generation; both are declared | ||
| // before the lock, so it destructs only after the lock releases. | ||
| InstallCredentials(*refreshed, delegates); |
There was a problem hiding this comment.
This installs a returned token even if it is already expired or its expiry is missing or invalid. It can replace a still-valid token and then suppress retries for 30 seconds. Validate temporary credentials before installing them and keep the old set on failure.
There was a problem hiding this comment.
Thanks! Done. A refreshed set is installed only if every S3 credential is usable: a session token needs a parseable expiry that has not passed, and the set must contain an S3 prefix. Otherwise the current set is kept and the refresh backs off as for any failed refresh, never past the current expiry. A token without a usable expiry at the initial install still triggers an immediate refresh, but is no longer treated as expired.
| // Outside `mutex_`: both are slow and must not block readers. | ||
| Status status; | ||
| DelegatesByPrefix delegates; | ||
| auto refreshed = provider->Load(); |
There was a problem hiding this comment.
The 10-second limit only covers waiting for the refresh lock. The thread running Load() can block indefinitely in the REST request, even while the old credentials are still valid. Add a timeout to the refresh request itself.
There was a problem hiding this comment.
Thanks! Done. HttpClient::Get takes an optional timeout, and RestStorageCredentialProvider::Load() uses 10 seconds, so a stalled catalog no longer blocks the refreshing operation indefinitely; a timeout counts as a failed refresh.
Vended storage credentials expire, so new file operations on a reused table could eventually fail. ArrowS3FileIO now refreshes them through the StorageCredentialProvider installed by InitializeStorageCredentials, starting five minutes before the earliest applicable s3.session-token-expires-at-ms. Later SetStorageCredentials calls keep the provider. Each vended S3 client signs through a VendedCredentialsProvider, which the AWS SDK asks before every request: it refreshes once due and returns the keys the FileIO currently vends for the client's prefix, as Java's VendedCredentialsProvider does. Handles and streams opened before a refresh therefore use the new keys, including those on clients a later refresh replaced. A refresh that changes only keys keeps the existing clients. A request cannot build clients, since Arrow holds its client registry for the request and building one locks it exclusively. A refresh it starts that needs new clients is left to the next file operation, and it never waits on another refresh. A refresh is installed only if every S3 credential is usable: a session token must carry a parseable expiry that has not passed. Otherwise, or if loading fails, the current set is kept and the refresh backs off for up to 30 seconds. Concurrent operations share one refresh, and operations with expired credentials wait up to 10 seconds for one in flight. A concurrent credential install supersedes any older refresh result. Once a delegate's credentials have expired and could not be refreshed, creating a handle or deleting under its prefix fails with TokenExpired instead of handing out credentials known to be unusable. The LoadCredentials request had no deadline, so a stalled catalog could block a file operation indefinitely. HttpClient::Get takes an optional timeout, after which the request fails with an IOError, and RestStorageCredentialProvider uses 10 seconds. The bundle now links aws-cpp-sdk-core when S3 is enabled.
a1cf5ef to
6f8cfe1
Compare
|
Could you rebase onto main and resolve the conflicts? |
| if (!refresh_lock.try_lock()) { | ||
| // Another operation is already fetching; normally just use what we have. | ||
| if (from_request) { | ||
| return; |
There was a problem hiding this comment.
This also skips the refresh when the token has expired. Another request on an open stream then signs with that expired token. Can we wait for a request-safe credential refresh, or fail explicitly instead of returning expired keys?
| } | ||
| auto result = delegates; | ||
| for (size_t i = 0; i < result.size(); ++i) { | ||
| if (!result[i].vended_keys || !DiffersOnlyInKeys(*current_s3[i], *refreshed_s3[i])) { |
There was a problem hiding this comment.
Can we match these credentials by prefix instead of array position? The server may return the same prefixes in a different order. That makes a key-only refresh look like a client change, so an existing stream keeps its old token.
| return {}; | ||
| } | ||
| // No response to hand the error handler. | ||
| if (response.error.code == cpr::ErrorCode::OPERATION_TIMEDOUT) { |
There was a problem hiding this comment.
Check the transport error before accepting the HTTP status. A server can send 200 headers and then stall during the body. CPR keeps status 200 when the transfer times out, so this returns success instead of an IOError.
Vended storage credentials expire, so new file operations on a reused table can eventually fail.
ArrowS3FileIOnow refreshes them on demand, starting five minutes before the earliest applicables3.session-token-expires-at-ms.Builds on the
StorageCredentialProviderAPI merged in #899:InitializeStorageCredentials()installs the initial credentials and provider before first use. LaterSetStorageCredentials()calls retain the provider.VendedCredentialsProvider(anAws::Auth::AWSCredentialsProviderset as Arrow'sS3Options::credentials_provider), which the AWS SDK asks before every request. It returns the keys the FileIO currently vends for the client's prefix and refreshes them once due, as Java'sVendedCredentialsProviderdoes, so handles and streams opened before a refresh use the new keys. A refresh that changes only keys keeps the existing clients.TokenExpired.HttpClient::Gettakes an optional timeout; the REST credentials request uses 10 seconds.Tests cover refresh timing, concurrency, unusable and failed replacements, expiry errors, provider initialization and the HTTP timeout, plus real S3 round trips: through REST FileIO and ResolvingFileIO, and reading through a handle opened before a refresh, both for a keys-only refresh and for one that replaces the client.