summaryrefslogtreecommitdiff
path: root/pymemcache/client/retrying.py
blob: a3312039c513540f7ff96d3476e715eab245abff (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
""" 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)