import six
import time


def ensure_redis_call(f, *args, **kwargs):
    """
    Helper for executing any callable with retry-logic for when
    redis is timing out or is experiencing connection errors
    while redis sentinel failover is in progress.

    The retries are attempted in exponential ease-off (1, 2, 4, ... sec).

    .. note::
        This helper is a blocking function. It waits between retries
        in a blocking fashion.

    .. note::
        In between reties this function prints a helpful error message.
        The reason why ``print`` is used rather then lets say a logger
        is because celery has a configuration at what level ``stdout``
        should be logged as - ``CELERY_REDIRECT_STDOUTS_LEVEL``

    Parameters
    ----------
    f : callable
        The callable to be executed
    attempts : int, optional
        Number of attempts to make with exponential ease-off.
        By default ``5`` is used which means the the wait time
        before last retry will be 16 seconds. Also in that case
        total wait time is 31 seconds.
    args : tuple
        Any arguments to be passed to ``f`` when calling it
    kwargs : dict
        Any keyword arguments to be passed to ``f`` when calling it
    """
    attempts = kwargs.pop('attempts', 5)

    for i in six.moves.range(attempts + 1):
        try:
            return f(*args, **kwargs)

        except (ConnectionError, TimeoutError) as e:
            if i == attempts:
                raise
            else:
                wait = 2 ** i
                msg = (
                    'Will reattempt to execute {} with args={} kwargs={} '
                    'after {} seconds due to exception {}: {}'
                    ''.format(f, args, kwargs, wait, type(e).__name__, e)
                )
                print(msg)
                time.sleep(wait)