diff options
Diffstat (limited to 'pymemcache/client/retrying.py')
-rw-r--r-- | pymemcache/client/retrying.py | 185 |
1 files changed, 185 insertions, 0 deletions
diff --git a/pymemcache/client/retrying.py b/pymemcache/client/retrying.py new file mode 100644 index 0000000..8c8f3a9 --- /dev/null +++ b/pymemcache/client/retrying.py @@ -0,0 +1,185 @@ +""" 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: 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) |