Skip to content
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

Merged
merged 34 commits into from Dec 18, 2023

Conversation

clundin25
Copy link
Contributor

No description provided.

@clundin25 clundin25 marked this pull request as ready for review September 12, 2023 17:36
@clundin25 clundin25 requested review from a team as code owners September 12, 2023 17:36
if not self.expiry:
return TokenState.FRESH

refresh_window = _helpers.utcnow() >= (self.expiry - _helpers.REFRESH_THRESHOLD)
Copy link
Contributor

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?

else:
self.refresh(request)

if self.token_state == TokenState.INVALID:
self.refresh(request)
Copy link
Contributor

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?

Copy link
Contributor Author

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

@clundin25 clundin25 added the owlbot:run Add this label to trigger the Owlbot post processor. label Nov 2, 2023
@gcf-owl-bot gcf-owl-bot bot removed the owlbot:run Add this label to trigger the Owlbot post processor. label Nov 2, 2023
@clundin25 clundin25 added the owlbot:run Add this label to trigger the Owlbot post processor. label Nov 10, 2023
@gcf-owl-bot gcf-owl-bot bot removed the owlbot:run Add this label to trigger the Owlbot post processor. label Nov 10, 2023
google/auth/credentials.py Show resolved Hide resolved
google/auth/credentials.py Show resolved Hide resolved
@@ -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:

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

google/auth/credentials.py Outdated Show resolved Hide resolved
@@ -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

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?

Copy link
Contributor Author

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 Show resolved Hide resolved
google/auth/_refresh_worker.py Show resolved Hide resolved
# 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)

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/_refresh_worker.py Outdated Show resolved Hide resolved
google/auth/credentials.py Outdated Show resolved Hide resolved
google/auth/_refresh_worker.py Outdated Show resolved Hide resolved
@clundin25 clundin25 added the owlbot:run Add this label to trigger the Owlbot post processor. label Dec 14, 2023
@gcf-owl-bot gcf-owl-bot bot removed the owlbot:run Add this label to trigger the Owlbot post processor. label Dec 14, 2023
def with_non_blocking_refresh(self):
self._use_non_blocking_refresh = True

def get_background_refresh_error(self):

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

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.

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.

# rety this error.
err, self._worker._error_info = self._worker._error_info, None

raise e.RefreshError(

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().

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good!

self._worker = RefreshThread(cred=cred, request=request)
self._worker.start()

def has_error(self):

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay that sounds good!

self.refresh(request)

def _non_blocking_refresh(self, request):
if (

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()

Copy link
Contributor Author

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):

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?

Copy link
Contributor Author

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.

google/auth/_refresh_worker.py Outdated Show resolved Hide resolved
google/auth/credentials.py Show resolved Hide resolved
@clundin25 clundin25 added the owlbot:run Add this label to trigger the Owlbot post processor. label Dec 18, 2023
@gcf-owl-bot gcf-owl-bot bot removed the owlbot:run Add this label to trigger the Owlbot post processor. label Dec 18, 2023
@clundin25 clundin25 added the owlbot:run Add this label to trigger the Owlbot post processor. label Dec 18, 2023
@gcf-owl-bot gcf-owl-bot bot removed the owlbot:run Add this label to trigger the Owlbot post processor. label Dec 18, 2023
@clundin25 clundin25 merged commit a6dc2c3 into googleapis:main Dec 18, 2023
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants