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

retries not working in a generator context? #138

Open
apurvis opened this issue Aug 16, 2018 · 3 comments
Open

retries not working in a generator context? #138

apurvis opened this issue Aug 16, 2018 · 3 comments

Comments

@apurvis
Copy link

apurvis commented Aug 16, 2018

So let me preface this by saying that I tried to code up a minimal example that showed retries not happening, and I couldn't get it to not work... so I'm very confused.

Code that was not retrying basically looks like this:

def retry_logger(fxn, attempt_number, elapsed_seconds):
    print("Retrying {} (try #{}, {}s passed)".format(fxn.__name__, attempt_number, elapsed_seconds))

class Extractor(SomeSuperClass):
    RETRY_KWARGS = {
        'after': retry_logger,
        'reraise': True,
        'retry': retry_if_exception_type(RateLimitExceeded),
        'stop': stop_after_attempt(20),
        'wait': wait_exponential(multiplier=1, max=120)
    }

    @retry(**RETRY_KWARGS)
    def get_children(self, uri):
        for child in self.client.get_collection(uri):
            yield child

extractor = Extractor()
for c in extractor.get_children():
    do_something

client.get_collection is a function that is a generator, so there's sort of a nested generator thing going on, and it is from within that method that the RateLimitExceeded exception springs that i want to retry.

Is there any reason this would not work? This is the minimal example I coded up to try to show it not working, but it worked fine:

from tenacity import retry, stop_after_attempt, retry_if_exception_type, wait_exponential

def retry_logger(fxn, attempt_number, elapsed_seconds):
    print("Retrying {} (try #{}, {}s passed)".format(fxn.__name__, attempt_number, elapsed_seconds))

class RetryTester:
    RETRY_KWARGS = {
        'after': retry_logger,
        'reraise': False,
        'retry': retry_if_exception_type(RuntimeError),
        'stop': stop_after_attempt(20),
        'wait': wait_exponential(multiplier=2, max=120)
    }

    def generator_with_exception(self):
        for i in range(100):
            if i > 50:
                raise RuntimeError
            else:
                yield i

    def nested_generator(self):
        for i in self.generator_with_exception():
            for x in self.generator_with_exception():
                yield x

    @retry(**RETRY_KWARGS)
    def does_not_retry(self):
        print "Retry statistics: {}".format(self.does_not_retry.retry.statistics)
        for x in self.nested_generator():
            print x

RetryTester().does_not_retry()
@dargueta
Copy link

dargueta commented Nov 12, 2018

An important thing to note here: Technically, does_not_retry is a function that returns a generator, as you can see here:

>>> def foo():
...     for i in range(2):
...         yield i
... 
>>> type(foo)
<type 'function'>
>>> type(foo())
<type 'generator'>

In your second example, does_not_retry isn't a generator function, because it doesn't contain yield anywhere. Thus, tenacity restarts it just fine.

The way generator functions work is that Python turns does_not_retry into something that, when executed, returns an iterable that executes the code inside does_not_retry. Weird, I know, but humor me for a second. So now we have two objects (ish); the generator that handles executing code, and then the actual code itself (what's inside does_not_retry). I'll refer to the executing stuff you care about as the "user's code".

Now suppose an exception occurs the user's code somewhere. tenacity.retry() wrapped the generator function, not the generator itself (i.e. the user's code). Thus, the exception propagates up.

@bersace
Copy link
Contributor

bersace commented Mar 19, 2019

Is this issue duplicate with #64 ?

@dargueta
Copy link

dargueta commented Mar 19, 2019

Not a duplicate so much as a use case for #64.

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

No branches or pull requests

3 participants