Creates a function equivalent to a_func, but that retries on certain
    exceptions.

    Args:
      a_func (callable): A callable.
      retry_options (RetryOptions): Configures the exceptions upon which the
        callable should retry, and the parameters to the exponential backoff
        retry algorithm.
      kwargs: Addtional arguments passed through to the callable.

    Returns:
        Callable: A function that will retry on exception.

def retryable(a_func, retry_options, **kwargs):
    """Creates a function equivalent to a_func, but that retries on certain
    exceptions.

    Args:
      a_func (callable): A callable.
      retry_options (RetryOptions): Configures the exceptions upon which the
        callable should retry, and the parameters to the exponential backoff
        retry algorithm.
      kwargs: Addtional arguments passed through to the callable.

    Returns:
        Callable: A function that will retry on exception.
    """
    delay_mult = retry_options.backoff_settings.retry_delay_multiplier
    max_delay_millis = retry_options.backoff_settings.max_retry_delay_millis
    has_timeout_settings = _has_timeout_settings(retry_options.backoff_settings)

    if has_timeout_settings:
        timeout_mult = retry_options.backoff_settings.rpc_timeout_multiplier
        max_timeout = (retry_options.backoff_settings.max_rpc_timeout_millis /
                       _MILLIS_PER_SECOND)
        total_timeout = (retry_options.backoff_settings.total_timeout_millis /
                         _MILLIS_PER_SECOND)

    def inner(*args):
        """Equivalent to ``a_func``, but retries upon transient failure.

        Retrying is done through an exponential backoff algorithm configured
        by the options in ``retry``.
        """
        delay = retry_options.backoff_settings.initial_retry_delay_millis
        exc = errors.RetryError('Retry total timeout exceeded before any'
                                'response was received')
        if has_timeout_settings:
            timeout = (
                retry_options.backoff_settings.initial_rpc_timeout_millis /
                _MILLIS_PER_SECOND)

            now = time.time()
            deadline = now + total_timeout
        else:
            timeout = None
            deadline = None

        while deadline is None or now < deadline:
            try:
                to_call = add_timeout_arg(a_func, timeout, **kwargs)
                return to_call(*args)
            except Exception as exception:  # pylint: disable=broad-except
                code = config.exc_to_code(exception)
                if code not in retry_options.retry_codes:
                    raise errors.RetryError(
                        'Exception occurred in retry method that was not'
                        ' classified as transient', exception)

                exc = errors.RetryError(
                    'Retry total timeout exceeded with exception', exception)

                # Sleep a random number which will, on average, equal the
                # expected delay.
                to_sleep = random.uniform(0, delay * 2)
                time.sleep(to_sleep / _MILLIS_PER_SECOND)
                delay = min(delay * delay_mult, max_delay_millis)

                if has_timeout_settings:
                    now = time.time()
                    timeout = min(
                        timeout * timeout_mult, max_timeout, deadline - now)

        raise exc

    return inner