aboutsummaryrefslogtreecommitdiffhomepage
path: root/infra/retry.py
blob: 1f6d54b8d00d081b672a7cbb9f998b5b92c624ce (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
# Copyright 2020 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Retry decorator. Copied from ClusterFuzz source."""

import functools
import inspect
import logging
import sys
import time

# pylint: disable=too-many-arguments,broad-except


def sleep(seconds):
  """Invoke time.sleep. This is to avoid the flakiness of time.sleep. See:
    crbug.com/770375"""
  time.sleep(seconds)


def get_delay(num_try, delay, backoff):
  """Compute backoff delay."""
  return delay * (backoff**(num_try - 1))


def wrap(retries,
         delay,
         backoff=2,
         exception_type=Exception,
         retry_on_false=False):
  """Retry decorator for a function."""

  assert delay > 0
  assert backoff >= 1
  assert retries >= 0

  def decorator(func):
    """Decorator for the given function."""
    tries = retries + 1
    is_generator = inspect.isgeneratorfunction(func)
    function_with_type = func.__qualname__
    if is_generator:
      function_with_type += ' (generator)'

    def handle_retry(num_try, exception=None):
      """Handle retry."""
      if (exception is None or
          isinstance(exception, exception_type)) and num_try < tries:
        logging.info('Retrying on %s failed with %s. Retrying again.',
                     function_with_type,
                     sys.exc_info()[1])
        sleep(get_delay(num_try, delay, backoff))
        return True

      logging.error('Retrying on %s failed with %s. Raise.', function_with_type,
                    sys.exc_info()[1])
      return False

    @functools.wraps(func)
    def _wrapper(*args, **kwargs):
      """Regular function wrapper."""
      for num_try in range(1, tries + 1):
        try:
          result = func(*args, **kwargs)
          if retry_on_false and not result:
            if not handle_retry(num_try):
              return result

            continue
          return result
        except Exception as error:
          if not handle_retry(num_try, exception=error):
            raise

    @functools.wraps(func)
    def _generator_wrapper(*args, **kwargs):
      """Generator function wrapper."""
      # This argument is not applicable for generator functions.
      assert not retry_on_false
      already_yielded_element_count = 0
      for num_try in range(1, tries + 1):
        try:
          for index, result in enumerate(func(*args, **kwargs)):
            if index >= already_yielded_element_count:
              yield result
              already_yielded_element_count += 1
          break
        except Exception as error:
          if not handle_retry(num_try, exception=error):
            raise

    if is_generator:
      return _generator_wrapper
    return _wrapper

  return decorator