New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Add optional non blocking refresh for sync auth code #1368
Conversation
6dffc03
to
b244471
Compare
google/auth/credentials.py
Outdated
if not self.expiry: | ||
return TokenState.FRESH | ||
|
||
refresh_window = _helpers.utcnow() >= (self.expiry - _helpers.REFRESH_THRESHOLD) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rename this variable to something like is_fresh?
google/auth/credentials.py
Outdated
else: | ||
self.refresh(request) | ||
|
||
if self.token_state == TokenState.INVALID: | ||
self.refresh(request) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Multiple requests may end up doing this refresh right?
Can we have a lock on this sync refresh?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code is using a bounded queue which should reduce duplicate work. There is a risk that there is a small amount of duplicate work, if two threads queue work at the same time. The queue itself is thread safe + has locks.
I believe using the queue to reduce duplicate work is acceptable, and that multiple threads will perform the refresh occasionally.
There should be no data corruption due to this
d99a734
to
aab77bb
Compare
google/auth/credentials.py
Outdated
@@ -171,11 +188,38 @@ def before_request(self, request, method, url, headers): | |||
# pylint: disable=unused-argument | |||
# (Subclasses may use these arguments to ascertain information about | |||
# the http request.) | |||
if not self.valid: | |||
|
|||
if self.token_state == TokenState.FRESH: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This section doesn't guard access to refresh worker resources with your feature flag.
Maybe something like:
if _use_non_blocking_refresh():
_non_blocking_refresh() # thread maintenance, stale handling, refresh on invalid
else
_blocking_refresh() # refresh on stale or invalid
@@ -259,7 +259,11 @@ def _update_token(self, request): | |||
""" | |||
|
|||
# Refresh our source credentials if it is not valid. | |||
if not self._source_credentials.valid: | |||
if ( | |||
not self._source_credentials.valid |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we remove this dependency and juse use TokenState.INVALID?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, I've made this change
google/auth/_refresh_worker.py
Outdated
# it can be flaky due to the scheduler. | ||
_LOGGER.error(f"Background refresh failed due to: {err}") | ||
if not self._error_queue.full(): | ||
self._error_queue.put_nowait(err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
with the code above, you can just stuff the error into a field.
google/auth/credentials.py
Outdated
def with_non_blocking_refresh(self): | ||
self._use_non_blocking_refresh = True | ||
|
||
def get_background_refresh_error(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I would prefer to remove this from the public API:
- it is not relevant without the feature flag
- it isn't clear when you'd want to call it
- it's hard to implement safely
google/auth/credentials.py
Outdated
class TokenState(Enum): | ||
""" | ||
Tracks the state of a token. | ||
FRESH: The token is valid. It is not expired or close to expired, or the token has no expiry. To make it mutually exclusive to STALE. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sorry about this: the "To make it mutually exclusive to STALE" part wasn't meant to be part of the comment.
google/auth/_refresh_worker.py
Outdated
# rety this error. | ||
err, self._worker._error_info = self._worker._error_info, None | ||
|
||
raise e.RefreshError( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this has a kind of weird 'every 2nd call fails' pattern that doesn't seem quite right.
I would suggest we try to background refresh once, log and record the error, then do foreground refreshes from then on until we get a new token.
You might return false
from start_refresh
to cause the caller to call refresh
on their own thread. The caller would be responsible for calling "clear error" any time they have a good token from refresh()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good!
google/auth/_refresh_worker.py
Outdated
self._worker = RefreshThread(cred=cred, request=request) | ||
self._worker.start() | ||
|
||
def has_error(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would remove this and get_error
from the public API since they are subject to the lock and difficult to reason about outside of this class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay that sounds good!
google/auth/credentials.py
Outdated
self.refresh(request) | ||
|
||
def _non_blocking_refresh(self, request): | ||
if ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
following the comment above, this would become:
if self.token_state == TokenState.FRESH:
return
need_refresh = True
if self.token_state == TokenState.STALE:
need_refresh = not self.refresh_worker.start_refresh(self, request)
if need_refresh:
self.refresh(request)
self._refresh_worker.clear_error()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done! I kept the INVALID
case, should a refresh was never called in the STALE state.
self._worker = None | ||
self._lock = threading.Lock() # protects access to worker threads. | ||
|
||
def start_refresh(self, cred, request): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is request
immutable or can we create a copy here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you expand on why copy it? I don't think it is necessary, as the request should not be re-used in other places.
I think copying the request would be a breaking change.
I'd have to do some work to understand the implication of deep copying a request object.
No description provided.