public R execute(Retryable<R> retryable) throws ExecutionException {
        long endMs = time.milliseconds() + retryBackoffMaxMs;
        int currAttempt = 0;
        ExecutionException error = null;

        while (time.milliseconds() <= endMs) {
            currAttempt++;

            try {
                return retryable.call();
            } catch (UnretryableException e) {
                // We've deemed this error to not be worth retrying, so collect the error and
                // fail immediately.
                if (error == null)
                    error = new ExecutionException(e);

                break;
            } catch (ExecutionException e) {
                log.warn("Error during retry attempt {}", currAttempt, e);

                if (error == null)
                    error = e;

                long waitMs = retryBackoffMs * (long) Math.pow(2, currAttempt - 1);
                long diff = endMs - time.milliseconds();
                waitMs = Math.min(waitMs, diff);

                if (waitMs <= 0)
                    break;

                String message = String.format("Attempt %d to make call resulted in an error; sleeping %d ms before retrying",
                    currAttempt, waitMs);
                log.warn(message, e);

                time.sleep(waitMs);
            }
        }

        if (error == null)
            // Really shouldn't ever get to here, but...
            error = new ExecutionException(new IllegalStateException("Exhausted all retry attempts but no attempt returned value or encountered exception"));

        throw error;
    }
Retry encapsulates the mechanism to perform a retry and then exponential
backoff using provided wait times between attempts.

@param <R> Result type
clients/src/main/java/org/apache/kafka/common/security/oauthbearer/internals/secured/Retry.java