diff --git a/retry/src/main/java/com/iluwatar/retry/App.java b/retry/src/main/java/com/iluwatar/retry/App.java index 376a64fbf..20205bdf3 100644 --- a/retry/src/main/java/com/iluwatar/retry/App.java +++ b/retry/src/main/java/com/iluwatar/retry/App.java @@ -71,6 +71,7 @@ public final class App { noErrors(); errorNoRetry(); errorWithRetry(); + errorWithRetryExponentialBackoff(); } private static void noErrors() throws Exception { @@ -102,4 +103,19 @@ public final class App { + "the result %s after a number of attempts %s", customerId, retry.attempts() )); } + + private static void errorWithRetryExponentialBackoff() throws Exception { + final RetryExponentialBackoff retry = new RetryExponentialBackoff<>( + new FindCustomer("123", new CustomerNotFoundException("not found")), + 6, //6 attempts + 30000, //30 s max delay between attempts + e -> CustomerNotFoundException.class.isAssignableFrom(e.getClass()) + ); + op = retry; + final String customerId = op.perform(); + LOG.info(String.format( + "However, retrying the operation while ignoring a recoverable error will eventually yield " + + "the result %s after a number of attempts %s", customerId, retry.attempts() + )); + } } diff --git a/retry/src/main/java/com/iluwatar/retry/RetryExponentialBackoff.java b/retry/src/main/java/com/iluwatar/retry/RetryExponentialBackoff.java new file mode 100644 index 000000000..b24bebbce --- /dev/null +++ b/retry/src/main/java/com/iluwatar/retry/RetryExponentialBackoff.java @@ -0,0 +1,115 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2014-2016 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.iluwatar.retry; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Predicate; + +/** + * Decorates {@link BusinessOperation business operation} with "retry" capabilities. + * + * @author George Aristy (george.aristy@gmail.com) + * @param the remote op's return type + */ +public final class RetryExponentialBackoff implements BusinessOperation { + private final BusinessOperation op; + private final int maxAttempts; + private final long maxDelay; + private final AtomicInteger attempts; + private final Predicate test; + private final List errors; + + /** + * Ctor. + * + * @param op the {@link BusinessOperation} to retry + * @param maxAttempts number of times to retry + * @param ignoreTests tests to check whether the remote exception can be ignored. No exceptions + * will be ignored if no tests are given + */ + @SafeVarargs + public RetryExponentialBackoff( + BusinessOperation op, + int maxAttempts, + long maxDelay, + Predicate... ignoreTests + ) { + this.op = op; + this.maxAttempts = maxAttempts; + this.maxDelay = maxDelay; + this.attempts = new AtomicInteger(); + this.test = Arrays.stream(ignoreTests).reduce(Predicate::or).orElse(e -> false); + this.errors = new ArrayList<>(); + } + + /** + * The errors encountered while retrying, in the encounter order. + * + * @return the errors encountered while retrying + */ + public List errors() { + return Collections.unmodifiableList(this.errors); + } + + /** + * The number of retries performed. + * + * @return the number of retries performed + */ + public int attempts() { + return this.attempts.intValue(); + } + + @Override + public T perform() throws BusinessException { + do { + try { + return this.op.perform(); + } catch (BusinessException e) { + this.errors.add(e); + + if (this.attempts.incrementAndGet() >= this.maxAttempts || !this.test.test(e)) { + throw e; + } + + try { + Random rand = new Random(); + long testDelay = (long) Math.pow(2, this.attempts()) * 1000 + rand.nextInt(1000); + long delay = testDelay < this.maxDelay ? testDelay : maxDelay; + Thread.sleep(delay); + } catch (InterruptedException f) { + //ignore + } + } + } + while (true); + } +} + diff --git a/retry/src/test/java/com/iluwatar/retry/RetryExponentialBackoffTest.java b/retry/src/test/java/com/iluwatar/retry/RetryExponentialBackoffTest.java new file mode 100644 index 000000000..d14b1eef6 --- /dev/null +++ b/retry/src/test/java/com/iluwatar/retry/RetryExponentialBackoffTest.java @@ -0,0 +1,115 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2014-2016 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.iluwatar.retry; + +import org.junit.jupiter.api.Test; +import static org.hamcrest.CoreMatchers.hasItem; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Unit tests for {@link Retry}. + * + * @author George Aristy (george.aristy@gmail.com) + */ +public class RetryExponentialBackoffTest { + /** + * Should contain all errors thrown. + */ + @Test + public void errors() throws Exception { + final BusinessException e = new BusinessException("unhandled"); + final RetryExponentialBackoff retry = new RetryExponentialBackoff<>( + () -> { + throw e; + }, + 2, + 0 + ); + try { + retry.perform(); + } catch (BusinessException ex) { + //ignore + } + + assertThat( + retry.errors(), + hasItem(e) + ); + } + + /** + * No exceptions will be ignored, hence final number of attempts should be 1 even if we're asking + * it to attempt twice. + */ + @Test + public void attempts() { + final BusinessException e = new BusinessException("unhandled"); + final RetryExponentialBackoff retry = new RetryExponentialBackoff<>( + () -> { + throw e; + }, + 2, + 0 + ); + try { + retry.perform(); + } catch (BusinessException ex) { + //ignore + } + + assertThat( + retry.attempts(), + is(1) + ); + } + + /** + * Final number of attempts should be equal to the number of attempts asked because we are + * asking it to ignore the exception that will be thrown. + */ + @Test + public void ignore() throws Exception { + final BusinessException e = new CustomerNotFoundException("customer not found"); + final RetryExponentialBackoff retry = new RetryExponentialBackoff<>( + () -> { + throw e; + }, + 2, + 0, + ex -> CustomerNotFoundException.class.isAssignableFrom(ex.getClass()) + ); + try { + retry.perform(); + } catch (BusinessException ex) { + //ignore + } + + assertThat( + retry.attempts(), + is(2) + ); + } +}