-
Notifications
You must be signed in to change notification settings - Fork 1
/
retry.py
116 lines (94 loc) · 3.55 KB
/
retry.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
import logging
from functools import partial
log = logging.getLogger(__name__)
class TryAgain(Exception):
"""A function encountered an error, and should be called again."""
class GaveUp(Exception):
"""A function encountered multiple errors, and will not be retried."""
def call_with_retries(func=None, expect=TryAgain, fail=GaveUp, max_attempts=3):
"""Call a function until it succeeds.
If the function returns a value, the value is returned.
If it raises an exception of type ``expect``, the exception is
caught and the call is retried.
If it raises a different exception, the exception is not caught.
If the function is called ``max_attempts`` times without returning
a value, ``fail`` is called and the result is raised.
This function may be used as a decorator, in order to specify a
block of code which should be retried. In that case, the decorated
function's name will be bound to its return value in the defining
scope, and the function object will be destroyed. (Consider naming
the decorated function ``_`` if its return value is not used.)
Parameters
----------
func : callable, optional
A function to call, which may raise an exception of type
``expect`` if the call should be retried.
expect : type or Tuple[type], optional
Exception type (or types) which should be caught, and indicate
that the function should be called again.
max_attempts : int, optional
The number of times to call ``func`` before giving up.
fail : callable, optional
Called and raised if the function repeatedly fails.
"""
if max_attempts < 1:
raise ValueError(max_attempts)
# Allow this function to be used as a decorator.
if func is None:
return partial(
call_with_retries,
expect=expect,
max_attempts=max_attempts,
)
for attempt in range(1, max_attempts + 1):
log.info("Beginning attempt %s of %s", attempt, max_attempts)
try:
result = func()
except expect:
if attempt == max_attempts:
retry_status = "giving up"
else:
retry_status = "will try again"
log.info(
"Encountered expected exception; %s", retry_status,
exc_info=True,
)
continue
except Exception:
log.exception("Encountered unexpected exception; halting retries")
raise
else:
log.info("Attempt %s succeeded", attempt)
return result
log.error("Gave up after %s attempt(s)", max_attempts)
raise fail()
if __name__ == '__main__':
import sys
_, max_attempts, *instructions = sys.argv
max_attempts = int(max_attempts)
instructions = iter(instructions)
logging.basicConfig(
format=' '.join([
# '[%(asctime)s]',
'%(levelname)s',
'%(pathname)s:%(lineno)d',
'(%(funcName)s)',
'%(message)s',
]),
datefmt='%Y-%m-%dT%H:%M:%S',
level='DEBUG',
)
try:
@print
@call_with_retries(max_attempts=max_attempts)
def process_next_instruction():
instruction = next(instructions)
log.info("Processing instruction %r", instruction)
if instruction == 'retry':
raise TryAgain
elif instruction == 'crash':
raise Exception("oh no")
else:
return instruction
except Exception:
print("RIP")