Repository navigation
Conversation
350dc82 to
010d6bc
Compare
|
@kevinjqliu I opened this PR as the previous one was closed for being stale. Here I've hopefully addressed your comments!
|
|
+1 (non-binding). GCS support would be pretty to add to this. |
| catalog=self, | ||
| ) | ||
|
|
||
| def _attach_credentials_provider( |
There was a problem hiding this comment.
Can we rename this to _attach_credentials_provider_to_io?
| longest-prefix matching against the freshly vended credentials. | ||
| """ | ||
| if storage_credentials and property_as_bool(self.properties, REFRESH_CREDENTIALS_ENABLED, False): | ||
| io.set_credentials_provider( |
There was a problem hiding this comment.
This breaks pickling support on FileIO. We can't pickle lambdas.
We have some tests for pickling FileIO, but none of them break because this is behind REFRESH_CREDENTIALS_ENABLE
There was a problem hiding this comment.
Good catch, this is a real blocker for this. If we want to keep the concern of refreshing the credentials by calling the catalog.load_credentials then we cannot reliable pickle this object. Specially this we would need to pickle AuthManagers and its locks and it would become a very fragile and prone to break.
I bellieve we should re-visit allowing the credential provider make the API call itself so there is no reference to any lambdas or catalog object dependency
There was a problem hiding this comment.
What if we just drop the lock when pickling? When the object is unpickled, it's a brand new instance anyways. It shouldn't have the same lock to begin with.
There was a problem hiding this comment.
Dropping and recreating the lock makes sense. My concern is that the refresh lambda still captures the catalog instance, and the catalog contains additional state like sessions, auth managers, and possibly other locks that may not be pickleable.
So we’d still need to make the refresh call independent of the catalog or reconstruct it after unpickling/
|
|
||
| def _can_refresh(self, location: str) -> bool: | ||
| scheme = urlparse(location).scheme | ||
| refresh_by_scheme = self._needs_refresh_by_scheme.get(scheme) |
There was a problem hiding this comment.
If the TTL on a S3 token is < 300s (the default value), we'll attempt to refresh on every call. That's a lot of possibly unnecessary refreshes.
It seems like we should track when the last refresh was and use that information to determine when we should next refresh.
Java does something similar in scheduleCredentialRefresh
There was a problem hiding this comment.
IIUC your comment, we wont refresh on every call, since the lock will hold until the refresh completes and all other threads will not reach the load_credentials
| The refresh callback returns the full LoadCredentialsResponse so the provider can re-run | ||
| longest-prefix matching against the freshly vended credentials. | ||
| """ | ||
| if storage_credentials and property_as_bool(self.properties, REFRESH_CREDENTIALS_ENABLED, False): |
There was a problem hiding this comment.
We should check that the server has access to the LoadCredentials endpoint. Otherwise, it'll probably throw an error.
(If you're setting this property, your catalog probably has the proper endpoints, but we should verify anyways)
There was a problem hiding this comment.
the self._load_credentials() method performs the check
| return best_match.config if best_match else {} | ||
|
|
||
|
|
||
| class CredentialsProvider: |
There was a problem hiding this comment.
I think this class ultimately needs to less S3-specific and delegate some functionality into scheme-specific handlers. I don't think that has to be part of this PR though, it could be part of a following that adds support for other schemes, such as GCS.
There was a problem hiding this comment.
Overall, I agree. I think we'll have a better idea of the shape of this class once we implement GCS.
|
Thanks for the reviews! I will make the changes this week been AFK |
|
@gabeiglio super excited for this! Anything I can do to help out? |
|
I'm excited for this too! I have some long-running PyIceberg readers and will be happy to use this when it lands 🚀 |
|
This pull request has been marked as stale due to 30 days of inactivity. It will be closed in 1 week if no further activity occurs. If you think that's incorrect or this pull request requires a review, please simply write any comment. If closed, you can revive the PR at any time and @mention a reviewer or discuss it on the dev@iceberg.apache.org list. Thank you for your contributions. |
|
@gabeiglio if you're not able to continue with this, I'd be happy to give it a shot. |
|
sorry about the silence here, @dossett feel free to take this further! |
|
@gabeiglio Thank you! would you mind giving me write access to this PR branch? That would let me keep everything together here. But no worries, if you'd rather me open a new PR. |
|
done sent the invite to my fork |
010d6bc to
dc32974
Compare
|
this got dropped from my radar... i'll take a look! |
|
I pushed a few commits, hopefully broken up in a sensible way. My local integration testing confirms this is working for GCS. I can't easily live test S3 at the moment.
|
|
@kevinjqliu @rambleraptor feedback welcome on the changes |
rambleraptor
left a comment
There was a problem hiding this comment.
Thanks so much for your work on this! I've got a few comments for you. Happy to take another review.
My biggest concern is just making sure that we don't break existing users with this.
| if (expiration := self.expiration_ms(properties)) is not None: | ||
| for key in self.required_properties: | ||
| if not properties.get(key): | ||
| raise ValueError(f"Missing vended credential property: {key}") |
There was a problem hiding this comment.
This is being called on every credential, even if refresh is off.
It doesn't appear that this happens in Java. Even if it did, this would potentially break users who are using credentials currently with refresh off. We should make sure that this only works for refresh, since the property acts as a feature flag for us automatically.
| now_ms = int(time() * 1000) | ||
| states = _credential_states(response.storage_credentials, now_ms) | ||
| refreshed = _matching_state(states, location) | ||
| if refreshed is None: |
There was a problem hiding this comment.
If the refresh fails, this will immediately throw a ValueError.
Here's the rough idea of how Java handles this:
- Credential due for refresh, but hasn't expired yet
- Refreshing threw an error of some kind
- If the old credential is still valid, we should use it and not immediately surface the failed refresh to the user
In this situation, we should suppress the exception, log a warning, and use the old credential.
| Retained file handles resolve current credentials when opened or created again. An already-open native stream keeps its original credentials; | ||
| its lifetime should fit within the credential lifetime. External engines that manage their own storage clients must manage their own credential refresh. | ||
|
|
||
| FileIO instances remain pickleable with refresh enabled. Deserialization performs no network requests; catalog access and authentication |
There was a problem hiding this comment.
I'm not sure if we need to keep this last paragraph. This seems like a code behavior and not super useful for users.
| result.storage_credentials, | ||
| location, | ||
| identifier=self.table_identifier, | ||
| existing_io=self.io, |
There was a problem hiding this comment.
Can you plumb through the plan_id as well? We'll want that to eventually get over to the load_credentials endpoint
|
@rambleraptor Thanks very much! Your comments make sense and I pushed some commits to address the separately (for readability) |
Third time's a charm (prev PR closed due to staleness) :) Closes #3506
Rationale for this change
Implement automatic vended credential refresh in PyIceberg.
Are these changes tested?
Yes,
Are there any user-facing changes?
Yes, users will have the option to set the refresh credentials configurations in the catalog