Skip to content

Instantly share code, notes, and snippets.

@moreau-nicolas
Created May 2, 2016 14:26
Show Gist options
  • Save moreau-nicolas/2f4a0ea0051eb09714ba3435a67ff046 to your computer and use it in GitHub Desktop.
Save moreau-nicolas/2f4a0ea0051eb09714ba3435a67ff046 to your computer and use it in GitHub Desktop.
A generic retry mechanism in Java 8
apply plugin: 'java'
sourceCompatibility = 1.8
group = 'com.github.moreaunicolas'
version = '0.0.1-SNAPSHOT'
repositories {
mavenCentral()
}
dependencies {
compile 'ch.qos.logback:logback-classic:1.1.7'
testCompile 'junit:junit:4.12'
testCompile 'org.mockito:mockito-core:1.10.19'
testCompile 'org.powermock:powermock-module-junit4:1.6.4'
testCompile 'org.powermock:powermock-api-mockito:1.6.4'
testCompile 'org.assertj:assertj-core:3.4.1'
}
task wrapper(type: Wrapper) {
gradleVersion = '2.9'
}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.github.moreaunicolas</groupId>
<artifactId>generic-retry</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.1.7</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>1.10.19</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>1.6.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito</artifactId>
<version>1.6.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.4.1</version>
</dependency>
</dependencies>
</project>
package com.github.moreaunicolas;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.Duration;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.function.Supplier;
public final class Retry<T> {
private static final Logger LOGGER = LoggerFactory.getLogger(Retry.class);
private static long DEFAULT_MAX_ATTEMPTS = 3;
private static Duration DEFAULT_WAIT = Duration.ofMillis(250);
private static double DEFAULT_WAIT_FACTOR = 1.;
private final String message;
private final Supplier<T> supplier;
private long maxAttempts = DEFAULT_MAX_ATTEMPTS;
private Duration wait = DEFAULT_WAIT;
private double waitFactor = DEFAULT_WAIT_FACTOR;
private long attempt = 0;
public static void setDefaultMaxAttempts(long defaultMaxAttempts) {
DEFAULT_MAX_ATTEMPTS = defaultMaxAttempts;
}
public static void setDefaultWait(Duration defaultWait) {
DEFAULT_WAIT = defaultWait;
}
public static void setDefaultWaitFactor(double defaultWaitFactor) {
DEFAULT_WAIT_FACTOR = defaultWaitFactor;
}
public static <U> Retry<U> get(String message, Supplier<U> supplier) {
return new Retry<>(message, supplier);
}
private Retry(String message, Supplier<T> supplier) {
this.message = message;
this.supplier = supplier;
}
public Retry<T> withMaxAttempts(long maxAttempts) {
this.maxAttempts = maxAttempts;
return this;
}
public Retry<T> withWait(Duration wait) {
this.wait = wait;
return this;
}
public Retry<T> withWaitFactor(double factor) {
this.waitFactor = factor;
return this;
}
public Optional<T> until(Predicate<T> expectation) {
boolean done;
T result = null;
do {
++attempt;
LOGGER.debug("{}, attempt #{}", message, attempt);
T value = supplier.get();
LOGGER.debug("{}, got value {}", message, value);
done = expectation.test(value);
if (done) {
LOGGER.debug("{}, all done!", message);
result = value;
} else if (hasRemainingAttempts()) {
LOGGER.debug("{}, waiting {} ms", message, wait.toMillis());
try {
Thread.sleep(wait.toMillis());
multiplyWaitByWaitFactor();
} catch (InterruptedException e) {
LOGGER.warn("{}, interrupted!", message);
// Restore the interrupted status
Thread.currentThread().interrupt();
break;
}
} else {
LOGGER.debug("{}, giving up", message);
}
} while (!done && hasRemainingAttempts());
return Optional.ofNullable(result);
}
private void multiplyWaitByWaitFactor() {
double nanos = waitFactor * wait.toNanos();
wait = Duration.ofNanos((long) nanos);
}
private boolean hasRemainingAttempts() {
return attempt < maxAttempts;
}
}
package com.github.moreaunicolas;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import java.time.Duration;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.function.Supplier;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.catchThrowable;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.anyLong;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.never;
@RunWith(PowerMockRunner.class)
@PrepareForTest({ Retry.class })
public class RetryTests {
private static final String MESSAGE = "testing generic retry mechanism";
private static final String EXCEPTION_MESSAGE = "oops";
@Mock private Supplier<Object> supplier;
@Before
public void setUp() throws InterruptedException {
Retry.setDefaultMaxAttempts(1);
Retry.setDefaultWait(Duration.ZERO);
Retry.setDefaultWaitFactor(1);
PowerMockito.spy(Thread.class);
PowerMockito.doNothing().when(Thread.class);
Thread.sleep(anyLong());
doReturn(null).when(supplier).get();
}
@Test
public void thatMaxAttemptZeroDoesNotPreventExecution() {
Retry.get(MESSAGE, supplier)
.withMaxAttempts(0)
.until(always -> false);
verify(supplier, times(1)).get();
}
@Test
public void thatExactlyOneAttemptIsMade() {
Retry.get(MESSAGE, supplier)
.until(always -> true);
verify(supplier, times(1)).get();
}
@Test
public void thatExactlyMaxAttemptsAreMade() {
final int MAX_ATTEMPTS = 5;
Retry.get(MESSAGE, supplier)
.withMaxAttempts(MAX_ATTEMPTS)
.until(always -> false);
verify(supplier, times(MAX_ATTEMPTS)).get();
}
@Test
public void thatDoesNotSleepOnFirstAttempt() throws InterruptedException {
Retry.get(MESSAGE, supplier)
.until(always -> true);
PowerMockito.verifyStatic(never());
Thread.sleep(anyLong());
}
@Test
public void thatDoesNotSleepOnLastAttempt() throws InterruptedException {
final int MAX_ATTEMPTS = 5;
Retry.get(MESSAGE, supplier)
.withMaxAttempts(MAX_ATTEMPTS)
.until(always -> false);
PowerMockito.verifyStatic(times(MAX_ATTEMPTS - 1));
Thread.sleep(anyLong());
}
@Test
public void testThatSleepsTheRightAmount() throws InterruptedException {
final int MAX_ATTEMPTS = 2;
final long WAIT = 12345;
Retry.get(MESSAGE, supplier)
.withMaxAttempts(MAX_ATTEMPTS)
.withWait(Duration.ofMillis(WAIT))
.until(always -> false);
PowerMockito.verifyStatic();
Thread.sleep(eq(WAIT));
}
@Test
public void testThatWaitFactorWorks() throws InterruptedException {
final int MAX_ATTEMPTS = 4;
final long WAIT = 10;
final long WAIT_FACTOR = 5;
Retry.get(MESSAGE, () -> null)
.withMaxAttempts(MAX_ATTEMPTS)
.withWait(Duration.ofMillis(WAIT))
.withWaitFactor(WAIT_FACTOR)
.until(always -> false);
PowerMockito.verifyStatic();
Thread.sleep(eq(WAIT));
PowerMockito.verifyStatic();
Thread.sleep(eq(WAIT * WAIT_FACTOR));
PowerMockito.verifyStatic();
Thread.sleep(eq(WAIT * WAIT_FACTOR * WAIT_FACTOR));
}
@Test
public void thatReturnsEmptyWhenExpectationNeverMet() {
final Optional<Object> actual = Retry.get(MESSAGE, supplier)
.until(always -> false);
assertThat(actual)
.isEmpty();
}
@Test
public void thatReturnsExpectedValueOnFirstAttempt() {
final Object expected = new Object();
doReturn(expected).when(supplier).get();
final Optional<Object> actual = Retry.get(MESSAGE, supplier)
.until(always -> true);
assertThat(actual)
.isPresent()
.contains(expected);
}
private static class TrueOnNthAttempt<T> implements Predicate<T> {
private final long attempt;
private long counter = 1;
private TrueOnNthAttempt(long attempt) {
this.attempt = attempt;
}
@Override
public boolean test(T t) {
return counter++ == attempt;
}
}
@Test
public void thatReturnsExpectedValueOnAnySuccessfulAttempt() {
final long MAX_ATTEMPTS = 4;
final long SUCCESS_ATTEMPT = 2;
final Object expected = new Object();
doReturn(expected).when(supplier).get();
final Optional<Object> actual = Retry.get(MESSAGE, supplier)
.withMaxAttempts(MAX_ATTEMPTS)
.until(new TrueOnNthAttempt<>(SUCCESS_ATTEMPT));
assertThat(actual)
.isPresent()
.contains(expected);
}
@Test
public void thatReturnsExpectedValueOnLastAttempt() {
final long MAX_ATTEMPTS = 4;
final Object expected = new Object();
doReturn(expected).when(supplier).get();
final Optional<Object> actual = Retry.get(MESSAGE, supplier)
.withMaxAttempts(MAX_ATTEMPTS)
.until(new TrueOnNthAttempt<>(MAX_ATTEMPTS));
assertThat(actual)
.isPresent()
.contains(expected);
}
@Test
public void thatDoesNotRetryOnInterruptedException() throws InterruptedException {
final long MAX_ATTEMPTS = 5;
PowerMockito.doThrow(new InterruptedException(EXCEPTION_MESSAGE)).when(Thread.class);
Thread.sleep(anyLong());
Retry.get(MESSAGE, supplier)
.withMaxAttempts(MAX_ATTEMPTS)
.until(always -> false);
verify(supplier, times(1)).get();
}
@Test
public void thatDoesNotRetryOnSupplierException() {
final long MAX_ATTEMPTS = 2;
doThrow(new RuntimeException(EXCEPTION_MESSAGE)).when(supplier).get();
try {
Retry.get(MESSAGE, supplier)
.withMaxAttempts(MAX_ATTEMPTS)
.until(always -> false);
} catch (RuntimeException e) {
verify(supplier, times(1)).get();
}
}
@Test
public void thatThrowsOnSupplierException() {
doThrow(new RuntimeException(EXCEPTION_MESSAGE)).when(supplier).get();
Throwable caught = catchThrowable(() ->
Retry.get(MESSAGE, supplier)
.until(always -> false)
);
assertThat(caught)
.isExactlyInstanceOf(RuntimeException.class)
.hasMessage(EXCEPTION_MESSAGE);
}
}
rootProject.name = 'generic-retry'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment