-
Notifications
You must be signed in to change notification settings - Fork 934
Refine delay jitter for exponential backoff #7206
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,7 @@ | |
| import java.util.concurrent.TimeUnit; | ||
| import java.util.function.Function; | ||
| import java.util.function.Predicate; | ||
| import java.util.function.Supplier; | ||
| import java.util.logging.Level; | ||
| import java.util.logging.Logger; | ||
| import okhttp3.Interceptor; | ||
|
|
@@ -37,7 +38,7 @@ public final class RetryInterceptor implements Interceptor { | |
| private final Function<Response, Boolean> isRetryable; | ||
| private final Predicate<IOException> retryExceptionPredicate; | ||
| private final Sleeper sleeper; | ||
| private final BoundedLongGenerator randomLong; | ||
| private final Supplier<Double> randomJitter; | ||
|
|
||
| /** Constructs a new retrier. */ | ||
| public RetryInterceptor(RetryPolicy retryPolicy, Function<Response, Boolean> isRetryable) { | ||
|
|
@@ -48,7 +49,7 @@ public RetryInterceptor(RetryPolicy retryPolicy, Function<Response, Boolean> isR | |
| ? RetryInterceptor::isRetryableException | ||
| : retryPolicy.getRetryExceptionPredicate(), | ||
| TimeUnit.NANOSECONDS::sleep, | ||
| bound -> ThreadLocalRandom.current().nextLong(bound)); | ||
| () -> ThreadLocalRandom.current().nextDouble(0.8d, 1.2d)); | ||
| } | ||
|
|
||
| // Visible for testing | ||
|
|
@@ -57,12 +58,12 @@ public RetryInterceptor(RetryPolicy retryPolicy, Function<Response, Boolean> isR | |
| Function<Response, Boolean> isRetryable, | ||
| Predicate<IOException> retryExceptionPredicate, | ||
| Sleeper sleeper, | ||
| BoundedLongGenerator randomLong) { | ||
| Supplier<Double> randomJitter) { | ||
| this.retryPolicy = retryPolicy; | ||
| this.isRetryable = isRetryable; | ||
| this.retryExceptionPredicate = retryExceptionPredicate; | ||
| this.sleeper = sleeper; | ||
| this.randomLong = randomLong; | ||
| this.randomJitter = randomJitter; | ||
| } | ||
|
|
||
| @Override | ||
|
|
@@ -75,9 +76,10 @@ public Response intercept(Chain chain) throws IOException { | |
| if (attempt > 0) { | ||
| // Compute and sleep for backoff | ||
| // https://github.com/grpc/proposal/blob/master/A6-client-retries.md#exponential-backoff | ||
| long upperBoundNanos = Math.min(nextBackoffNanos, retryPolicy.getMaxBackoff().toNanos()); | ||
| long backoffNanos = randomLong.get(upperBoundNanos); | ||
| nextBackoffNanos = (long) (nextBackoffNanos * retryPolicy.getBackoffMultiplier()); | ||
| long currentBackoffNanos = | ||
| Math.min(nextBackoffNanos, retryPolicy.getMaxBackoff().toNanos()); | ||
| long backoffNanos = (long) (randomJitter.get() * currentBackoffNanos); | ||
| nextBackoffNanos = (long) (currentBackoffNanos * retryPolicy.getBackoffMultiplier()); | ||
| try { | ||
| sleeper.sleep(backoffNanos); | ||
| } catch (InterruptedException e) { | ||
|
|
@@ -88,31 +90,31 @@ public Response intercept(Chain chain) throws IOException { | |
| if (response != null) { | ||
| response.close(); | ||
| } | ||
| exception = null; | ||
| } | ||
|
|
||
| attempt++; | ||
| try { | ||
| response = chain.proceed(chain.request()); | ||
| if (response != null) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the motivation for changing this part of the logic?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if response is null and no exception happened, the code fails in
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suppose this is possible but I haven't seen response null in practice. If it does occur, we can simply add a null check immediately after
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it's exactly what I did in this PR 😃 also previous code has the issue - if previous(before last) attempt returned rertryable response but the last attempt gets retryable exception method still returns the previous response which is not good as the last state (exception) should be returned |
||
| boolean retryable = Boolean.TRUE.equals(isRetryable.apply(response)); | ||
| if (logger.isLoggable(Level.FINER)) { | ||
| logger.log( | ||
| Level.FINER, | ||
| "Attempt " | ||
| + attempt | ||
| + " returned " | ||
| + (retryable ? "retryable" : "non-retryable") | ||
| + " response: " | ||
| + responseStringRepresentation(response)); | ||
| } | ||
| if (!retryable) { | ||
| return response; | ||
| } | ||
| } else { | ||
| throw new NullPointerException("response cannot be null."); | ||
| } | ||
| } catch (IOException e) { | ||
| exception = e; | ||
| } | ||
| if (response != null) { | ||
| boolean retryable = Boolean.TRUE.equals(isRetryable.apply(response)); | ||
| if (logger.isLoggable(Level.FINER)) { | ||
| logger.log( | ||
| Level.FINER, | ||
| "Attempt " | ||
| + attempt | ||
| + " returned " | ||
| + (retryable ? "retryable" : "non-retryable") | ||
| + " response: " | ||
| + responseStringRepresentation(response)); | ||
| } | ||
| if (!retryable) { | ||
| return response; | ||
| } | ||
| } | ||
| if (exception != null) { | ||
| response = null; | ||
| boolean retryable = retryExceptionPredicate.test(exception); | ||
| if (logger.isLoggable(Level.FINER)) { | ||
| logger.log( | ||
|
|
@@ -128,8 +130,7 @@ public Response intercept(Chain chain) throws IOException { | |
| throw exception; | ||
| } | ||
| } | ||
|
|
||
| } while (attempt < retryPolicy.getMaxAttempts()); | ||
| } while (++attempt < retryPolicy.getMaxAttempts()); | ||
|
|
||
| if (response != null) { | ||
| return response; | ||
|
|
@@ -172,11 +173,6 @@ static boolean isRetryableException(IOException e) { | |
| return false; | ||
| } | ||
|
|
||
| // Visible for testing | ||
| interface BoundedLongGenerator { | ||
| long get(long bound); | ||
| } | ||
|
|
||
| // Visible for testing | ||
| interface Sleeper { | ||
| void sleep(long delayNanos) throws InterruptedException; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should also update the implementation in JdkHttpSender.
I know its not ideal that there are two implementations.. Maybe worth adding a utility function to
RetryPolicythat computes the backoff for a given attemptN. Signature might look like:It wouldn't be as efficient as the current implementation, but...
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, Added code for JdkHttpSender
I did not consider adding that method to calculate a backoff delay time
looking at the code I would say we can build more abstractions for sending requests and checking responses and exceptions, but not sure it's really helpful so let's probably move on with duplicated approach as we had before