Repository navigation
feat(io): refresh vended storage credentials before they expire - #892
plusplusjiajia wants to merge 4 commits 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.
There was a problem hiding this comment.
Update: a refresh now only rotates keys, so the caveat above no longer applies.
| } | ||
| // 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.
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?
There was a problem hiding this comment.
Thanks! Done. A refresh now only rotates keys and never builds clients, so a request with expired keys waits up to 10s for one in flight. If that fails, the provider can't fail the request (the SDK sends empty keys unsigned), so S3 rejects the expired keys with ExpiredToken; new handles and deletes fail with TokenExpired.
| } | ||
| 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.
There was a problem hiding this comment.
Thanks! Done. Refreshed credentials are now matched by canonical prefix, so a reordered response is a key-only refresh. One that adds or drops a prefix or changes a setting is rejected.
| 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.
There was a problem hiding this comment.
Thanks! Done. Get checks transport errors first, so a timeout or a broken transfer after a 2xx is an IOError. I left POST as is: a commit may already have succeeded, and an error would clean up its files.
6f8cfe1 to
b6990f9
Compare
|
@wgtmac Thanks! Rebased onto main. The first commit is the previous one rebased; the second addresses this round. Since #783 made |
| std::lock_guard lock(mutex_); | ||
| if (current.has_value()) { | ||
| credentials_ = std::move(current).value(); | ||
| } |
There was a problem hiding this comment.
The AWS signer calls this provider while the new client is being built, before SetStorageCredentials installs the new keys. This replaces the initial keys with the old ones. If the FileIO is released before the handle's first request, that handle keeps the old keys; can we prevent this cache rollback?
There was a problem hiding this comment.
Thanks! Fixed. A client now asks the FileIO for keys only after its own install, so the signer's warm-up while building it keeps the keys it was built with. Added HandleKeepsItsKeysOnceTheFileIOIsGone, which reads through such a handle after the FileIO is released.
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.
A refresh may now only rotate keys: refreshed credentials must keep the S3 prefixes, matched by canonical prefix in any order, their settings and which of them sign with keys. It therefore never builds S3 clients, and a request whose keys have expired waits up to 10 seconds for a refresh in flight, as file operations do, instead of signing with the expired keys. A response that changes that layout is rejected like any unusable refresh, which drops the deferred client rebuild. Only keys vended with the settings a client was built with reach it, so keys meant for another endpoint are never sent to an old one. Keys, settings and token expiries come from the vended config merged over the FileIO's properties, and only a token that signs requests is tracked. For a repeated prefix, the first credential is used throughout. Expiries are tracked per prefix from the installed credentials, and a batch delete resolves each file as a single delete does, so a long batch follows refreshes. HttpClient::Get checks the transfer before the status: a timeout, or a transport error after a successful status, is an IOError. Writes keep their status, since one may have taken effect: failing a commit outright would clean up its files. An installed Arrow does not export the AWS SDK it bundles, so with one the system SDK is used for the S3 FileIO's credentials provider.
A response keeps its curl handle, which is attached to the client's connection pool. Destroying the client first left that handle pointing at a freed pool, so the POST test crashed when the response went away.
The AWS signer asks the credentials provider while the client is built, before SetStorageCredentials installs its keys, so the provider cached the keys being replaced. A handle whose FileIO was released before its first request then signed with them. A client now asks the FileIO for keys only once its own are installed.
13e8736 to
6972755
Compare
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. Only keys vended with the settings a client was built with reach it, so keys meant for another endpoint are never sent to an old one. A client asks only once its own keys are installed: the SDK also asks while building it, when the FileIO still holds the keys being replaced.TokenExpired; a batch delete resolves each file as a single delete does. A request on a handle opened earlier can only sign with the expired keys, which S3 rejects withExpiredToken: the SDK gives a credentials provider no way to report an error, and sends a request with empty keys unsigned.HttpClient::Gettakes a timeout; the REST credentials request uses 10 seconds. A GET whose transfer fails after a successful status, such as a body that stalls past the timeout, is anIOErrorrather than a success. Writes keep their status, since one may have taken effect: failing a commit outright would clean up its files.aws-cpp-sdk-corewhen S3 is enabled. An installed Arrow does not export the SDK it bundles, so with one the system SDK is used.Tests cover refresh timing, concurrency, unusable, failed and layout-changing replacements, reordered prefixes, expiry errors, provider initialization, HTTP timeouts and truncated responses, plus real S3 round trips: through REST FileIO and ResolvingFileIO, reading through a handle opened before a refresh (and not taking keys installed with other settings), reading through a handle after its FileIO is released, and a request that waits for a refresh under way once its keys have expired.