Cache Azure user delegation key for SAS signing#781
Conversation
3aa759a to
2690d70
Compare
2690d70 to
5067d88
Compare
Agreed -- they use TokenCache S3: arrow-rs-object-store/src/aws/credential.rs Line 759 in 09adc41 GCP: arrow-rs-object-store/src/client/mod.rs Line 1053 in 09adc41 |
alamb
left a comment
There was a problem hiding this comment.
Thanks @emilk -- I went through this carefully and I think it makes sense to me
I left a few small comments but I don't think any of them are required for merge (we can do them as follow on PRs for example)
The one thing I would like to see before we merge this is some test of the actual flow (e.g. that cached credentials are actually fetched and reused). I took the liberty of pushing such a test -- please give it a look
Also, @kevinjqliu as our resident Azure expert, does this behavior make sense to you?
| /// How long a freshly fetched user delegation key is requested to remain valid. | ||
| /// | ||
| /// The SAS tokens we sign with it stay short-lived; this only bounds how often | ||
| /// we call `GetUserDelegationKey`. Azure caps the key lifetime at 7 days. |
There was a problem hiding this comment.
Can you add a URL that with some evidence for this claim? It seems this could be a good one
https://learn.microsoft.com/en-us/rest/api/storageservices/get-user-delegation-key#request-body
Start Required. The start time for the user delegation SAS, in ISO date format. It must be a valid date and time within seven days of the current date.
|
|
||
| /// How long a freshly fetched user delegation key is requested to remain valid. | ||
| /// | ||
| /// The SAS tokens we sign with it stay short-lived; this only bounds how often |
There was a problem hiding this comment.
I think it would help to define SAS at least once (I realize it is used elsewhere in this module without that)
| /// The SAS tokens we sign with it stay short-lived; this only bounds how often | |
| /// The shared access signature (SAS) tokens we sign with it stay short-lived; this only bounds how often |
|
|
||
| /// Fetch a user delegation key valid for `validity` and wrap it as a | ||
| /// [`TemporaryToken`] so [`TokenCache`] can expire it. | ||
| async fn fetch_delegation_key( |
There was a problem hiding this comment.
I found the naming somewhat confusing here (as it wasn't clear to me what the relationship between get_user_delegation_key and fetch_user_delegation_key was (get and fetch are very similar). I wa sall the more confused because get actually does the network call, when I would normally exepct fetch to do that
Something like this might be easier to understand
- fetch_delegation_key --> get_delegation_key
- (current) get_delegation_key --> get_delegation_key_inner
|
I took the liberty of pushing the comment changes and function name changes. I'll plan to merge this once CI passes Thanks @emilk |
|
I verified this with my own azure storage account, it works great. Thanks for adding this! I think there are a few follow-ups could make the delegation-key cache more flexible and robust:
EDIT: tracking as #807 |
Rationale for this change
AzureClient::signer()callsget_user_delegation_key()— aGetUserDelegationKeynetwork round-trip (POST /?restype=service&comp=userdelegationkey) — on everysigned_url/signed_urlscall. A user delegation key is reusable until its expiry, so re-fetching it per request is wasteful and, under load, gets throttled by Azure (HTTP 503ServerBusy).Notably the other backends do not make an uncached network call while signing: the AWS signer signs locally from its cached credential, and GCP caches its signing credentials. Azure is the outlier — it makes a
GetUserDelegationKeycall on top of the already-cached AAD token.We hit this in production: a workload issuing many presigned-URL requests against Azure Blob drove ~100k
GetUserDelegationKeyPOSTs in 2h (~840/min), ~35% of which returned 503. The retry client backed off 10× (~10s) and then surfaced the error to the caller.What changes are included in this PR?
Reuse the existing
TokenCache<T>to cache the user delegation key onAzureClient:TokenCacheonly returns a key with more than itsmin_ttl(2h here) remaining, so any SAS no longer than that is guaranteed to expire before its key (a SAS must not outlive the key it is signed with). The rare longer-lived SAS fetch a dedicated key.SignedExpiry), not when we requested.TokenCache’s refresh-race double-check and fetch backoff.TokenCache::with_min_ttlis extended to theazure-basefeature so the Azure client can configure the cache (previously gated toaws-base/gcp-base).The signed SAS tokens themselves are unchanged (still caller-specified
expires_in).Are there any user-facing changes?
No public API changes. Presigned-URL generation against Azure now performs far fewer
GetUserDelegationKeyrequests.Tests
Added
azure::client::tests::test_delegation_key_expiryfor the granted-vs-requested expiry parsing. The caching/refresh machinery itself is covered by the existingclient::tokentests.Related issues / PRs
No existing issue tracks this specifically. Related:
GetUserDelegationKeycall this PR caches.azure/mod.rssigned_url/signed_urls); orthogonal change, flagging for awareness of a possible minor merge interaction.