Source code for gigl.common.utils.retry

import threading
import time
from functools import wraps
from typing import Callable, Optional, Tuple, Type, TypeVar, Union
from xmlrpc.client import Boolean

from gigl.common.logger import Logger
from gigl.src.common.utils.timeout import TimedOutException, timeout

[docs] T = TypeVar("T") # return type
[docs] logger = Logger()
[docs] class RetriesFailedException(Exception): pass
class __RetriableTimeoutException(Exception): pass
[docs] def retry( exception_to_check: Union[Type, Tuple[Type, ...]] = Exception, tries: int = 5, delay_s: int = 3, backoff: int = 2, max_delay_s: Optional[int] = None, fn_execution_timeout_s: Optional[int] = None, deadline_s: Optional[int] = None, should_throw_original_exception: Boolean = False, ) -> Callable[[Callable[..., T]], Callable[..., T]]: """ Decorator that can be added around a function to retry incase it fails i.e. throws some exceptions Args: exception_to_check (Union[Type, Tuple[Type, ...]]): the exception to check. Could also be a tuple of exceptions to check. Defaults to Exception. i.e. catches everything tries (Optional[int]): [description]. number of times to try (not retry) before giving up. Defaults to 5. delay_s (Optional[int]): [description]. initial delay between retries in seconds. Defaults to 3. backoff (Optional[int]): [description]. backoff multiplier e.g. value of 2 will double the delay each retry. Defaults to 2. max_delay_s (Optional[int]): [description]. maximum delay between retries in seconds. Defaults to None. fn_execution_timeout_s (Optional[int]): Maximum time given before a single function execution should time out. Defaults to None. deadline_s (Optional[int]): [description]. Total time in seconds to spend retrying, fails if exceeds this time. Note this timeout can also stop the first execution, so ensure to provide a lot of extra room so retries can actually take place. If timeout occurs, src.common.utils.timeout.TimedOutException is raised. Defaults to None. Only applicable if run in a main thread. Raise RuntimeError if not in main thread. should_throw_original_exception (Optional[bool]): Defaults to False. """ def deco_retry(f) -> Callable[..., T]: @wraps(f) def f_retry(*args, **kwargs) -> T: # type: ignore[type-var] mtries, mdelay = tries, delay_s def fn(*args, **kwargs) -> T: if fn_execution_timeout_s is not None: timeout_individual_fn_call_decorator = timeout( seconds=fn_execution_timeout_s, exception_thrown_on_timeout=__RetriableTimeoutException, ) return timeout_individual_fn_call_decorator(f)(*args, **kwargs) return f(*args, **kwargs) acceptable_exceptions: Tuple[Type[Exception], ...] = ( # ty: ignore[invalid-assignment] exception_to_check if isinstance(exception_to_check, tuple) else (exception_to_check,) ) acceptable_exceptions = acceptable_exceptions + ( __RetriableTimeoutException, ) ret_val: T while mtries >= 0: try: ret_val = fn(*args, **kwargs) break except TimedOutException: raise except acceptable_exceptions as e: if mtries == 0: # Failed for the final time logger.exception(f"Failed for the last time: {e}") if should_throw_original_exception: raise # Reraise original exception raise RetriesFailedException( f"Retry failed, permanently failing {f.__module__}:{f.__name__}, see logs for {e}" ) msg = f"{e}, Retrying {f.__module__}:{f.__name__} in {mdelay} seconds..." logger.warning(msg) time.sleep(mdelay) mtries -= 1 mdelay = ( min(mdelay * backoff, max_delay_s) if max_delay_s else mdelay * backoff ) return ret_val if deadline_s is not None: timed_f_retry = timeout(seconds=deadline_s)(f_retry) @wraps(f) def f_retry_with_deadline(*args, **kwargs) -> T: if threading.current_thread() is not threading.main_thread(): raise RuntimeError( "retry(deadline_s=...) only works in the main thread." f"{f.__module__}:{f.__name__} was called from thread: " f"{threading.current_thread().name!r}." "This failure is because deadline_s uses SIGALRM, " "under the hood, which only works in the main thread." ) return timed_f_retry(*args, **kwargs) return f_retry_with_deadline return f_retry return deco_retry