123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185 |
- """ Module containing the RetryingClient wrapper class. """
- from time import sleep
- def _ensure_tuple_argument(argument_name, argument_value):
- """
- Helper function to ensure the given arguments are tuples of Exceptions (or
- subclasses), or can at least be converted to such.
- Args:
- argument_name: str, name of the argument we're checking, only used for
- raising meaningful exceptions.
- argument_value: any, the argument itself.
- Returns:
- tuple[Exception]: A tuple with the elements from the argument if they are
- valid.
- Exceptions:
- ValueError: If the argument was not None, tuple or Iterable.
- ValueError: If any of the elements of the argument is not a subclass of
- Exception.
- """
- # Ensure the argument is a tuple, set or list.
- if argument_value is None:
- return tuple()
- elif not isinstance(argument_value, (tuple, set, list)):
- raise ValueError(
- "%s must be either a tuple, a set or a list." % argument_name
- )
- # Convert the argument before checking contents.
- argument_tuple = tuple(argument_value)
- # Check that all the elements are actually inherited from Exception.
- # (Catchable)
- if not all([issubclass(arg, Exception) for arg in argument_tuple]):
- raise ValueError(
- "%s is only allowed to contain elements that are subclasses of "
- "Exception." % argument_name
- )
- return argument_tuple
- class RetryingClient(object):
- """
- Client that allows retrying calls for the other clients.
- """
- def __init__(
- self,
- client,
- attempts=2,
- retry_delay=0,
- retry_for=None,
- do_not_retry_for=None
- ):
- """
- Constructor for RetryingClient.
- Args:
- client: Client|PooledClient|HashClient, inner client to use for
- performing actual work.
- attempts: optional int, how many times to attempt an action before
- failing. Must be 1 or above. Defaults to 2.
- retry_delay: optional int|float, how many seconds to sleep between
- each attempt.
- Defaults to 0.
- retry_for: optional None|tuple|set|list, what exceptions to
- allow retries for. Will allow retries for all exceptions if None.
- Example:
- `(MemcacheClientError, MemcacheUnexpectedCloseError)`
- Accepts any class that is a subclass of Exception.
- Defaults to None.
- do_not_retry_for: optional None|tuple|set|list, what
- exceptions should be retried. Will not block retries for any
- Exception if None.
- Example:
- `(IOError, MemcacheIllegalInputError)`
- Accepts any class that is a subclass of Exception.
- Defaults to None.
- Exceptions:
- ValueError: If `attempts` is not 1 or above.
- ValueError: If `retry_for` or `do_not_retry_for` is not None, tuple or
- Iterable.
- ValueError: If any of the elements of `retry_for` or
- `do_not_retry_for` is not a subclass of Exception.
- ValueError: If there is any overlap between `retry_for` and
- `do_not_retry_for`.
- """
- if attempts < 1:
- raise ValueError(
- "`attempts` argument must be at least 1. "
- "Otherwise no attempts are made."
- )
- self._client = client
- self._attempts = attempts
- self._retry_delay = retry_delay
- self._retry_for = _ensure_tuple_argument("retry_for", retry_for)
- self._do_not_retry_for = _ensure_tuple_argument(
- "do_not_retry_for", do_not_retry_for
- )
- # Verify no overlap in the go/no-go exception collections.
- for exc_class in self._retry_for:
- if exc_class in self._do_not_retry_for:
- raise ValueError(
- "Exception class \"%s\" was present in both `retry_for` "
- "and `do_not_retry_for`. Any exception class is only "
- "allowed in a single argument." % repr(exc_class)
- )
- # Take dir from the client to speed up future checks.
- self._client_dir = dir(self._client)
- def _retry(self, name, func, *args, **kwargs):
- """
- Workhorse function, handles retry logic.
- Args:
- name: str, Name of the function called.
- func: callable, the function to retry.
- *args: args, array arguments to pass to the function.
- **kwargs: kwargs, keyword arguments to pass to the function.
- """
- for attempt in range(self._attempts):
- try:
- result = func(*args, **kwargs)
- return result
- except Exception as exc:
- # Raise the exception to caller if either is met:
- # - We've used the last attempt.
- # - self._retry_for is set, and we do not match.
- # - self._do_not_retry_for is set, and we do match.
- # - name is not actually a member of the client class.
- if attempt >= self._attempts - 1 \
- or (self._retry_for
- and not isinstance(exc, self._retry_for)) \
- or (self._do_not_retry_for
- and isinstance(exc, self._do_not_retry_for)) \
- or name not in self._client_dir:
- raise exc
- # Sleep and try again.
- sleep(self._retry_delay)
- # This is the real magic soup of the class, we catch anything that isn't
- # strictly defined for ourselves and pass it on to whatever client we've
- # been given.
- def __getattr__(self, name):
- return lambda *args, **kwargs: self._retry(
- name,
- self._client.__getattribute__(name),
- *args,
- **kwargs
- )
- # We implement these explicitly because they're "magic" functions and won't
- # get passed on by __getattr__.
- def __dir__(self):
- return self._client_dir
- # These magics are copied from the base client.
- def __setitem__(self, key, value):
- self.set(key, value, noreply=True)
- def __getitem__(self, key):
- value = self.get(key)
- if value is None:
- raise KeyError
- return value
- def __delitem__(self, key):
- self.delete(key, noreply=True)
|