tenacity icon indicating copy to clipboard operation
tenacity copied to clipboard

Retries count

Open naphta opened this issue 7 years ago • 11 comments

Is it possible to access the number of retries which have occurred?

naphta avatar Jul 05 '17 14:07 naphta

Each tenacity.Retrying has a statistics attribute that you can use to access the number of retries.

I'll let this bug open to make sure its documened somewhere.

jd avatar Jul 05 '17 16:07 jd

Something related which might be helpful for people looking at this; We wanted to have access to the statistics during the execution, and used a closure for that. Skeleton code for it (which for example uses a statistic to put into a header of a request) looks like this:

def doRequest(url):
    # Closure structure to access retry statistics
    @retry(reraise=True)
    def doRequest_closure(url):
        try:
            headers.update({"AttemptNumber": str(doRequest_closure.retry.statistics["attempt_number"])})
        except:
            pass
        return r = requests.post(url, headers=headers)
    return doRequest_closure(url)

mhindery avatar May 18 '18 14:05 mhindery

Feels like wrapping it with another decorator might be a pretty way of doing it.

e.g.

@tenacity.retry
@tenacity.pass_statistics
def foo(statistics, value):
    print(f'Attempt #{statistics.attempt_number}: {value}'

naphta avatar May 18 '18 14:05 naphta

@mhindery I don't think you need a closure. You can just reference the function directly:

>>> import tenacity
>>> @tenacity.retry
... def x():
...     print(x.retry.statistics)
...
>>> x()
{'start_time': 743610.875068273, 'attempt_number': 1, 'idle_for': 0}

jd avatar May 21 '18 14:05 jd

All of the above one's are correct in their own cases :) I think a decorator could be useful, as this functionality of accessing statistics during execution looks like something not that uncommon to me.

@jd this indeed works if you have a function, but not in the case we used it in (a static method on a class). :

class Utils(object):
    # works
    @staticmethod
    def doRequest():
        # Closure structure to access retry statistics
        @tenacity.retry(reraise=True)
        def doRequest_closure():
            print(doRequest_closure.retry.statistics)
        return doRequest_closure()

    # doesn't work
    @staticmethod
    @tenacity.retry(reraise=True, stop=tenacity.stop_after_attempt(1))
    def doRequest2():
        print(doRequest2.retry.statistics)

The doesn't work case raises this error:

<ipython-input-8-a41ae46bbd83> in doRequest2()
     13     @tenacity.retry(reraise=True, stop=tenacity.stop_after_attempt(1))
     14     def doRequest2():
---> 15         print(doRequest2.retry.statistics)

NameError: global name 'doRequest2' is not defined

mhindery avatar May 23 '18 08:05 mhindery

You should define it as a classmethod or a method, not a staticmethod. It calls itself inside a class, so it's not static.

jd avatar May 23 '18 12:05 jd

I don't mind putting in a small amount of time to support a @tenacity.pass_statistics for what it's worth.

naphta avatar May 23 '18 14:05 naphta

@naphta that's not needed, see the discussion.

jd avatar May 23 '18 14:05 jd

Is it possible to access the number of retries which have occurred?

If you want the overall retry count for all tasks running an f function, you should use f.retry.statistics["attempt_number"] - active_task_count, where this non-local active_task_count should be increased only at the first attempt of each task (which might be quite hard to do).

If you want the retry count for a single task, I'm afraid it's not possible with the retry.statistics, unless you're not calling the same function more than once, since its context is lexical, and the retry count in this case is part of a dynamic context. If you have several tasks running the same decorated function, the statistics is mixed.

This example shows this behavior and includes a solution for the second problem using contextvars:

import asyncio, contextvars, datetime, random, tenacity
attempt = contextvars.ContextVar("attempt")

@tenacity.retry
async def f(n):
    attempt.set(attempt.get(0) + 1)
    values = [n, datetime.datetime.utcnow(), attempt.get(),
              f.retry.statistics["attempt_number"]]
    print(" ".join(map(str, values)))
    await asyncio.sleep(random.random())
    raise tenacity.TryAgain

async def call():
    for n in range(5):
        asyncio.ensure_future(f(n))

asyncio.run(call())

The result:

0 2019-08-08 20:39:30.991814 1 1
1 2019-08-08 20:39:30.992007 1 1
2 2019-08-08 20:39:30.992135 1 1
3 2019-08-08 20:39:30.992247 1 1
4 2019-08-08 20:39:30.992353 1 1
4 2019-08-08 20:39:30.993940 2 6
1 2019-08-08 20:39:30.994081 2 6
0 2019-08-08 20:39:30.994182 2 6
3 2019-08-08 20:39:30.994273 2 6
2 2019-08-08 20:39:30.994358 2 6
1 2019-08-08 20:39:31.074111 3 7
2 2019-08-08 20:39:31.128905 3 8
3 2019-08-08 20:39:31.206795 3 9
2 2019-08-08 20:39:31.284957 4 10
0 2019-08-08 20:39:31.289798 3 11
4 2019-08-08 20:39:31.516829 3 12
1 2019-08-08 20:39:31.652761 4 13
0 2019-08-08 20:39:31.713638 4 14
1 2019-08-08 20:39:31.772541 5 15
3 2019-08-08 20:39:31.841446 4 16
1 2019-08-08 20:39:32.051496 6 17
4 2019-08-08 20:39:32.062492 4 18
2 2019-08-08 20:39:32.174466 5 19
3 2019-08-08 20:39:32.236457 5 20
1 2019-08-08 20:39:32.303372 7 21
0 2019-08-08 20:39:32.323257 5 22
4 2019-08-08 20:39:32.607363 5 23
4 2019-08-08 20:39:32.632185 6 24
3 2019-08-08 20:39:32.810179 6 25
1 2019-08-08 20:39:32.930142 8 26
2 2019-08-08 20:39:33.020101 6 27
3 2019-08-08 20:39:33.045923 7 28
1 2019-08-08 20:39:33.065823 9 29
4 2019-08-08 20:39:33.120670 7 30
2 2019-08-08 20:39:33.143518 7 31
0 2019-08-08 20:39:33.270527 6 32
0 2019-08-08 20:39:33.440524 7 33
4 2019-08-08 20:39:33.497371 8 34
0 2019-08-08 20:39:33.543119 8 35
0 2019-08-08 20:39:33.692038 9 36
3 2019-08-08 20:39:33.778935 8 37
0 2019-08-08 20:39:33.793688 10 38
0 2019-08-08 20:39:33.925558 11 39
2 2019-08-08 20:39:33.956431 8 40
1 2019-08-08 20:39:34.039304 10 41
4 2019-08-08 20:39:34.475522 9 42
1 2019-08-08 20:39:34.536371 11 43
0 2019-08-08 20:39:34.696280 12 44
2 2019-08-08 20:39:34.750217 9 45
3 2019-08-08 20:39:34.754077 9 46
1 2019-08-08 20:39:34.921033 12 47
4 2019-08-08 20:39:35.233154 10 48
0 2019-08-08 20:39:35.268114 13 49
2 2019-08-08 20:39:35.517181 10 50
3 2019-08-08 20:39:35.529995 10 51
1 2019-08-08 20:39:35.534860 13 52
0 2019-08-08 20:39:35.671832 14 53
4 2019-08-08 20:39:35.922999 11 54
1 2019-08-08 20:39:36.212128 14 55
4 2019-08-08 20:39:36.289996 12 56
3 2019-08-08 20:39:36.305002 11 57
2 2019-08-08 20:39:36.358790 11 58
0 2019-08-08 20:39:36.472697 15 59
1 2019-08-08 20:39:36.489496 15 60
1 2019-08-08 20:39:36.558043 16 61
3 2019-08-08 20:39:36.678750 12 62
4 2019-08-08 20:39:36.728588 13 63
2 2019-08-08 20:39:37.223850 12 64
2 2019-08-08 20:39:37.240736 13 65
0 2019-08-08 20:39:37.251575 16 66
4 2019-08-08 20:39:37.400507 14 67
2 2019-08-08 20:39:37.428611 14 69
1 2019-08-08 20:39:37.428857 17 69
4 2019-08-08 20:39:37.526645 15 70
3 2019-08-08 20:39:37.557577 13 71
1 2019-08-08 20:39:37.683582 18 72
1 2019-08-08 20:39:37.815516 19 73
0 2019-08-08 20:39:37.939429 17 74
2 2019-08-08 20:39:37.962317 15 75
2 2019-08-08 20:39:38.092201 16 76
4 2019-08-08 20:39:38.141038 16 77

If you sort these lines, you'll see the attempt context variable is the attempt number for that dynamic context. The number of retries is just attempt.get() - 1.

For the first approach (the retry count summing retries from all tasks), you'll need to increase the global active_task_count when attempt.get() == 1.

danilobellini avatar Aug 08 '19 22:08 danilobellini

@mhindery I don't think you need a closure. You can just reference the function directly:

>>> import tenacity
>>> @tenacity.retry
... def x():
...     print(x.retry.statistics)
...
>>> x()
{'start_time': 743610.875068273, 'attempt_number': 1, 'idle_for': 0}

@jd but in this case the function x needs to access to attributes defined outside of their own scope. But what happens in the case that you cant modify the original function and add just the retry decorator? I think in that cases could be nice to have or another decorator, or maybe there is a way to use a custom callback for tihs?

nueces avatar Feb 22 '22 03:02 nueces

With respect to global vs local retry stats, retry.statistics is local like you'd expect. Perhaps not for async functions, but I suspect most are not using async.

from tenacity import retry, stop_after_attempt

@retry(stop=stop_after_attempt(3))
def foo():
    attempt = foo.retry.statistics['attempt_number']
    print(f"Attempt {attempt}...")
    if attempt < 3:
        raise Exception("Failed")

foo()
foo()

prints

Attempt 1...
Attempt 2...
Attempt 3...
Attempt 1...
Attempt 2...
Attempt 3...

crizCraig avatar Aug 11 '23 03:08 crizCraig