Skip to content

Commit 7fd7cf9

Browse files
committed
Add duration based maxRetries option to Retry class
1 parent f3d5fb0 commit 7fd7cf9

File tree

2 files changed

+149
-0
lines changed

2 files changed

+149
-0
lines changed

core/src/main/java/org/apache/accumulo/core/util/Retry.java

+60
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,12 @@ public interface NeedsRetries {
272272
* @return this builder with the maximum number of retries set to the provided value
273273
*/
274274
NeedsRetryDelay maxRetries(long max);
275+
276+
/**
277+
* @return this builder with the maximum number of retries set to the number of retries that can
278+
* occur within the given duration
279+
*/
280+
NeedsRetryDelay retriesForDuration(Duration duration);
275281
}
276282

277283
public interface NeedsRetryDelay {
@@ -358,6 +364,7 @@ private static class RetryFactoryBuilder
358364
private Duration waitIncrement;
359365
private Duration logInterval;
360366
private double backOffFactor = 1.5;
367+
private Duration retriesForDuration = null;
361368

362369
RetryFactoryBuilder() {}
363370

@@ -381,6 +388,55 @@ public NeedsRetryDelay maxRetries(long max) {
381388
return this;
382389
}
383390

391+
@Override
392+
public NeedsRetryDelay retriesForDuration(Duration duration) {
393+
checkState();
394+
Preconditions.checkState(!duration.isNegative(), "Duration must not be negative");
395+
this.retriesForDuration = duration;
396+
return this;
397+
}
398+
399+
private void calculateRetriesForDuration() {
400+
long durationMillis = this.retriesForDuration.toMillis();
401+
long totalWaitMillis = initialWait.toMillis();
402+
long currentWaitMillis = totalWaitMillis;
403+
404+
long retries = 0L;
405+
// if the initial wait already exceeds or meets the duration
406+
if (totalWaitMillis >= durationMillis) {
407+
this.maxRetries = retries;
408+
return;
409+
}
410+
411+
while (totalWaitMillis + currentWaitMillis <= durationMillis) {
412+
413+
if (backOffFactor > 1) {
414+
currentWaitMillis = (long) (currentWaitMillis * backOffFactor);
415+
} else {
416+
currentWaitMillis += waitIncrement.toMillis();
417+
}
418+
419+
// Ensure the wait time does not exceed maxWait
420+
if (currentWaitMillis > maxWait.toMillis()) {
421+
currentWaitMillis = maxWait.toMillis();
422+
}
423+
424+
// Break if adding another wait period would exceed the duration
425+
if (totalWaitMillis + currentWaitMillis > durationMillis) {
426+
break;
427+
}
428+
429+
totalWaitMillis += currentWaitMillis;
430+
retries++;
431+
432+
if (retries > 100) {
433+
break;
434+
}
435+
}
436+
437+
this.maxRetries = retries;
438+
}
439+
384440
@Override
385441
public NeedsTimeIncrement retryAfter(Duration duration) {
386442
checkState();
@@ -434,6 +490,10 @@ public RetryFactory createFactory() {
434490

435491
@Override
436492
public Retry createRetry() {
493+
if (retriesForDuration != null) {
494+
calculateRetriesForDuration();
495+
}
496+
this.modifiable = false;
437497
return new Retry(maxRetries, initialWait, waitIncrement, maxWait, logInterval, backOffFactor);
438498
}
439499

core/src/test/java/org/apache/accumulo/core/util/RetryTest.java

+89
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import org.easymock.EasyMock;
3535
import org.junit.jupiter.api.BeforeEach;
3636
import org.junit.jupiter.api.Test;
37+
import org.junit.jupiter.api.Timeout;
3738
import org.slf4j.Logger;
3839
import org.slf4j.LoggerFactory;
3940

@@ -351,4 +352,92 @@ public void testInfiniteRetryWithBackoff() throws InterruptedException {
351352
}
352353
}
353354
}
355+
356+
@Timeout(30)
357+
@Test
358+
public void testRetriesForDuration() throws InterruptedException {
359+
final Duration duration = Duration.ofSeconds(10);
360+
final Duration increment = Duration.ofMillis(500);
361+
Retry retry = Retry.builder().retriesForDuration(duration).retryAfter(Duration.ofMillis(0))
362+
.incrementBy(increment).maxWait(increment).backOffFactor(1)
363+
.logInterval(Duration.ofMinutes(3)).createRetry();
364+
365+
// with the backoff factor set to 1 and with max wait set to the increment value, the expected
366+
// number of retries is the duration divided by the increment
367+
long expectedRetries = duration.dividedBy(increment);
368+
369+
assertExpectedRetries(retry, duration, expectedRetries);
370+
}
371+
372+
@Timeout(30)
373+
@Test
374+
public void testRetriesForDurationWithInitial() throws InterruptedException {
375+
final Duration duration = Duration.ofSeconds(10);
376+
final Duration increment = Duration.ofMillis(500);
377+
final Duration initialWait = Duration.ofMillis(400);
378+
Retry retry =
379+
Retry.builder().retriesForDuration(duration).retryAfter(initialWait).incrementBy(increment)
380+
.maxWait(increment).backOffFactor(1).logInterval(Duration.ofMinutes(3)).createRetry();
381+
382+
// with the backoff factor set to 1 and with max wait set to the increment value, the expected
383+
// number of retries is the duration minus the initial wait divided by the increment
384+
long expectedRetries = duration.minus(initialWait).dividedBy(increment);
385+
386+
assertExpectedRetries(retry, duration, expectedRetries);
387+
}
388+
389+
@Timeout(30)
390+
@Test
391+
public void testRetriesForDuration2() throws InterruptedException {
392+
final Duration increment = Duration.ofMillis(100);
393+
final double backOffFactor = 2;
394+
Duration maxWait = Duration.ofMillis(800); // Allows for 4 doublings 100- > 200 -> 400 -> 800
395+
final Duration initialWait = Duration.ofMillis(50);
396+
397+
// Total duration accounting for the initial wait, the max wait, and the backoff factor
398+
Duration duration =
399+
initialWait.plus(Duration.ofMillis(100 + 200 + 400 + 800 + 800 + 800 + 800));
400+
401+
// count up the expected retries from the calculation above
402+
long expectedRetries = 7;
403+
404+
Retry retry = Retry.builder().retriesForDuration(duration).retryAfter(initialWait)
405+
.incrementBy(increment).maxWait(maxWait).backOffFactor(backOffFactor)
406+
.logInterval(Duration.ofMinutes(3)).createRetry();
407+
408+
assertExpectedRetries(retry, duration, expectedRetries);
409+
}
410+
411+
private static void assertExpectedRetries(Retry retry, Duration duration, long expectedRetries)
412+
throws InterruptedException {
413+
Duration totalTime = Duration.ZERO;
414+
415+
int iterations = 0;
416+
// While the total wait time is less than the duration and there are retries left
417+
while (retry.canRetry() && (totalTime.compareTo(duration) <= 0)) {
418+
iterations++;
419+
totalTime = totalTime.plus(retry.getCurrentWait());
420+
if (totalTime.compareTo(duration) <= 0) {
421+
retry.useRetry();
422+
if (retry.canRetry()) {
423+
log.info("Iteration {} - Total time: {}ms - Current wait: {}ms", iterations,
424+
totalTime.toMillis(), retry.getCurrentWait().toMillis());
425+
retry.waitForNextAttempt(log, "testRetriesForDuration");
426+
}
427+
}
428+
}
429+
430+
assertEquals(expectedRetries, retry.retriesCompleted(),
431+
() -> getErrorMessage(expectedRetries, retry));
432+
}
433+
434+
static String getErrorMessage(long expectedRetries, Retry retry) {
435+
return String.format(
436+
"Expected %d retries, but only completed %d retries. Retry parameters: maxRetries=%dms, startWait=%sms, maxWait=%sms, waitIncrement=%sms, backOffFactor=%f, logInterval=%sms",
437+
expectedRetries, retry.retriesCompleted(), retry.getMaxRetries(),
438+
retry.getCurrentWait().toMillis(), retry.getMaxWait().toMillis(),
439+
retry.getWaitIncrement().toMillis(), retry.getWaitFactor(),
440+
retry.getLogInterval().toMillis());
441+
}
442+
354443
}

0 commit comments

Comments
 (0)