retrying.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. """ Module containing the RetryingClient wrapper class. """
  2. from time import sleep
  3. def _ensure_tuple_argument(argument_name, argument_value):
  4. """
  5. Helper function to ensure the given arguments are tuples of Exceptions (or
  6. subclasses), or can at least be converted to such.
  7. Args:
  8. argument_name: str, name of the argument we're checking, only used for
  9. raising meaningful exceptions.
  10. argument_value: any, the argument itself.
  11. Returns:
  12. tuple[Exception]: A tuple with the elements from the argument if they are
  13. valid.
  14. Exceptions:
  15. ValueError: If the argument was not None, tuple or Iterable.
  16. ValueError: If any of the elements of the argument is not a subclass of
  17. Exception.
  18. """
  19. # Ensure the argument is a tuple, set or list.
  20. if argument_value is None:
  21. return tuple()
  22. elif not isinstance(argument_value, (tuple, set, list)):
  23. raise ValueError(
  24. "%s must be either a tuple, a set or a list." % argument_name
  25. )
  26. # Convert the argument before checking contents.
  27. argument_tuple = tuple(argument_value)
  28. # Check that all the elements are actually inherited from Exception.
  29. # (Catchable)
  30. if not all([issubclass(arg, Exception) for arg in argument_tuple]):
  31. raise ValueError(
  32. "%s is only allowed to contain elements that are subclasses of "
  33. "Exception." % argument_name
  34. )
  35. return argument_tuple
  36. class RetryingClient(object):
  37. """
  38. Client that allows retrying calls for the other clients.
  39. """
  40. def __init__(
  41. self,
  42. client,
  43. attempts=2,
  44. retry_delay=0,
  45. retry_for=None,
  46. do_not_retry_for=None
  47. ):
  48. """
  49. Constructor for RetryingClient.
  50. Args:
  51. client: Client|PooledClient|HashClient, inner client to use for
  52. performing actual work.
  53. attempts: optional int, how many times to attempt an action before
  54. failing. Must be 1 or above. Defaults to 2.
  55. retry_delay: optional int|float, how many seconds to sleep between
  56. each attempt.
  57. Defaults to 0.
  58. retry_for: optional None|tuple|set|list, what exceptions to
  59. allow retries for. Will allow retries for all exceptions if None.
  60. Example:
  61. `(MemcacheClientError, MemcacheUnexpectedCloseError)`
  62. Accepts any class that is a subclass of Exception.
  63. Defaults to None.
  64. do_not_retry_for: optional None|tuple|set|list, what
  65. exceptions should be retried. Will not block retries for any
  66. Exception if None.
  67. Example:
  68. `(IOError, MemcacheIllegalInputError)`
  69. Accepts any class that is a subclass of Exception.
  70. Defaults to None.
  71. Exceptions:
  72. ValueError: If `attempts` is not 1 or above.
  73. ValueError: If `retry_for` or `do_not_retry_for` is not None, tuple or
  74. Iterable.
  75. ValueError: If any of the elements of `retry_for` or
  76. `do_not_retry_for` is not a subclass of Exception.
  77. ValueError: If there is any overlap between `retry_for` and
  78. `do_not_retry_for`.
  79. """
  80. if attempts < 1:
  81. raise ValueError(
  82. "`attempts` argument must be at least 1. "
  83. "Otherwise no attempts are made."
  84. )
  85. self._client = client
  86. self._attempts = attempts
  87. self._retry_delay = retry_delay
  88. self._retry_for = _ensure_tuple_argument("retry_for", retry_for)
  89. self._do_not_retry_for = _ensure_tuple_argument(
  90. "do_not_retry_for", do_not_retry_for
  91. )
  92. # Verify no overlap in the go/no-go exception collections.
  93. for exc_class in self._retry_for:
  94. if exc_class in self._do_not_retry_for:
  95. raise ValueError(
  96. "Exception class \"%s\" was present in both `retry_for` "
  97. "and `do_not_retry_for`. Any exception class is only "
  98. "allowed in a single argument." % repr(exc_class)
  99. )
  100. # Take dir from the client to speed up future checks.
  101. self._client_dir = dir(self._client)
  102. def _retry(self, name, func, *args, **kwargs):
  103. """
  104. Workhorse function, handles retry logic.
  105. Args:
  106. name: str, Name of the function called.
  107. func: callable, the function to retry.
  108. *args: args, array arguments to pass to the function.
  109. **kwargs: kwargs, keyword arguments to pass to the function.
  110. """
  111. for attempt in range(self._attempts):
  112. try:
  113. result = func(*args, **kwargs)
  114. return result
  115. except Exception as exc:
  116. # Raise the exception to caller if either is met:
  117. # - We've used the last attempt.
  118. # - self._retry_for is set, and we do not match.
  119. # - self._do_not_retry_for is set, and we do match.
  120. # - name is not actually a member of the client class.
  121. if attempt >= self._attempts - 1 \
  122. or (self._retry_for
  123. and not isinstance(exc, self._retry_for)) \
  124. or (self._do_not_retry_for
  125. and isinstance(exc, self._do_not_retry_for)) \
  126. or name not in self._client_dir:
  127. raise exc
  128. # Sleep and try again.
  129. sleep(self._retry_delay)
  130. # This is the real magic soup of the class, we catch anything that isn't
  131. # strictly defined for ourselves and pass it on to whatever client we've
  132. # been given.
  133. def __getattr__(self, name):
  134. return lambda *args, **kwargs: self._retry(
  135. name,
  136. self._client.__getattribute__(name),
  137. *args,
  138. **kwargs
  139. )
  140. # We implement these explicitly because they're "magic" functions and won't
  141. # get passed on by __getattr__.
  142. def __dir__(self):
  143. return self._client_dir
  144. # These magics are copied from the base client.
  145. def __setitem__(self, key, value):
  146. self.set(key, value, noreply=True)
  147. def __getitem__(self, key):
  148. value = self.get(key)
  149. if value is None:
  150. raise KeyError
  151. return value
  152. def __delitem__(self, key):
  153. self.delete(key, noreply=True)