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;
    }
@Test
    public void test() throws ExecutionException {
        Exception[] attempts = new Exception[] {
            new IOException("pretend connect error"),
            new IOException("pretend timeout error"),
            new IOException("pretend read error"),
            null    // success!
        };
        long retryWaitMs = 1000;
        long maxWaitMs = 10000;
        Retryable<String> call = createRetryable(attempts);

        Time time = new MockTime(0, 0, 0);
        assertEquals(0L, time.milliseconds());
        Retry<String> r = new Retry<>(time, retryWaitMs, maxWaitMs);
        r.execute(call);

        long secondWait = retryWaitMs * 2;
        long thirdWait = retryWaitMs * 4;
        long totalWait = retryWaitMs + secondWait + thirdWait;
        assertEquals(totalWait, time.milliseconds());
    }