__init__.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. import copy
  2. import datetime
  3. import functools
  4. import itertools
  5. import logging
  6. import random
  7. import time
  8. """
  9. Retry library provides an ability to retry function calls in a configurable way.
  10. To retry a certain function call use `retry_call` function. To make function auto-retriable use `retry`
  11. or `retry_intrusive` decorators. Both `retry_call` and `retry` optionally accept retry configuration object
  12. or its fields as kwargs. The `retry_intrusive` is designed for methods and uses intrusive configuration object.
  13. >>> retry_call(foo)
  14. >>> retry_call(foo, foo_args, foo_kwargs)
  15. >>> retry_call(foo, foo_args, foo_kwargs, conf=conf)
  16. >>> @retry()
  17. >>> def foo(...):
  18. >>> ...
  19. >>> @retry(conf)
  20. >>> def foo(...):
  21. >>> ...
  22. >>> class Obj(object):
  23. >>> def __init__(self):
  24. >>> self.retry_conf = conf
  25. >>>
  26. >>> @retry_intrusive
  27. >>> def foo(self, ...):
  28. >>> ...
  29. This library differs from its alternatives:
  30. * `retry` contrib library lacks time-based limits, reusable configuration objects and is generally less flexible
  31. * `retrying` contrib library is somewhat more complex, but also lacks reusable configuration objects
  32. """
  33. DEFAULT_SLEEP_FUNC = time.sleep
  34. LOGGER = logging.getLogger(__name__)
  35. class RetryConf(object):
  36. """
  37. Configuration object defines retry behaviour and is composed of these fields:
  38. * `retriable` - function that decides if an exception should trigger retrying
  39. * `get_delay` - function that returns a number of seconds retrier must wait before doing the next attempt
  40. * `max_time` - maximum `datetime.timedelta` that can pass after the first call for any retry attempt to be done
  41. * `max_times` - maximum number of retry attempts (note retries, not tries/calls)
  42. * `handle_error` - function that is called for each failed call attempt
  43. * `logger` - logger object to record retry warnings with
  44. * `sleep` - custom sleep function to use for waiting
  45. >>> RetryConf(max_time=datetime.timedelta(seconds=30), max_times=10)
  46. Empty configuration retries indefinitely on any exceptions raised.
  47. By default `DEFAULT_CONF` if used, which retries indefinitely, waiting 1 sec with 1.2 backoff between attempts, and
  48. also logging with built-in logger object.
  49. Configuration must be cloned before modification to create separate configuration:
  50. >>> DEFAULT_CONF.clone()
  51. There are various methods that provide convenient clone-and-modify shortcuts and "retry recipes".
  52. """
  53. _PROPS = {
  54. "retriable": lambda e: True,
  55. "get_delay": lambda n, raised_after, last: 0,
  56. "max_time": None,
  57. "max_times": None,
  58. "handle_error": None,
  59. "logger": None,
  60. "sleep": DEFAULT_SLEEP_FUNC,
  61. }
  62. def __init__(self, **kwargs):
  63. for prop, default_value in self._PROPS.items():
  64. setattr(self, prop, default_value)
  65. self._set(**kwargs)
  66. def __repr__(self):
  67. return repr(self.__dict__)
  68. def clone(self, **kwargs):
  69. """
  70. Clone configuration.
  71. """
  72. obj = copy.copy(self)
  73. obj._set(**kwargs)
  74. return obj
  75. def on(self, *errors):
  76. """
  77. Clone and retry on specific exception types (retriable shortcut):
  78. >>> conf = conf.on(MyException, MyOtherException)
  79. """
  80. obj = self.clone()
  81. obj.retriable = lambda e: isinstance(e, errors)
  82. return obj
  83. def waiting(self, delay=0, backoff=1.0, jitter=0, limit=None):
  84. """
  85. Clone and wait between attempts with backoff, jitter and limit (get_delay shortcut):
  86. >>> conf = conf.waiting(delay)
  87. >>> conf = conf.waiting(delay, backoff=2.0) # initial delay with backoff x2 on each attempt
  88. >>> conf = conf.waiting(delay, jitter=3) # jitter from 0 to 3 seconds
  89. >>> conf = conf.waiting(delay, backoff=2.0, limit=60) # delay with backoff, but not greater than a minute
  90. All these options can be combined together, of course.
  91. """
  92. def get_delay(n, raised_after, last):
  93. if n == 1:
  94. return delay
  95. s = last * backoff
  96. s += random.uniform(0, jitter)
  97. if limit is not None:
  98. s = min(s, limit)
  99. return s
  100. obj = self.clone()
  101. obj.get_delay = get_delay
  102. return obj
  103. def upto(self, seconds=0, **other_timedelta_kwargs):
  104. """
  105. Clone and do retry attempts only for some time (max_time shortcut):
  106. >>> conf = conf.upto(30) # retrying for 30 seconds
  107. >>> conf = conf.upto(hours=1, minutes=20) # retrying for 1:20
  108. Any `datetime.timedelta` kwargs can be used here.
  109. """
  110. obj = self.clone()
  111. obj.max_time = datetime.timedelta(seconds=seconds, **other_timedelta_kwargs)
  112. return obj
  113. def upto_retries(self, retries=0):
  114. """
  115. Set limit for retry attempts number (max_times shortcut):
  116. >>> conf = conf.upto_retries(10)
  117. """
  118. obj = self.clone()
  119. obj.max_times = retries
  120. return obj
  121. def _set(self, **kwargs):
  122. for prop, value in kwargs.items():
  123. if prop not in self._PROPS:
  124. continue
  125. setattr(self, prop, value)
  126. DEFAULT_CONF = RetryConf(logger=LOGGER).waiting(1, backoff=1.2)
  127. def retry_call(f, f_args=(), f_kwargs={}, conf=DEFAULT_CONF, **kwargs):
  128. """
  129. Retry function call.
  130. :param f: function to be retried
  131. :param f_args: target function args
  132. :param f_kwargs: target function kwargs
  133. :param conf: configuration
  134. """
  135. if kwargs:
  136. conf = conf.clone(**kwargs)
  137. return _retry(conf, functools.partial(f, *f_args, **f_kwargs))
  138. def retry(conf=DEFAULT_CONF, **kwargs):
  139. """
  140. Retrying decorator.
  141. :param conf: configuration
  142. """
  143. if kwargs:
  144. conf = conf.clone(**kwargs)
  145. def decorator(f):
  146. @functools.wraps(f)
  147. def wrapped(*f_args, **f_kwargs):
  148. return _retry(conf, functools.partial(f, *f_args, **f_kwargs))
  149. return wrapped
  150. return decorator
  151. def retry_intrusive(f):
  152. """
  153. Retrying method decorator that uses an intrusive conf (obj.retry_conf).
  154. """
  155. @functools.wraps(f)
  156. def wrapped(obj, *f_args, **f_kwargs):
  157. assert hasattr(obj, "retry_conf"), "Object must have retry_conf attribute for decorator to run"
  158. return _retry(obj.retry_conf, functools.partial(f, obj, *f_args, **f_kwargs))
  159. return wrapped
  160. def _retry(conf, f):
  161. start = datetime.datetime.now()
  162. delay = 0
  163. for n in itertools.count(1):
  164. try:
  165. return f()
  166. except Exception as error:
  167. raised_after = datetime.datetime.now() - start
  168. if conf.handle_error:
  169. conf.handle_error(error, n, raised_after)
  170. delay = conf.get_delay(n, raised_after, delay)
  171. retry_after = raised_after + datetime.timedelta(seconds=delay)
  172. retrying = (
  173. conf.retriable(error)
  174. and (conf.max_times is None or n <= conf.max_times)
  175. and (conf.max_time is None or retry_after <= conf.max_time)
  176. )
  177. if not retrying:
  178. raise
  179. if delay:
  180. conf.sleep(delay)
  181. if conf.logger:
  182. conf.logger.warning(
  183. "Retrying (try %d) after %s (%s + %s sec) on %s: %s",
  184. n,
  185. retry_after,
  186. raised_after,
  187. delay,
  188. error.__class__.__name__,
  189. error,
  190. exc_info=True,
  191. )