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: Retry handling for Clients #90

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

santhoshramaraj
Copy link
Member

What does this Pull Request accomplish?

Exposes retry handling from Uplink and example to implement retry handling with clients.

Why should this Pull Request be merged?

To develop robust client applications, users must handle common scenarios such as Too Many Requests and https disconnections when using the SystemLink clients.

What testing has been done?

TBD

@santhoshramaraj santhoshramaraj added the enhancement New feature or request label Jan 13, 2025
@santhoshramaraj santhoshramaraj self-assigned this Jan 13, 2025
@santhoshramaraj santhoshramaraj marked this pull request as ready for review January 13, 2025 04:38
_client = FileClient()

# Wrap the client with retry and define the conditions to retry
client: FileClient = retry(
Copy link
Contributor

Choose a reason for hiding this comment

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

While this approach will work, its pretty kludgy to invoke the retry decorator in this way. I would prefer a new function on BaseClient like with_retry so your example would instead look like:

client = FileClient().with_retry(when= ...)
# OR because `retry` decorator modifies the class
client = FileClient()
client.with_retry(when=...)
T = TypeVar('T', bound='BaseClass')

class BaseClient(Consumer):
    ...
    def with_retry(
        self: T, 
        when=None,
        ...
    ) -> T:
        return retry(
            when,
            ...
        )(self)

And ideally we'd type these retry parameters properly, even though uplink does not.

One thorn in the side for this approach is subsequent calls to with_retry would likely behave strangely, from my understanding of how uplink is handling these annotations. I believe you'd end up with multiple instances of the retry handler applied when the requests are ultimately built. This is a problem with your original approach, too, but its less likely to be a a problem since you're doing this at construction.

This duplicate handlers problem is annoying because it would make adding a default retry behavior that you could then override with with_retry not work properly, and I think having a default is the direction we should go in based on feedback from @ChrisStrykesAgain. If we are sure we will not use the @retry decorator and always use this new with_retry function we can simply keep hold on that retry object and update it like so

T = TypeVar('T', bound='BaseClass')

class BaseClient(Consumer):
    ...
    def with_retry(
        self: T, 
        when=None,
        ...
    ) -> T:
        retry = retry(
            when,
            ...
        )
        if self._retry is None:
            self._retry = retry
            # Bind to this client's methods
            retry(self)
        else:
            # replace the already bound retry annotations config
            self._retry._when = retry._when
            self._retry._backoff = retry._backoff
            self._retry._stop = retry._stop
        
        return self

Or we can punt on subsequent updates until we also add default retries and for now just throw if you try to set the retry config more than once to prevent unexpected behavior. Making a new client with a different retry is pretty trivial after all.

from nisystemlink.clients.file import FileClient

# Retry on Too Many Requests status or any Exception
WHEN_CLAUSE = retry.when.status(429) | retry.when.raises(Exception)
Copy link
Contributor

Choose a reason for hiding this comment

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

As I said in my other comment, we'd like to have a default pretty good retry config that is useful in most cases that is simple to add and can be updated as we find cases to include. Could you take a crack at making one?

Retrying 502 and 503 would be good, and 429 as you have. I'm not sure what Exception cases you're trying to retry, did you have something in mind? I would start without that. We definitely want to stop after a number of retries, but I think we can leave the delay off and just rely on the count of retries plus backoff to get to the time we want.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants