Skip to content

Commit d409b22

Browse files
authored
Merge pull request #11 from /issues/10-add-retry
Add `retry` from Toil (resolves #10)
2 parents b6691ac + df0e62b commit d409b22

1 file changed

Lines changed: 138 additions & 0 deletions

File tree

src/bd2k/util/retry.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
from __future__ import absolute_import
2+
3+
import time
4+
import urllib2
5+
from contextlib import contextmanager
6+
7+
import logging
8+
9+
log = logging.getLogger( __name__ )
10+
11+
12+
# noinspection PyUnusedLocal
13+
def never( exception ):
14+
return False
15+
16+
17+
def retry( delays=(0, 1, 1, 4, 16, 64), timeout=300, predicate=never ):
18+
"""
19+
Retry an operation while the failure matches a given predicate and until a given timeout
20+
expires, waiting a given amount of time in between attempts. This function is a generator
21+
that yields contextmanagers. See doctests below for example usage.
22+
23+
:param Iterable[float] delays: an interable yielding the time in seconds to wait before each
24+
retried attempt, the last element of the iterable will be repeated.
25+
26+
:param float timeout: a overall timeout that should not be exceeded for all attempts together.
27+
This is a best-effort mechanism only and it won't abort an ongoing attempt, even if the
28+
timeout expires during that attempt.
29+
30+
:param Callable[[Exception],bool] predicate: a unary callable returning True if another
31+
attempt should be made to recover from the given exception. The default value for this
32+
parameter will prevent any retries!
33+
34+
:return: a generator yielding context managers, one per attempt
35+
:rtype: Iterator
36+
37+
Retry for a limited amount of time:
38+
39+
>>> true = lambda _:True
40+
>>> false = lambda _:False
41+
>>> i = 0
42+
>>> for attempt in retry( delays=[0], timeout=.1, predicate=true ):
43+
... with attempt:
44+
... i += 1
45+
... raise RuntimeError('foo')
46+
Traceback (most recent call last):
47+
...
48+
RuntimeError: foo
49+
>>> i > 1
50+
True
51+
52+
If timeout is 0, do exactly one attempt:
53+
54+
>>> i = 0
55+
>>> for attempt in retry( timeout=0 ):
56+
... with attempt:
57+
... i += 1
58+
... raise RuntimeError( 'foo' )
59+
Traceback (most recent call last):
60+
...
61+
RuntimeError: foo
62+
>>> i
63+
1
64+
65+
Don't retry on success:
66+
67+
>>> i = 0
68+
>>> for attempt in retry( delays=[0], timeout=.1, predicate=true ):
69+
... with attempt:
70+
... i += 1
71+
>>> i
72+
1
73+
74+
Don't retry on unless predicate returns True:
75+
76+
>>> i = 0
77+
>>> for attempt in retry( delays=[0], timeout=.1, predicate=false):
78+
... with attempt:
79+
... i += 1
80+
... raise RuntimeError( 'foo' )
81+
Traceback (most recent call last):
82+
...
83+
RuntimeError: foo
84+
>>> i
85+
1
86+
"""
87+
if timeout > 0:
88+
go = [ None ]
89+
90+
@contextmanager
91+
def repeated_attempt( delay ):
92+
try:
93+
yield
94+
except Exception as e:
95+
if time.time( ) + delay < expiration and predicate( e ):
96+
log.info( 'Got %s, trying again in %is.', e, delay )
97+
time.sleep( delay )
98+
else:
99+
raise
100+
else:
101+
go.pop( )
102+
103+
delays = iter( delays )
104+
expiration = time.time( ) + timeout
105+
delay = next( delays )
106+
while go:
107+
yield repeated_attempt( delay )
108+
delay = next( delays, delay )
109+
else:
110+
@contextmanager
111+
def single_attempt( ):
112+
yield
113+
114+
yield single_attempt( )
115+
116+
117+
default_delays = (0, 1, 1, 4, 16, 64)
118+
default_timeout = 300
119+
120+
121+
def retryable_http_error( e ):
122+
return isinstance( e, urllib2.HTTPError ) and e.code in ('503', '408', '500')
123+
124+
125+
def retry_http( delays=default_delays, timeout=default_timeout, predicate=retryable_http_error ):
126+
"""
127+
>>> i = 0
128+
>>> for attempt in retry_http(timeout=5):
129+
... with attempt:
130+
... i += 1
131+
... raise urllib2.HTTPError('http://www.test.com', '408', 'some message', {}, None)
132+
Traceback (most recent call last):
133+
...
134+
HTTPError: HTTP Error 408: some message
135+
>>> i > 1
136+
True
137+
"""
138+
return retry( delays=delays, timeout=timeout, predicate=predicate )

0 commit comments

Comments
 (0)