├── .gitignore ├── upload.ps1 ├── src ├── main │ └── java │ │ └── io │ │ └── github │ │ └── itning │ │ └── retry │ │ ├── strategy │ │ ├── limit │ │ │ ├── NoAttemptTimeLimit.java │ │ │ ├── AttemptTimeLimiter.java │ │ │ ├── FixedAttemptTimeLimit.java │ │ │ └── AttemptTimeLimiters.java │ │ ├── block │ │ │ ├── ThreadSleepStrategy.java │ │ │ ├── BlockStrategies.java │ │ │ └── BlockStrategy.java │ │ ├── stop │ │ │ ├── NeverStopStrategy.java │ │ │ ├── StopAfterDelayStrategy.java │ │ │ ├── StopAfterAttemptStrategy.java │ │ │ ├── StopStrategy.java │ │ │ └── StopStrategies.java │ │ └── wait │ │ │ ├── FixedWaitStrategy.java │ │ │ ├── CompositeWaitStrategy.java │ │ │ ├── IncrementingWaitStrategy.java │ │ │ ├── RandomWaitStrategy.java │ │ │ ├── ExceptionWaitStrategy.java │ │ │ ├── WaitStrategy.java │ │ │ ├── ExponentialWaitStrategy.java │ │ │ ├── FibonacciWaitStrategy.java │ │ │ └── WaitStrategies.java │ │ ├── listener │ │ └── RetryListener.java │ │ ├── Attempt.java │ │ ├── RetryException.java │ │ ├── RetryerBuilder.java │ │ └── Retryer.java └── test │ └── java │ └── io │ └── github │ └── itning │ └── retry │ ├── RetryWithRunnableTest.java │ ├── AttemptTimeLimiterTest.java │ ├── StopStrategiesTest.java │ ├── WaitStrategiesTest.java │ └── RetryerBuilderTest.java ├── .github ├── dependabot.yml └── workflows │ ├── compile_test.yml │ └── deploy.yml ├── NOTICE ├── HISTORY.md ├── pom.xml ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .idea/ 3 | 4 | -------------------------------------------------------------------------------- /upload.ps1: -------------------------------------------------------------------------------- 1 | mvn -s D:\maven\conf\nexus-aliyun-settings-upload.xml deploy -------------------------------------------------------------------------------- /src/main/java/io/github/itning/retry/strategy/limit/NoAttemptTimeLimit.java: -------------------------------------------------------------------------------- 1 | package io.github.itning.retry.strategy.limit; 2 | 3 | import javax.annotation.concurrent.Immutable; 4 | import java.util.concurrent.Callable; 5 | 6 | /** 7 | * No Attempt Time Limit 8 | * 9 | * @author itning 10 | * @since 3.0.0 11 | */ 12 | @Immutable 13 | public final class NoAttemptTimeLimit implements AttemptTimeLimiter { 14 | @Override 15 | public V call(Callable callable) throws Exception { 16 | return callable.call(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "maven" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2012-2015 Ray Holder 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /src/main/java/io/github/itning/retry/strategy/block/ThreadSleepStrategy.java: -------------------------------------------------------------------------------- 1 | package io.github.itning.retry.strategy.block; 2 | 3 | import javax.annotation.concurrent.Immutable; 4 | 5 | /** 6 | * @author itning 7 | * @since 3.0.0 8 | */ 9 | @Immutable 10 | public final class ThreadSleepStrategy implements BlockStrategy { 11 | 12 | public static final BlockStrategy INSTANCE = new ThreadSleepStrategy(); 13 | 14 | private ThreadSleepStrategy() { 15 | } 16 | 17 | @Override 18 | public void block(long sleepTime) throws InterruptedException { 19 | Thread.sleep(sleepTime); 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/java/io/github/itning/retry/strategy/stop/NeverStopStrategy.java: -------------------------------------------------------------------------------- 1 | package io.github.itning.retry.strategy.stop; 2 | 3 | import io.github.itning.retry.Attempt; 4 | 5 | import javax.annotation.concurrent.Immutable; 6 | 7 | /** 8 | * @author itning 9 | * @since 3.0.0 10 | */ 11 | @Immutable 12 | public final class NeverStopStrategy implements StopStrategy { 13 | public static final StopStrategy INSTANCE = new NeverStopStrategy(); 14 | 15 | private NeverStopStrategy() { 16 | } 17 | 18 | @Override 19 | public boolean shouldStop(Attempt failedAttempt) { 20 | return false; 21 | } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /src/main/java/io/github/itning/retry/strategy/wait/FixedWaitStrategy.java: -------------------------------------------------------------------------------- 1 | package io.github.itning.retry.strategy.wait; 2 | 3 | import io.github.itning.retry.Attempt; 4 | 5 | import javax.annotation.concurrent.Immutable; 6 | 7 | /** 8 | * @author itning 9 | * @since 3.0.0 10 | */ 11 | @Immutable 12 | public final class FixedWaitStrategy implements WaitStrategy { 13 | private final long sleepTime; 14 | 15 | public FixedWaitStrategy(long sleepTime) { 16 | if (sleepTime < 0L) { 17 | throw new IllegalArgumentException("sleepTime must be >= 0 but is " + sleepTime); 18 | } 19 | 20 | this.sleepTime = sleepTime; 21 | } 22 | 23 | @Override 24 | public long computeSleepTime(Attempt failedAttempt) { 25 | return sleepTime; 26 | } 27 | } -------------------------------------------------------------------------------- /.github/workflows/compile_test.yml: -------------------------------------------------------------------------------- 1 | name: compile 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | push: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Set up JDK 1.8 18 | uses: actions/setup-java@v4 19 | with: 20 | distribution: 'zulu' 21 | java-version: 8 22 | 23 | - name: Cache local Maven repository 24 | uses: actions/cache@v4 25 | with: 26 | path: ~/.m2/repository 27 | key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} 28 | restore-keys: | 29 | ${{ runner.os }}-maven- 30 | - name: compile with Maven 31 | run: mvn -B test --file pom.xml 32 | -------------------------------------------------------------------------------- /src/main/java/io/github/itning/retry/strategy/stop/StopAfterDelayStrategy.java: -------------------------------------------------------------------------------- 1 | package io.github.itning.retry.strategy.stop; 2 | 3 | import io.github.itning.retry.Attempt; 4 | 5 | import javax.annotation.concurrent.Immutable; 6 | 7 | /** 8 | * @author itning 9 | * @since 3.0.0 10 | */ 11 | @Immutable 12 | public final class StopAfterDelayStrategy implements StopStrategy { 13 | private final long maxDelay; 14 | 15 | public StopAfterDelayStrategy(long maxDelay) { 16 | if (maxDelay < 0L) { 17 | throw new IllegalArgumentException("maxDelay must be >= 0 but is " + maxDelay); 18 | } 19 | this.maxDelay = maxDelay; 20 | } 21 | 22 | @Override 23 | public boolean shouldStop(Attempt failedAttempt) { 24 | return failedAttempt.getDelaySinceFirstAttempt() >= maxDelay; 25 | } 26 | } -------------------------------------------------------------------------------- /src/main/java/io/github/itning/retry/strategy/stop/StopAfterAttemptStrategy.java: -------------------------------------------------------------------------------- 1 | package io.github.itning.retry.strategy.stop; 2 | 3 | import io.github.itning.retry.Attempt; 4 | 5 | import javax.annotation.concurrent.Immutable; 6 | 7 | /** 8 | * @author itning 9 | * @since 3.0.0 10 | */ 11 | @Immutable 12 | public final class StopAfterAttemptStrategy implements StopStrategy { 13 | private final int maxAttemptNumber; 14 | 15 | public StopAfterAttemptStrategy(int maxAttemptNumber) { 16 | if (maxAttemptNumber < 1) { 17 | throw new IllegalArgumentException("maxAttemptNumber must be >= 1 but is " + maxAttemptNumber); 18 | } 19 | this.maxAttemptNumber = maxAttemptNumber; 20 | } 21 | 22 | @Override 23 | public boolean shouldStop(Attempt failedAttempt) { 24 | return failedAttempt.getAttemptNumber() >= maxAttemptNumber; 25 | } 26 | } -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Publish package to the Maven Central Repository 2 | on: 3 | workflow_dispatch: 4 | #release: 5 | #types: [created] 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - name: Import GPG Key 12 | uses: crazy-max/ghaction-import-gpg@v1 13 | env: 14 | GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} 15 | PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} 16 | - name: Set up Maven Central Repository 17 | uses: actions/setup-java@v4 18 | with: 19 | java-version: '8' 20 | distribution: 'adopt' 21 | server-id: ossrh 22 | server-username: MAVEN_USERNAME 23 | server-password: MAVEN_PASSWORD 24 | - name: Publish package 25 | run: mvn --batch-mode deploy 26 | env: 27 | MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }} 28 | MAVEN_PASSWORD: ${{ secrets.OSSRH_TOKEN }} 29 | -------------------------------------------------------------------------------- /src/main/java/io/github/itning/retry/strategy/wait/CompositeWaitStrategy.java: -------------------------------------------------------------------------------- 1 | package io.github.itning.retry.strategy.wait; 2 | 3 | import io.github.itning.retry.Attempt; 4 | 5 | import javax.annotation.concurrent.Immutable; 6 | import java.util.List; 7 | 8 | /** 9 | * @author itning 10 | * @since 3.0.0 11 | */ 12 | @Immutable 13 | public final class CompositeWaitStrategy implements WaitStrategy { 14 | private final List waitStrategies; 15 | 16 | public CompositeWaitStrategy(List waitStrategies) { 17 | if (waitStrategies.isEmpty()) { 18 | throw new IllegalStateException("Need at least one wait strategy"); 19 | } 20 | this.waitStrategies = waitStrategies; 21 | } 22 | 23 | @Override 24 | public long computeSleepTime(Attempt failedAttempt) { 25 | long waitTime = 0L; 26 | for (WaitStrategy waitStrategy : waitStrategies) { 27 | waitTime += waitStrategy.computeSleepTime(failedAttempt); 28 | } 29 | return waitTime; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/io/github/itning/retry/strategy/wait/IncrementingWaitStrategy.java: -------------------------------------------------------------------------------- 1 | package io.github.itning.retry.strategy.wait; 2 | 3 | import io.github.itning.retry.Attempt; 4 | 5 | import javax.annotation.concurrent.Immutable; 6 | 7 | /** 8 | * @author itning 9 | * @since 3.0.0 10 | */ 11 | @Immutable 12 | public final class IncrementingWaitStrategy implements WaitStrategy { 13 | private final long initialSleepTime; 14 | private final long increment; 15 | 16 | public IncrementingWaitStrategy(long initialSleepTime, 17 | long increment) { 18 | if (initialSleepTime < 0L) { 19 | throw new IllegalArgumentException("initialSleepTime must be >= 0 but is " + initialSleepTime); 20 | } 21 | 22 | this.initialSleepTime = initialSleepTime; 23 | this.increment = increment; 24 | } 25 | 26 | @Override 27 | public long computeSleepTime(Attempt failedAttempt) { 28 | long result = initialSleepTime + (increment * (failedAttempt.getAttemptNumber() - 1)); 29 | return Math.max(result, 0L); 30 | } 31 | } -------------------------------------------------------------------------------- /src/main/java/io/github/itning/retry/strategy/wait/RandomWaitStrategy.java: -------------------------------------------------------------------------------- 1 | package io.github.itning.retry.strategy.wait; 2 | 3 | import io.github.itning.retry.Attempt; 4 | 5 | import javax.annotation.concurrent.Immutable; 6 | import java.util.Random; 7 | 8 | /** 9 | * @author itning 10 | * @since 3.0.0 11 | */ 12 | @Immutable 13 | public final class RandomWaitStrategy implements WaitStrategy { 14 | private static final Random RANDOM = new Random(); 15 | private final long minimum; 16 | private final long maximum; 17 | 18 | public RandomWaitStrategy(long minimum, long maximum) { 19 | if (minimum < 0) { 20 | throw new IllegalArgumentException("minimum must be >= 0 but is " + minimum); 21 | } 22 | if (maximum <= minimum) { 23 | throw new IllegalArgumentException("maximum must be > minimum but maximum is " + maximum + " and minimum is " + minimum); 24 | } 25 | this.minimum = minimum; 26 | this.maximum = maximum; 27 | } 28 | 29 | @Override 30 | public long computeSleepTime(Attempt failedAttempt) { 31 | long t = Math.abs(RANDOM.nextLong()) % (maximum - minimum); 32 | return t + minimum; 33 | } 34 | } -------------------------------------------------------------------------------- /src/main/java/io/github/itning/retry/strategy/wait/ExceptionWaitStrategy.java: -------------------------------------------------------------------------------- 1 | package io.github.itning.retry.strategy.wait; 2 | 3 | import io.github.itning.retry.Attempt; 4 | 5 | import javax.annotation.Nonnull; 6 | import javax.annotation.concurrent.Immutable; 7 | import java.util.function.Function; 8 | 9 | /** 10 | * @author itning 11 | * @since 3.0.0 12 | */ 13 | @Immutable 14 | public final class ExceptionWaitStrategy implements WaitStrategy { 15 | private final Class exceptionClass; 16 | private final Function function; 17 | 18 | public ExceptionWaitStrategy(@Nonnull Class exceptionClass, @Nonnull Function function) { 19 | this.exceptionClass = exceptionClass; 20 | this.function = function; 21 | } 22 | 23 | @SuppressWarnings({"ThrowableResultOfMethodCallIgnored", "unchecked"}) 24 | @Override 25 | public long computeSleepTime(Attempt lastAttempt) { 26 | if (lastAttempt.hasException()) { 27 | Throwable cause = lastAttempt.getExceptionCause(); 28 | if (exceptionClass.isAssignableFrom(cause.getClass())) { 29 | return function.apply((T) cause); 30 | } 31 | } 32 | return 0L; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/io/github/itning/retry/strategy/wait/WaitStrategy.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2015 Ray Holder 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.itning.retry.strategy.wait; 18 | 19 | import io.github.itning.retry.Attempt; 20 | 21 | /** 22 | * A strategy used to decide how long to sleep before retrying after a failed attempt. 23 | * 24 | * @author JB 25 | */ 26 | public interface WaitStrategy { 27 | 28 | /** 29 | * Returns the time, in milliseconds, to sleep before retrying. 30 | * 31 | * @param failedAttempt the previous failed {@code Attempt} 32 | * @return the sleep time before next attempt 33 | */ 34 | long computeSleepTime(Attempt failedAttempt); 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/io/github/itning/retry/strategy/block/BlockStrategies.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2015 Ray Holder 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.itning.retry.strategy.block; 18 | 19 | /** 20 | * Factory class for {@link BlockStrategy} instances. 21 | */ 22 | public final class BlockStrategies { 23 | 24 | private BlockStrategies() { 25 | } 26 | 27 | /** 28 | * Returns a block strategy that puts the current thread to sleep between 29 | * retries. 30 | * 31 | * @return a block strategy that puts the current thread to sleep between retries 32 | */ 33 | public static BlockStrategy threadSleepStrategy() { 34 | return ThreadSleepStrategy.INSTANCE; 35 | } 36 | } -------------------------------------------------------------------------------- /src/main/java/io/github/itning/retry/strategy/stop/StopStrategy.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2015 Ray Holder 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.itning.retry.strategy.stop; 18 | 19 | import io.github.itning.retry.Attempt; 20 | 21 | /** 22 | * A strategy used to decide if a retryer must stop retrying after a failed attempt or not. 23 | * 24 | * @author JB 25 | */ 26 | public interface StopStrategy { 27 | 28 | /** 29 | * Returns true if the retryer should stop retrying. 30 | * 31 | * @param failedAttempt the previous failed {@code Attempt} 32 | * @return true if the retryer must stop, false otherwise 33 | */ 34 | boolean shouldStop(Attempt failedAttempt); 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/io/github/itning/retry/listener/RetryListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2015 Ray Holder 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.itning.retry.listener; 18 | 19 | import io.github.itning.retry.Attempt; 20 | import io.github.itning.retry.Retryer; 21 | 22 | /** 23 | * This listener provides callbacks for several events that occur when running 24 | * code through a {@link Retryer} instance. 25 | * 26 | * @param the type returned by the retryer callable 27 | */ 28 | @FunctionalInterface 29 | public interface RetryListener { 30 | 31 | /** 32 | * The listener method will be triggered if and only when retrying 33 | * 34 | * @param attempt the current {@link Attempt} 35 | */ 36 | void onRetry(Attempt attempt); 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/io/github/itning/retry/strategy/limit/AttemptTimeLimiter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2015 Ray Holder 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.itning.retry.strategy.limit; 18 | 19 | import java.util.concurrent.Callable; 20 | 21 | /** 22 | * A rule to wrap any single attempt in a time limit, where it will possibly be interrupted if the limit is exceeded. 23 | * 24 | * @param return type of Callable 25 | * @author Jason Dunkelberger (dirkraft) 26 | */ 27 | public interface AttemptTimeLimiter { 28 | /** 29 | * Call this method to achieve the purpose of retry 30 | * 31 | * @param callable to subject to the time limit 32 | * @return the return of the given callable 33 | * @throws Exception any exception from this invocation 34 | */ 35 | V call(Callable callable) throws Exception; 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/io/github/itning/retry/strategy/wait/ExponentialWaitStrategy.java: -------------------------------------------------------------------------------- 1 | package io.github.itning.retry.strategy.wait; 2 | 3 | import io.github.itning.retry.Attempt; 4 | 5 | import javax.annotation.concurrent.Immutable; 6 | 7 | /** 8 | * @author itning 9 | * @since 3.0.0 10 | */ 11 | @Immutable 12 | public final class ExponentialWaitStrategy implements WaitStrategy { 13 | private final long multiplier; 14 | private final long maximumWait; 15 | 16 | public ExponentialWaitStrategy(long multiplier, 17 | long maximumWait) { 18 | if (multiplier <= 0L) { 19 | throw new IllegalArgumentException("multiplier must be > 0 but is " + multiplier); 20 | } 21 | if (maximumWait < 0L) { 22 | throw new IllegalArgumentException("maximumWait must be >= 0 but is " + maximumWait); 23 | } 24 | if (multiplier >= maximumWait) { 25 | throw new IllegalArgumentException("multiplier must be < maximumWait but is " + multiplier); 26 | } 27 | 28 | this.multiplier = multiplier; 29 | this.maximumWait = maximumWait; 30 | } 31 | 32 | @Override 33 | public long computeSleepTime(Attempt failedAttempt) { 34 | double exp = Math.pow(2, failedAttempt.getAttemptNumber()); 35 | long result = Math.round(multiplier * exp); 36 | if (result > maximumWait) { 37 | result = maximumWait; 38 | } 39 | return Math.max(result, 0L); 40 | } 41 | } -------------------------------------------------------------------------------- /src/main/java/io/github/itning/retry/strategy/limit/FixedAttemptTimeLimit.java: -------------------------------------------------------------------------------- 1 | package io.github.itning.retry.strategy.limit; 2 | 3 | import com.google.common.util.concurrent.SimpleTimeLimiter; 4 | import com.google.common.util.concurrent.TimeLimiter; 5 | 6 | import javax.annotation.Nonnull; 7 | import javax.annotation.concurrent.Immutable; 8 | import java.util.Objects; 9 | import java.util.concurrent.Callable; 10 | import java.util.concurrent.ExecutorService; 11 | import java.util.concurrent.TimeUnit; 12 | 13 | /** 14 | * Fixed Attempt Time Limit 15 | * 16 | * @author itning 17 | * @since 3.0.0 18 | */ 19 | @Immutable 20 | public final class FixedAttemptTimeLimit implements AttemptTimeLimiter { 21 | 22 | private final TimeLimiter timeLimiter; 23 | private final long duration; 24 | private final TimeUnit timeUnit; 25 | 26 | public FixedAttemptTimeLimit(long duration, @Nonnull TimeUnit timeUnit, @Nonnull ExecutorService executorService) { 27 | this(SimpleTimeLimiter.create(executorService), duration, timeUnit); 28 | } 29 | 30 | private FixedAttemptTimeLimit(@Nonnull TimeLimiter timeLimiter, long duration, @Nonnull TimeUnit timeUnit) { 31 | Objects.requireNonNull(timeLimiter); 32 | Objects.requireNonNull(timeUnit); 33 | this.timeLimiter = timeLimiter; 34 | this.duration = duration; 35 | this.timeUnit = timeUnit; 36 | } 37 | 38 | @Override 39 | public V call(Callable callable) throws Exception { 40 | return timeLimiter.callWithTimeout(callable, duration, timeUnit); 41 | } 42 | } -------------------------------------------------------------------------------- /src/test/java/io/github/itning/retry/RetryWithRunnableTest.java: -------------------------------------------------------------------------------- 1 | package io.github.itning.retry; 2 | 3 | import io.github.itning.retry.strategy.limit.AttemptTimeLimiters; 4 | import io.github.itning.retry.strategy.stop.StopStrategies; 5 | import io.github.itning.retry.strategy.wait.WaitStrategies; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import java.util.concurrent.ExecutionException; 9 | import java.util.concurrent.TimeUnit; 10 | import java.util.concurrent.atomic.AtomicInteger; 11 | 12 | import static org.junit.jupiter.api.Assertions.fail; 13 | 14 | /** 15 | * @author itning 16 | * @since 3.0.0 17 | */ 18 | public class RetryWithRunnableTest { 19 | 20 | 21 | @Test 22 | void testRunnableApi() { 23 | AtomicInteger atomicInteger = new AtomicInteger(); 24 | Retryer retryer = RetryerBuilder.newBuilder() 25 | .retryIfException() 26 | .withRetryListener(attempt -> {}) 27 | .withWaitStrategy(WaitStrategies.fixedWait(1, TimeUnit.SECONDS)) 28 | .withStopStrategy(StopStrategies.stopAfterAttempt(5)) 29 | .withAttemptTimeLimiter(AttemptTimeLimiters.noTimeLimit()) 30 | .build(); 31 | 32 | try { 33 | retryer.call(new Runnable() { 34 | @Override 35 | public void run() { 36 | atomicInteger.incrementAndGet(); 37 | } 38 | }); 39 | } catch (ExecutionException e) { 40 | fail("ExecutionException expected"); 41 | } catch (RetryException e) { 42 | fail("RetryException expected"); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/io/github/itning/retry/strategy/block/BlockStrategy.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2015 Ray Holder 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.itning.retry.strategy.block; 18 | 19 | import io.github.itning.retry.strategy.wait.WaitStrategy; 20 | 21 | /** 22 | * This is a strategy used to decide how a retryer should block between retry 23 | * attempts. Normally this is just a Thread.sleep(), but implementations can be 24 | * something more elaborate if desired. 25 | */ 26 | public interface BlockStrategy { 27 | 28 | /** 29 | * Attempt to block for the designated amount of time. Implementations 30 | * that don't block or otherwise delay the processing from within this 31 | * method for the given sleep duration can significantly modify the behavior 32 | * of any configured {@link WaitStrategy}. Caution 33 | * is advised when generating your own implementations. 34 | * 35 | * @param sleepTime the computed sleep duration in milliseconds 36 | * @throws InterruptedException when block interrupted 37 | */ 38 | void block(long sleepTime) throws InterruptedException; 39 | } -------------------------------------------------------------------------------- /src/main/java/io/github/itning/retry/strategy/wait/FibonacciWaitStrategy.java: -------------------------------------------------------------------------------- 1 | package io.github.itning.retry.strategy.wait; 2 | 3 | import io.github.itning.retry.Attempt; 4 | 5 | import javax.annotation.concurrent.Immutable; 6 | 7 | /** 8 | * @author itning 9 | * @since 3.0.0 10 | */ 11 | @Immutable 12 | public final class FibonacciWaitStrategy implements WaitStrategy { 13 | private final long multiplier; 14 | private final long maximumWait; 15 | 16 | public FibonacciWaitStrategy(long multiplier, long maximumWait) { 17 | if (multiplier <= 0L) { 18 | throw new IllegalArgumentException("multiplier must be > 0 but is " + multiplier); 19 | } 20 | if (maximumWait < 0L) { 21 | throw new IllegalArgumentException("maximumWait must be >= 0 but is " + maximumWait); 22 | } 23 | if (multiplier >= maximumWait) { 24 | throw new IllegalArgumentException("multiplier must be < maximumWait but is " + multiplier); 25 | } 26 | 27 | this.multiplier = multiplier; 28 | this.maximumWait = maximumWait; 29 | } 30 | 31 | @Override 32 | public long computeSleepTime(Attempt failedAttempt) { 33 | long fib = fib(failedAttempt.getAttemptNumber()); 34 | long result = multiplier * fib; 35 | 36 | if (result > maximumWait || result < 0L) { 37 | result = maximumWait; 38 | } 39 | 40 | return Math.max(result, 0L); 41 | } 42 | 43 | private long fib(long n) { 44 | if (n == 0L) { 45 | return 0L; 46 | } 47 | if (n == 1L) { 48 | return 1L; 49 | } 50 | 51 | long prevPrev = 0L; 52 | long prev = 1L; 53 | long result = 0L; 54 | 55 | for (long i = 2L; i <= n; i++) { 56 | result = prev + prevPrev; 57 | prevPrev = prev; 58 | prev = result; 59 | } 60 | 61 | return result; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | ## 3.0.0-BETA0 - 2021-09-08 2 | * The first personal maintenance version 3 | * Change project build tool from gradle to maven and chang package name 4 | * Retry listener strategy and remove @Beta annotation 5 | * Remove useless interface method modifiers 6 | * Optimize the code 7 | * Add a runnable param call method 8 | * fix: UPSTREAM ISSUES https://github.com/rholder/guava-retrying/pull/99/files 9 | * fix: UPSTREAM ISSUES https://github.com/rholder/guava-retrying/pull/98/files 10 | * Modify test case 11 | 12 | ## 2.0.0 - 2015-06-30 13 | * Calculate sleep time from failed attempt #25 (yaroslavm) 14 | * Be consistent about "duration" method parameters #30 (Stephan202) 15 | * Use an open Maven dependency range for Guava dependency #32 (voiceinsideyou) 16 | * Add @Beta RetryListener support #36 (kevinconaway) 17 | * Update to Gradle 2.x #38 18 | * Minimal breaking 1.0.x to 2.0.x API changes for Attempt state, hence the major version update 19 | 20 | ## 1.0.7 - 2015-01-20 21 | * New composite wait strategy #12 (shasts) 22 | * Adding block strategies to the Retryer to decide how to block (tchdp) 23 | 24 | ## 1.0.6 - 2014-03-26 25 | * Javadoc updates for Java 8 (shasts) 26 | * Bug from System.nanoTime() (fror), fix in #15 27 | * Travis CI testing now working for Java 8 28 | 29 | ## 1.0.5 - 2013-12-04 30 | * Added Javadoc for all versions 31 | * Added FibonacciWaitStrategy (joschi) 32 | * Updated tested Guava version range from 10.x.x - 15.0 (joschi) 33 | * Updated all dependencies (joschi) 34 | * Updated to Gradle 1.9 (joschi) 35 | 36 | ## 1.0.4 - 2013-07-08 37 | * Added tested Guava version range from 10.x.x - 14.0.1 38 | * Added Exception cause propagation to RetryException to fix #3 39 | 40 | ## 1.0.3 - 2013-01-16 41 | * Added time limit per attempt in a Retryer (dirkraft) 42 | * Added license text 43 | 44 | ## 1.0.2 - 2012-11-22 45 | * Added Gradle wrapper support 46 | * Updated top-level package to com.github.rholder.retry 47 | 48 | ## 1.0.1 - 2012-08-29 49 | * Added Javadoc links 50 | * Added exponential wait strategy and unit tests 51 | 52 | ## 1.0.0 - 2012-08-26 53 | * Initial stable release, packaging for Maven central, no changes from original source 54 | -------------------------------------------------------------------------------- /src/main/java/io/github/itning/retry/strategy/limit/AttemptTimeLimiters.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2015 Ray Holder 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.itning.retry.strategy.limit; 18 | 19 | 20 | import javax.annotation.Nonnull; 21 | import java.util.Objects; 22 | import java.util.concurrent.ExecutorService; 23 | import java.util.concurrent.TimeUnit; 24 | 25 | /** 26 | * Factory class for instances of {@link AttemptTimeLimiter} 27 | * 28 | * @author Jason Dunkelberger (dirkraft) 29 | */ 30 | public class AttemptTimeLimiters { 31 | 32 | private AttemptTimeLimiters() { 33 | } 34 | 35 | /** 36 | * @param The type of the computation result. 37 | * @return an {@link AttemptTimeLimiter} impl which has no time limit 38 | */ 39 | public static AttemptTimeLimiter noTimeLimit() { 40 | return new NoAttemptTimeLimit<>(); 41 | } 42 | 43 | /** 44 | * For control over thread management. 45 | * See the note on {@link com.google.common.util.concurrent.SimpleTimeLimiter#create(ExecutorService)}, which this AttemptTimeLimiter uses. 46 | * 47 | * @param duration that an attempt may persist before being circumvented 48 | * @param timeUnit of the 'duration' arg 49 | * @param executorService used to enforce time limit 50 | * @param the type of the computation result 51 | * @return an {@link AttemptTimeLimiter} with a fixed time limit for each attempt 52 | */ 53 | public static AttemptTimeLimiter fixedTimeLimit(long duration, @Nonnull TimeUnit timeUnit, @Nonnull ExecutorService executorService) { 54 | Objects.requireNonNull(timeUnit); 55 | return new FixedAttemptTimeLimit<>(duration, timeUnit, executorService); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/test/java/io/github/itning/retry/AttemptTimeLimiterTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2015 Ray Holder 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.itning.retry; 18 | 19 | import io.github.itning.retry.strategy.limit.AttemptTimeLimiters; 20 | import org.junit.jupiter.api.Test; 21 | 22 | import java.util.concurrent.*; 23 | 24 | import static org.junit.jupiter.api.Assertions.assertEquals; 25 | import static org.junit.jupiter.api.Assertions.fail; 26 | 27 | /** 28 | * @author Jason Dunkelberger (dirkraft) 29 | */ 30 | public class AttemptTimeLimiterTest { 31 | 32 | private static final ExecutorService EXECUTOR_SERVICE = Executors.newSingleThreadExecutor(); 33 | 34 | Retryer r = RetryerBuilder.newBuilder() 35 | .withAttemptTimeLimiter(AttemptTimeLimiters.fixedTimeLimit(1, TimeUnit.SECONDS, EXECUTOR_SERVICE)) 36 | .build(); 37 | 38 | @Test 39 | public void testAttemptTimeLimit() throws ExecutionException, RetryException { 40 | try { 41 | r.call(new SleepyOut(0L)); 42 | } catch (ExecutionException e) { 43 | fail("Should not timeout"); 44 | } 45 | 46 | try { 47 | r.call(new SleepyOut(10 * 1000L)); 48 | fail("Expected timeout exception"); 49 | } catch (ExecutionException e) { 50 | // expected 51 | assertEquals(TimeoutException.class, e.getCause().getClass()); 52 | } 53 | } 54 | 55 | static class SleepyOut implements Callable { 56 | 57 | final long sleepMs; 58 | 59 | SleepyOut(long sleepMs) { 60 | this.sleepMs = sleepMs; 61 | } 62 | 63 | @Override 64 | public Void call() throws Exception { 65 | Thread.sleep(sleepMs); 66 | System.out.println("I'm awake now"); 67 | return null; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/test/java/io/github/itning/retry/StopStrategiesTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2015 Ray Holder 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.itning.retry; 18 | 19 | import io.github.itning.retry.strategy.stop.StopStrategies; 20 | import org.junit.jupiter.api.Test; 21 | 22 | import java.util.concurrent.TimeUnit; 23 | 24 | import static org.junit.jupiter.api.Assertions.assertFalse; 25 | import static org.junit.jupiter.api.Assertions.assertTrue; 26 | 27 | 28 | public class StopStrategiesTest { 29 | 30 | @Test 31 | public void testNeverStop() { 32 | assertFalse(StopStrategies.neverStop().shouldStop(failedAttempt(3, 6546L))); 33 | } 34 | 35 | @Test 36 | public void testStopAfterAttempt() { 37 | assertFalse(StopStrategies.stopAfterAttempt(3).shouldStop(failedAttempt(2, 6546L))); 38 | assertTrue(StopStrategies.stopAfterAttempt(3).shouldStop(failedAttempt(3, 6546L))); 39 | assertTrue(StopStrategies.stopAfterAttempt(3).shouldStop(failedAttempt(4, 6546L))); 40 | } 41 | 42 | @Test 43 | public void testStopAfterDelayWithMilliseconds() { 44 | assertFalse(StopStrategies.stopAfterDelay(1000L, TimeUnit.MILLISECONDS).shouldStop(failedAttempt(2, 999L))); 45 | assertTrue(StopStrategies.stopAfterDelay(1000L, TimeUnit.MILLISECONDS).shouldStop(failedAttempt(2, 1000L))); 46 | assertTrue(StopStrategies.stopAfterDelay(1000L, TimeUnit.MILLISECONDS).shouldStop(failedAttempt(2, 1001L))); 47 | } 48 | 49 | @Test 50 | public void testStopAfterDelayWithTimeUnit() { 51 | assertFalse(StopStrategies.stopAfterDelay(1, TimeUnit.SECONDS).shouldStop(failedAttempt(2, 999L))); 52 | assertTrue(StopStrategies.stopAfterDelay(1, TimeUnit.SECONDS).shouldStop(failedAttempt(2, 1000L))); 53 | assertTrue(StopStrategies.stopAfterDelay(1, TimeUnit.SECONDS).shouldStop(failedAttempt(2, 1001L))); 54 | } 55 | 56 | public Attempt failedAttempt(long attemptNumber, long delaySinceFirstAttempt) { 57 | return new Retryer.ExceptionAttempt<>(new RuntimeException(), attemptNumber, delaySinceFirstAttempt); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/io/github/itning/retry/strategy/stop/StopStrategies.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2015 Ray Holder 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.itning.retry.strategy.stop; 18 | 19 | import javax.annotation.Nonnull; 20 | import java.util.Objects; 21 | import java.util.concurrent.TimeUnit; 22 | 23 | /** 24 | * Factory class for {@link StopStrategy} instances. 25 | * 26 | * @author JB 27 | */ 28 | public final class StopStrategies { 29 | 30 | private StopStrategies() { 31 | } 32 | 33 | /** 34 | * Returns a stop strategy which never stops retrying. It might be best to 35 | * try not to abuse services with this kind of behavior when small wait 36 | * intervals between retry attempts are being used. 37 | * 38 | * @return a stop strategy which never stops 39 | */ 40 | public static StopStrategy neverStop() { 41 | return NeverStopStrategy.INSTANCE; 42 | } 43 | 44 | /** 45 | * Returns a stop strategy which stops after N failed attempts. 46 | * 47 | * @param attemptNumber the number of failed attempts before stopping 48 | * @return a stop strategy which stops after {@code attemptNumber} attempts 49 | */ 50 | public static StopStrategy stopAfterAttempt(int attemptNumber) { 51 | return new StopAfterAttemptStrategy(attemptNumber); 52 | } 53 | 54 | /** 55 | * Returns a stop strategy which stops after a given delay. If an 56 | * unsuccessful attempt is made, this {@link StopStrategy} will check if the 57 | * amount of time that's passed from the first attempt has exceeded the 58 | * given delay amount. If it has exceeded this delay, then using this 59 | * strategy causes the retrying to stop. 60 | * 61 | * @param duration the delay, starting from first attempt 62 | * @param timeUnit the unit of the duration 63 | * @return a stop strategy which stops after {@code delayInMillis} time in milliseconds 64 | */ 65 | public static StopStrategy stopAfterDelay(long duration, @Nonnull TimeUnit timeUnit) { 66 | Objects.requireNonNull(timeUnit, "The time unit may not be null"); 67 | return new StopAfterDelayStrategy(timeUnit.toMillis(duration)); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/io/github/itning/retry/Attempt.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2015 Ray Holder 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.itning.retry; 18 | 19 | import java.util.concurrent.ExecutionException; 20 | 21 | /** 22 | * An attempt of a call, which resulted either in a result returned by the call, 23 | * or in a Throwable thrown by the call. 24 | * 25 | * @param The type returned by the wrapped callable. 26 | * @author JB 27 | */ 28 | public interface Attempt { 29 | 30 | /** 31 | * Returns the result of the attempt, if any. 32 | * 33 | * @return the result of the attempt 34 | * @throws ExecutionException if an exception was thrown by the attempt. The thrown 35 | * exception is set as the cause of the ExecutionException 36 | */ 37 | V get() throws ExecutionException; 38 | 39 | /** 40 | * Tells if the call returned a result or not 41 | * 42 | * @return true if the call returned a result, false 43 | * if it threw an exception 44 | */ 45 | boolean hasResult(); 46 | 47 | /** 48 | * Tells if the call threw an exception or not 49 | * 50 | * @return true if the call threw an exception, false 51 | * if it returned a result 52 | */ 53 | boolean hasException(); 54 | 55 | /** 56 | * Gets the result of the call 57 | * 58 | * @return the result of the call 59 | * @throws IllegalStateException if the call didn't return a result, but threw an exception, 60 | * as indicated by {@link #hasResult()} 61 | */ 62 | V getResult() throws IllegalStateException; 63 | 64 | /** 65 | * Gets the exception thrown by the call 66 | * 67 | * @return the exception thrown by the call 68 | * @throws IllegalStateException if the call didn't throw an exception, 69 | * as indicated by {@link #hasException()} 70 | */ 71 | Throwable getExceptionCause() throws IllegalStateException; 72 | 73 | /** 74 | * The number, starting from 1, of this attempt. 75 | * 76 | * @return the attempt number 77 | */ 78 | long getAttemptNumber(); 79 | 80 | /** 81 | * The delay since the start of the first attempt, in milliseconds. 82 | * 83 | * @return the delay since the start of the first attempt, in milliseconds 84 | */ 85 | long getDelaySinceFirstAttempt(); 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/io/github/itning/retry/RetryException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2015 Ray Holder 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.itning.retry; 18 | 19 | import javax.annotation.Nonnull; 20 | import javax.annotation.concurrent.Immutable; 21 | import java.util.Objects; 22 | 23 | /** 24 | * An exception indicating that none of the attempts of the {@link Retryer} 25 | * succeeded. If the last {@link Attempt} resulted in an Exception, it is set as 26 | * the cause of the {@link RetryException}. 27 | * 28 | * @author JB 29 | */ 30 | @Immutable 31 | public final class RetryException extends Exception { 32 | 33 | private final int numberOfFailedAttempts; 34 | private final Attempt lastFailedAttempt; 35 | 36 | /** 37 | * If the last {@link Attempt} had an Exception, ensure it is available in 38 | * the stack trace. 39 | * 40 | * @param numberOfFailedAttempts times we've tried and failed 41 | * @param lastFailedAttempt what happened the last time we failed 42 | */ 43 | public RetryException(int numberOfFailedAttempts, @Nonnull Attempt lastFailedAttempt) { 44 | this("Retrying failed to complete successfully after " + numberOfFailedAttempts + " attempts.", numberOfFailedAttempts, lastFailedAttempt); 45 | } 46 | 47 | /** 48 | * If the last {@link Attempt} had an Exception, ensure it is available in 49 | * the stack trace. 50 | * 51 | * @param message Exception description to be added to the stack trace 52 | * @param numberOfFailedAttempts times we've tried and failed 53 | * @param lastFailedAttempt what happened the last time we failed 54 | */ 55 | public RetryException(String message, int numberOfFailedAttempts, Attempt lastFailedAttempt) { 56 | super(message, Objects.requireNonNull(lastFailedAttempt, "Last attempt was null").hasException() ? lastFailedAttempt.getExceptionCause() : null); 57 | this.numberOfFailedAttempts = numberOfFailedAttempts; 58 | this.lastFailedAttempt = lastFailedAttempt; 59 | } 60 | 61 | /** 62 | * Returns the number of failed attempts 63 | * 64 | * @return the number of failed attempts 65 | */ 66 | public int getNumberOfFailedAttempts() { 67 | return numberOfFailedAttempts; 68 | } 69 | 70 | /** 71 | * Returns the last failed attempt 72 | * 73 | * @return the last failed attempt 74 | */ 75 | public Attempt getLastFailedAttempt() { 76 | return lastFailedAttempt; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | io.github.itning 7 | guava-retrying3 8 | 3.0.7 9 | jar 10 | 11 | guava-retrying3 12 | guava retrying3 is a basic guava retry library 13 | https://github.com/itning/guava-retrying3 14 | 15 | 16 | 17 | The Apache Software License, Version 2.0 18 | http://www.apache.org/licenses/LICENSE-2.0.txt 19 | repo 20 | 21 | 22 | 23 | 24 | scm:git@github.com:itning/guava-retrying3.git 25 | scm:git@github.com:itning/guava-retrying3.git 26 | scm:git@github.com:itning/guava-retrying3.git 27 | 28 | 29 | 30 | 31 | itning 32 | itning666@gmail.com 33 | 34 | 35 | 36 | 37 | 1.8 38 | 33.5.0-jre 39 | 5.13.4 40 | UTF-8 41 | 3.14.1 42 | 3.4.0 43 | 3.12.0 44 | 3.2.8 45 | 1.7.0 46 | 47 | 48 | 49 | 50 | 51 | com.google.guava 52 | guava 53 | ${guava.version} 54 | 55 | 56 | 57 | com.github.spotbugs 58 | spotbugs-annotations 59 | 4.9.8 60 | 61 | 62 | 63 | org.junit.jupiter 64 | junit-jupiter-api 65 | ${junit.version} 66 | test 67 | 68 | 69 | 70 | 71 | 72 | 73 | sonatype-snapshots 74 | https://oss.sonatype.org/content/repositories/snapshots 75 | 76 | 77 | sonatype-staging 78 | https://oss.sonatype.org/service/local/staging/deploy/maven2 79 | 80 | 81 | 82 | 83 | 84 | 85 | org.apache.maven.plugins 86 | maven-javadoc-plugin 87 | ${maven-javadoc-plugin.version} 88 | 89 | ${project.build.sourceEncoding} 90 | ${project.build.sourceEncoding} 91 | false 92 | 93 | 94 | 95 | attach-javadocs 96 | 97 | jar 98 | 99 | 100 | 101 | 102 | 103 | org.apache.maven.plugins 104 | maven-source-plugin 105 | ${maven-source-plugin.version} 106 | 107 | 108 | attach-sources 109 | 110 | jar-no-fork 111 | 112 | 113 | 114 | 115 | 116 | org.apache.maven.plugins 117 | maven-compiler-plugin 118 | ${maven-compiler-plugin.version} 119 | 120 | ${java.version} 121 | ${java.version} 122 | ${project.build.sourceEncoding} 123 | true 124 | 125 | 126 | 127 | org.apache.maven.plugins 128 | maven-gpg-plugin 129 | ${maven-gpg-plugin.version} 130 | 131 | 132 | sign-artifacts 133 | verify 134 | 135 | sign 136 | 137 | 138 | 139 | 140 | 141 | org.sonatype.plugins 142 | nexus-staging-maven-plugin 143 | ${nexus-staging-maven-plugin.version} 144 | true 145 | 146 | ossrh 147 | https://ossrh-staging-api.central.sonatype.com/ 148 | true 149 | 150 | 151 | 152 | 153 | 154 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | guava-retrying3 3 |

4 | 5 |
6 | 7 | A basic guava retry library. 8 | 9 | [![GitHub stars](https://img.shields.io/github/stars/itning/guava-retrying3.svg?style=social&label=Stars)](https://github.com/itning/guava-retrying3/stargazers) 10 | [![GitHub forks](https://img.shields.io/github/forks/itning/guava-retrying3.svg?style=social&label=Fork)](https://github.com/itning/guava-retrying3/network/members) 11 | [![GitHub watchers](https://img.shields.io/github/watchers/itning/guava-retrying3.svg?style=social&label=Watch)](https://github.com/itning/guava-retrying3/watchers) 12 | [![GitHub followers](https://img.shields.io/github/followers/itning.svg?style=social&label=Follow)](https://github.com/itning?tab=followers) 13 | 14 | [![GitHub issues](https://img.shields.io/github/issues/itning/guava-retrying3.svg)](https://github.com/itning/guava-retrying3/issues) 15 | [![GitHub license](https://img.shields.io/github/license/itning/guava-retrying3.svg)](https://github.com/itning/guava-retrying3/blob/main/LICENSE) 16 | [![GitHub last commit](https://img.shields.io/github/last-commit/itning/guava-retrying3.svg)](https://github.com/itning/guava-retrying3/commits) 17 | [![GitHub release](https://img.shields.io/github/release/itning/guava-retrying3.svg)](https://search.maven.org/artifact/io.github.itning/guava-retrying3) 18 | [![GitHub repo size in bytes](https://img.shields.io/github/repo-size/itning/guava-retrying3.svg)](https://github.com/itning/guava-retrying3) 19 | ![HitCount](https://hitcount.itning.com/?u=itning&r=guava-retrying3) 20 | [![language](https://img.shields.io/badge/language-JAVA-green.svg)](https://github.com/itning/guava-retrying3) 21 | 22 |
23 | 24 | 25 | # Notice 26 | This is a personal maintenance version, which is not compatible with the API of the [original library](https://github.com/rholder/guava-retrying) 27 | 28 | **Incompatible changes** 29 | 30 | - Package structure modification 31 | - [RetryListener](https://github.com/itning/guava-retrying3/blob/main/src/main/java/io/github/itning/retry/listener/RetryListener.java#L34) now only calls back the onRetry method when a retry occurs 32 | - Replace the project build tool from gradle to maven 33 | - Package name from `com.github.rholder.retry.*` to `io.github.itning.retry.*` 34 | 35 | # What is this? 36 | The guava-retrying module provides a general purpose method for retrying arbitrary Java code with specific stop, retry, 37 | and exception handling capabilities that are enhanced by Guava's predicate matching. 38 | 39 | This is a fork of the excellent RetryerBuilder code posted [here](http://code.google.com/p/guava-libraries/issues/detail?id=490) 40 | by Jean-Baptiste Nizet (JB). I've added a Gradle build for pushing it up to my little corner of Maven Central so that 41 | others can easily pull it into their existing projects with minimal effort. It also includes 42 | exponential and Fibonacci backoff [WaitStrategies](http://rholder.github.io/guava-retrying/javadoc/2.0.0/com/github/rholder/retry/WaitStrategies.html) 43 | that might be useful for situations where more well-behaved service polling is preferred. 44 | 45 | # Maven 46 | ```xml 47 | 48 | io.github.itning 49 | guava-retrying3 50 | 3.0.5 51 | 52 | ``` 53 | 54 | ## TIP 55 | 56 | If you don't use `io.github.itning.retry.strategy.limit.FixedAttemptTimeLimit` in your code, you can exclude `guava` dependency 57 | 58 | ```xml 59 | 60 | io.github.itning 61 | guava-retrying3 62 | 3.0.5 63 | 64 | 65 | com.google.guava 66 | guava 67 | 68 | 69 | 70 | ``` 71 | 72 | # Gradle 73 | 74 | ```groovy 75 | implementation "io.github.itning:guava-retrying3:3.0.5" 76 | ``` 77 | 78 | # Quickstart 79 | A minimal sample of some of the functionality would look like: 80 | 81 | ```java 82 | Callable callable = new Callable() { 83 | public Boolean call() throws Exception { 84 | return true; // do something useful here 85 | } 86 | }; 87 | 88 | Retryer retryer = RetryerBuilder.newBuilder() 89 | .retryIfResult(Predicates.isNull()) 90 | .retryIfExceptionOfType(IOException.class) 91 | .retryIfRuntimeException() 92 | .withStopStrategy(StopStrategies.stopAfterAttempt(3)) 93 | .build(); 94 | try { 95 | retryer.call(callable); 96 | } catch (RetryException e) { 97 | e.printStackTrace(); 98 | } catch (ExecutionException e) { 99 | e.printStackTrace(); 100 | } 101 | ``` 102 | 103 | This will retry whenever the result of the `Callable` is null, if an `IOException` is thrown, or if any other 104 | `RuntimeException` is thrown from the `call()` method. It will stop after attempting to retry 3 times and throw a 105 | `RetryException` that contains information about the last failed attempt. If any other `Exception` pops out of the 106 | `call()` method it's wrapped and rethrown in an `ExecutionException`. 107 | 108 | # Exponential Backoff 109 | 110 | Create a `Retryer` that retries forever, waiting after every failed retry in increasing exponential backoff intervals 111 | until at most 5 minutes. After 5 minutes, retry from then on in 5 minute intervals. 112 | 113 | ```java 114 | Retryer retryer = RetryerBuilder.newBuilder() 115 | .retryIfExceptionOfType(IOException.class) 116 | .retryIfRuntimeException() 117 | .withWaitStrategy(WaitStrategies.exponentialWait(100, 5, TimeUnit.MINUTES)) 118 | .withStopStrategy(StopStrategies.neverStop()) 119 | .build(); 120 | ``` 121 | You can read more about [exponential backoff](http://en.wikipedia.org/wiki/Exponential_backoff) and the historic role 122 | it played in the development of TCP/IP in [Congestion Avoidance and Control](http://ee.lbl.gov/papers/congavoid.pdf). 123 | 124 | # Fibonacci Backoff 125 | 126 | Create a `Retryer` that retries forever, waiting after every failed retry in increasing Fibonacci backoff intervals 127 | until at most 2 minutes. After 2 minutes, retry from then on in 2 minute intervals. 128 | 129 | ```java 130 | Retryer retryer = RetryerBuilder.newBuilder() 131 | .retryIfExceptionOfType(IOException.class) 132 | .retryIfRuntimeException() 133 | .withWaitStrategy(WaitStrategies.fibonacciWait(100, 2, TimeUnit.MINUTES)) 134 | .withStopStrategy(StopStrategies.neverStop()) 135 | .build(); 136 | ``` 137 | 138 | Similar to the `ExponentialWaitStrategy`, the `FibonacciWaitStrategy` follows a pattern of waiting an increasing amount 139 | of time after each failed attempt. 140 | 141 | Instead of an exponential function it's (obviously) using a 142 | [Fibonacci sequence](https://en.wikipedia.org/wiki/Fibonacci_numbers) to calculate the wait time. 143 | 144 | Depending on the problem at hand, the `FibonacciWaitStrategy` might perform better and lead to better throughput than 145 | the `ExponentialWaitStrategy` - at least according to 146 | [A Performance Comparison of Different Backoff Algorithms under Different Rebroadcast Probabilities for MANETs](http://www.comp.leeds.ac.uk/ukpew09/papers/12.pdf). 147 | 148 | The implementation of `FibonacciWaitStrategy` is using an iterative version of the Fibonacci because a (naive) recursive 149 | version will lead to a [StackOverflowError](http://docs.oracle.com/javase/7/docs/api/java/lang/StackOverflowError.html) 150 | at a certain point (although very unlikely with useful parameters for retrying). 151 | 152 | Inspiration for this implementation came from [Efficient retry/backoff mechanisms](https://paperairoplane.net/?p=640). 153 | 154 | # Building from source 155 | The guava-retrying module uses a [maven](https://maven.apache.org/)-based build system. 156 | The only prerequisites are [Git](https://help.github.com/articles/set-up-git) and JDK 1.8+. 157 | 158 | ## check out sources 159 | `git clone git://github.com/itning/guava-retrying3.git` 160 | 161 | ## compile and test, build all jars 162 | `./mvn ` 163 | 164 | ## install all jars into your local Maven cache 165 | `./mvn install` 166 | 167 | # License 168 | The guava-retrying module is released under version 2.0 of the 169 | [Apache License](http://www.apache.org/licenses/LICENSE-2.0). 170 | 171 | # Contributors 172 | * Jean-Baptiste Nizet (JB) 173 | * Jason Dunkelberger (dirkraft) 174 | * Diwaker Gupta (diwakergupta) 175 | * Jochen Schalanda (joschi) 176 | * Shajahan Palayil (shasts) 177 | * Olivier Grégoire (fror) 178 | * Andrei Savu (andreisavu) 179 | * (tchdp) 180 | * (squalloser) 181 | * Yaroslav Matveychuk (yaroslavm) 182 | * Stephan Schroevers (Stephan202) 183 | * Chad (voiceinsideyou) 184 | * Kevin Conaway (kevinconaway) 185 | * Alberto Scotto (alb-i986) 186 | -------------------------------------------------------------------------------- /src/test/java/io/github/itning/retry/WaitStrategiesTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2015 Ray Holder 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.itning.retry; 18 | 19 | import com.google.common.base.Function; 20 | import com.google.common.collect.Sets; 21 | import io.github.itning.retry.strategy.wait.WaitStrategies; 22 | import io.github.itning.retry.strategy.wait.WaitStrategy; 23 | import org.junit.jupiter.api.Test; 24 | 25 | import java.util.Set; 26 | import java.util.concurrent.TimeUnit; 27 | 28 | import static org.junit.jupiter.api.Assertions.assertEquals; 29 | import static org.junit.jupiter.api.Assertions.assertTrue; 30 | 31 | public class WaitStrategiesTest { 32 | 33 | 34 | @Test 35 | public void testNoWait() { 36 | WaitStrategy noWait = WaitStrategies.noWait(); 37 | assertEquals(0L, noWait.computeSleepTime(failedAttempt(18, 9879L))); 38 | } 39 | 40 | @Test 41 | public void testFixedWait() { 42 | WaitStrategy fixedWait = WaitStrategies.fixedWait(1000L, TimeUnit.MILLISECONDS); 43 | assertEquals(1000L, fixedWait.computeSleepTime(failedAttempt(12, 6546L))); 44 | } 45 | 46 | @Test 47 | public void testIncrementingWait() { 48 | WaitStrategy incrementingWait = WaitStrategies.incrementingWait(500L, TimeUnit.MILLISECONDS, 100L, TimeUnit.MILLISECONDS); 49 | assertEquals(500L, incrementingWait.computeSleepTime(failedAttempt(1, 6546L))); 50 | assertEquals(600L, incrementingWait.computeSleepTime(failedAttempt(2, 6546L))); 51 | assertEquals(700L, incrementingWait.computeSleepTime(failedAttempt(3, 6546L))); 52 | } 53 | 54 | @Test 55 | public void testRandomWait() { 56 | WaitStrategy randomWait = WaitStrategies.randomWait(1000L, TimeUnit.MILLISECONDS, 2000L, TimeUnit.MILLISECONDS); 57 | Set times = Sets.newHashSet(); 58 | times.add(randomWait.computeSleepTime(failedAttempt(1, 6546L))); 59 | times.add(randomWait.computeSleepTime(failedAttempt(1, 6546L))); 60 | times.add(randomWait.computeSleepTime(failedAttempt(1, 6546L))); 61 | times.add(randomWait.computeSleepTime(failedAttempt(1, 6546L))); 62 | assertTrue(times.size() > 1); // if not, the random is not random 63 | for (long time : times) { 64 | assertTrue(time >= 1000L); 65 | assertTrue(time <= 2000L); 66 | } 67 | } 68 | 69 | @Test 70 | public void testRandomWaitWithoutMinimum() { 71 | WaitStrategy randomWait = WaitStrategies.randomWait(2000L, TimeUnit.MILLISECONDS); 72 | Set times = Sets.newHashSet(); 73 | times.add(randomWait.computeSleepTime(failedAttempt(1, 6546L))); 74 | times.add(randomWait.computeSleepTime(failedAttempt(1, 6546L))); 75 | times.add(randomWait.computeSleepTime(failedAttempt(1, 6546L))); 76 | times.add(randomWait.computeSleepTime(failedAttempt(1, 6546L))); 77 | assertTrue(times.size() > 1); // if not, the random is not random 78 | for (long time : times) { 79 | assertTrue(time >= 0L); 80 | assertTrue(time <= 2000L); 81 | } 82 | } 83 | 84 | @Test 85 | public void testExponential() { 86 | WaitStrategy exponentialWait = WaitStrategies.exponentialWait(); 87 | assertTrue(exponentialWait.computeSleepTime(failedAttempt(1, 0)) == 2); 88 | assertTrue(exponentialWait.computeSleepTime(failedAttempt(2, 0)) == 4); 89 | assertTrue(exponentialWait.computeSleepTime(failedAttempt(3, 0)) == 8); 90 | assertTrue(exponentialWait.computeSleepTime(failedAttempt(4, 0)) == 16); 91 | assertTrue(exponentialWait.computeSleepTime(failedAttempt(5, 0)) == 32); 92 | assertTrue(exponentialWait.computeSleepTime(failedAttempt(6, 0)) == 64); 93 | } 94 | 95 | @Test 96 | public void testExponentialWithMaximumWait() { 97 | WaitStrategy exponentialWait = WaitStrategies.exponentialWait(40, TimeUnit.MILLISECONDS); 98 | assertTrue(exponentialWait.computeSleepTime(failedAttempt(1, 0)) == 2); 99 | assertTrue(exponentialWait.computeSleepTime(failedAttempt(2, 0)) == 4); 100 | assertTrue(exponentialWait.computeSleepTime(failedAttempt(3, 0)) == 8); 101 | assertTrue(exponentialWait.computeSleepTime(failedAttempt(4, 0)) == 16); 102 | assertTrue(exponentialWait.computeSleepTime(failedAttempt(5, 0)) == 32); 103 | assertTrue(exponentialWait.computeSleepTime(failedAttempt(6, 0)) == 40); 104 | assertTrue(exponentialWait.computeSleepTime(failedAttempt(7, 0)) == 40); 105 | assertTrue(exponentialWait.computeSleepTime(failedAttempt(Integer.MAX_VALUE, 0)) == 40); 106 | } 107 | 108 | @Test 109 | public void testExponentialWithMultiplierAndMaximumWait() { 110 | WaitStrategy exponentialWait = WaitStrategies.exponentialWait(1000, 50000, TimeUnit.MILLISECONDS); 111 | assertTrue(exponentialWait.computeSleepTime(failedAttempt(1, 0)) == 2000); 112 | assertTrue(exponentialWait.computeSleepTime(failedAttempt(2, 0)) == 4000); 113 | assertTrue(exponentialWait.computeSleepTime(failedAttempt(3, 0)) == 8000); 114 | assertTrue(exponentialWait.computeSleepTime(failedAttempt(4, 0)) == 16000); 115 | assertTrue(exponentialWait.computeSleepTime(failedAttempt(5, 0)) == 32000); 116 | assertTrue(exponentialWait.computeSleepTime(failedAttempt(6, 0)) == 50000); 117 | assertTrue(exponentialWait.computeSleepTime(failedAttempt(7, 0)) == 50000); 118 | assertTrue(exponentialWait.computeSleepTime(failedAttempt(Integer.MAX_VALUE, 0)) == 50000); 119 | } 120 | 121 | @Test 122 | public void testFibonacci() { 123 | WaitStrategy fibonacciWait = WaitStrategies.fibonacciWait(); 124 | assertTrue(fibonacciWait.computeSleepTime(failedAttempt(1, 0L)) == 1L); 125 | assertTrue(fibonacciWait.computeSleepTime(failedAttempt(2, 0L)) == 1L); 126 | assertTrue(fibonacciWait.computeSleepTime(failedAttempt(3, 0L)) == 2L); 127 | assertTrue(fibonacciWait.computeSleepTime(failedAttempt(4, 0L)) == 3L); 128 | assertTrue(fibonacciWait.computeSleepTime(failedAttempt(5, 0L)) == 5L); 129 | assertTrue(fibonacciWait.computeSleepTime(failedAttempt(6, 0L)) == 8L); 130 | } 131 | 132 | @Test 133 | public void testFibonacciWithMaximumWait() { 134 | WaitStrategy fibonacciWait = WaitStrategies.fibonacciWait(10L, TimeUnit.MILLISECONDS); 135 | assertTrue(fibonacciWait.computeSleepTime(failedAttempt(1, 0L)) == 1L); 136 | assertTrue(fibonacciWait.computeSleepTime(failedAttempt(2, 0L)) == 1L); 137 | assertTrue(fibonacciWait.computeSleepTime(failedAttempt(3, 0L)) == 2L); 138 | assertTrue(fibonacciWait.computeSleepTime(failedAttempt(4, 0L)) == 3L); 139 | assertTrue(fibonacciWait.computeSleepTime(failedAttempt(5, 0L)) == 5L); 140 | assertTrue(fibonacciWait.computeSleepTime(failedAttempt(6, 0L)) == 8L); 141 | assertTrue(fibonacciWait.computeSleepTime(failedAttempt(7, 0L)) == 10L); 142 | assertTrue(fibonacciWait.computeSleepTime(failedAttempt(Integer.MAX_VALUE, 0L)) == 10L); 143 | } 144 | 145 | @Test 146 | public void testFibonacciWithMultiplierAndMaximumWait() { 147 | WaitStrategy fibonacciWait = WaitStrategies.fibonacciWait(1000L, 50000L, TimeUnit.MILLISECONDS); 148 | assertTrue(fibonacciWait.computeSleepTime(failedAttempt(1, 0L)) == 1000L); 149 | assertTrue(fibonacciWait.computeSleepTime(failedAttempt(2, 0L)) == 1000L); 150 | assertTrue(fibonacciWait.computeSleepTime(failedAttempt(3, 0L)) == 2000L); 151 | assertTrue(fibonacciWait.computeSleepTime(failedAttempt(4, 0L)) == 3000L); 152 | assertTrue(fibonacciWait.computeSleepTime(failedAttempt(5, 0L)) == 5000L); 153 | assertTrue(fibonacciWait.computeSleepTime(failedAttempt(6, 0L)) == 8000L); 154 | assertTrue(fibonacciWait.computeSleepTime(failedAttempt(7, 0L)) == 13000L); 155 | assertTrue(fibonacciWait.computeSleepTime(failedAttempt(Integer.MAX_VALUE, 0L)) == 50000L); 156 | } 157 | 158 | @Test 159 | public void testExceptionWait() { 160 | WaitStrategy exceptionWait = WaitStrategies.exceptionWait(RuntimeException.class, zeroSleepFunction()); 161 | assertEquals(0L, exceptionWait.computeSleepTime(failedAttempt(42, 7227))); 162 | 163 | WaitStrategy oneMinuteWait = WaitStrategies.exceptionWait(RuntimeException.class, oneMinuteSleepFunction()); 164 | assertEquals(3600 * 1000L, oneMinuteWait.computeSleepTime(failedAttempt(42, 7227))); 165 | 166 | WaitStrategy noMatchRetryAfterWait = WaitStrategies.exceptionWait(RetryAfterException.class, customSleepFunction()); 167 | assertEquals(0L, noMatchRetryAfterWait.computeSleepTime(failedAttempt(42, 7227))); 168 | 169 | WaitStrategy retryAfterWait = WaitStrategies.exceptionWait(RetryAfterException.class, customSleepFunction()); 170 | assertEquals(29L, retryAfterWait.computeSleepTime(failedRetryAfterAttempt(42, 7227))); 171 | } 172 | 173 | public Attempt failedAttempt(long attemptNumber, long delaySinceFirstAttempt) { 174 | return new Retryer.ExceptionAttempt(new RuntimeException(), attemptNumber, delaySinceFirstAttempt); 175 | } 176 | 177 | public Attempt failedRetryAfterAttempt(long attemptNumber, long delaySinceFirstAttempt) { 178 | return new Retryer.ExceptionAttempt(new RetryAfterException(), attemptNumber, delaySinceFirstAttempt); 179 | } 180 | 181 | public Function zeroSleepFunction() { 182 | return new Function() { 183 | @Override 184 | public Long apply(RuntimeException input) { 185 | return 0L; 186 | } 187 | }; 188 | } 189 | 190 | public Function oneMinuteSleepFunction() { 191 | return new Function() { 192 | @Override 193 | public Long apply(RuntimeException input) { 194 | return 3600 * 1000L; 195 | } 196 | }; 197 | } 198 | 199 | public Function customSleepFunction() { 200 | return new Function() { 201 | @Override 202 | public Long apply(RetryAfterException input) { 203 | return input.getRetryAfter(); 204 | } 205 | }; 206 | } 207 | 208 | public class RetryAfterException extends RuntimeException { 209 | private final long retryAfter = 29L; 210 | 211 | public long getRetryAfter() { 212 | return retryAfter; 213 | } 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/main/java/io/github/itning/retry/RetryerBuilder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2015 Ray Holder 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.itning.retry; 18 | 19 | import io.github.itning.retry.listener.RetryListener; 20 | import io.github.itning.retry.strategy.block.BlockStrategies; 21 | import io.github.itning.retry.strategy.block.BlockStrategy; 22 | import io.github.itning.retry.strategy.limit.AttemptTimeLimiter; 23 | import io.github.itning.retry.strategy.limit.AttemptTimeLimiters; 24 | import io.github.itning.retry.strategy.stop.StopStrategies; 25 | import io.github.itning.retry.strategy.stop.StopStrategy; 26 | import io.github.itning.retry.strategy.wait.WaitStrategies; 27 | import io.github.itning.retry.strategy.wait.WaitStrategy; 28 | 29 | import javax.annotation.Nonnull; 30 | import java.util.ArrayList; 31 | import java.util.List; 32 | import java.util.Objects; 33 | import java.util.function.Predicate; 34 | 35 | /** 36 | * A builder used to configure and create a {@link Retryer}. 37 | * 38 | * @param result of a {@link Retryer}'s call, the type of the call return value 39 | * @author JB 40 | * @author Jason Dunkelberger (dirkraft) 41 | */ 42 | public class RetryerBuilder { 43 | private AttemptTimeLimiter attemptTimeLimiter; 44 | private StopStrategy stopStrategy; 45 | private WaitStrategy waitStrategy; 46 | private BlockStrategy blockStrategy; 47 | private Predicate> rejectionPredicate = vAttempt -> false; 48 | private final List> listeners = new ArrayList<>(); 49 | 50 | private RetryerBuilder() { 51 | } 52 | 53 | /** 54 | * Constructs a new builder 55 | * 56 | * @param result of a {@link Retryer}'s call, the type of the call return value 57 | * @return the new builder 58 | */ 59 | public static RetryerBuilder newBuilder() { 60 | return new RetryerBuilder<>(); 61 | } 62 | 63 | /** 64 | * Adds a listener that will be notified of each attempt that is made 65 | * 66 | * @param listener Listener to add 67 | * @return this 68 | */ 69 | public RetryerBuilder withRetryListener(@Nonnull RetryListener listener) { 70 | Objects.requireNonNull(listener, "listener may not be null"); 71 | listeners.add(listener); 72 | return this; 73 | } 74 | 75 | /** 76 | * Sets the wait strategy used to decide how long to sleep between failed attempts. 77 | * The default strategy is to retry immediately after a failed attempt. 78 | * 79 | * @param waitStrategy the strategy used to sleep between failed attempts 80 | * @return this 81 | * @throws IllegalStateException if a wait strategy has already been set. 82 | */ 83 | public RetryerBuilder withWaitStrategy(@Nonnull WaitStrategy waitStrategy) throws IllegalStateException { 84 | Objects.requireNonNull(waitStrategy, "waitStrategy may not be null"); 85 | if (this.waitStrategy != null) { 86 | throw new IllegalStateException("a wait strategy has already been set " + this.waitStrategy); 87 | } 88 | this.waitStrategy = waitStrategy; 89 | return this; 90 | } 91 | 92 | /** 93 | * Sets the stop strategy used to decide when to stop retrying. The default strategy is to not stop at all . 94 | * 95 | * @param stopStrategy the strategy used to decide when to stop retrying 96 | * @return this 97 | * @throws IllegalStateException if a stop strategy has already been set. 98 | */ 99 | public RetryerBuilder withStopStrategy(@Nonnull StopStrategy stopStrategy) throws IllegalStateException { 100 | Objects.requireNonNull(stopStrategy, "stopStrategy may not be null"); 101 | if (this.stopStrategy != null) { 102 | throw new IllegalStateException("a stop strategy has already been set " + this.stopStrategy); 103 | } 104 | this.stopStrategy = stopStrategy; 105 | return this; 106 | } 107 | 108 | 109 | /** 110 | * Sets the block strategy used to decide how to block between retry attempts. The default strategy is to use Thread#sleep(). 111 | * 112 | * @param blockStrategy the strategy used to decide how to block between retry attempts 113 | * @return this 114 | * @throws IllegalStateException if a block strategy has already been set. 115 | */ 116 | public RetryerBuilder withBlockStrategy(@Nonnull BlockStrategy blockStrategy) throws IllegalStateException { 117 | Objects.requireNonNull(blockStrategy, "blockStrategy may not be null"); 118 | if (this.blockStrategy != null) { 119 | throw new IllegalStateException("a block strategy has already been set " + this.blockStrategy); 120 | } 121 | this.blockStrategy = blockStrategy; 122 | return this; 123 | } 124 | 125 | 126 | /** 127 | * Configures the retryer to limit the duration of any particular attempt by the given duration. 128 | * 129 | * @param attemptTimeLimiter to apply to each attempt 130 | * @return this 131 | */ 132 | public RetryerBuilder withAttemptTimeLimiter(@Nonnull AttemptTimeLimiter attemptTimeLimiter) { 133 | Objects.requireNonNull(attemptTimeLimiter); 134 | this.attemptTimeLimiter = attemptTimeLimiter; 135 | return this; 136 | } 137 | 138 | /** 139 | * Configures the retryer to retry if an exception (i.e. any Exception or subclass 140 | * of Exception) is thrown by the call. 141 | * 142 | * @return this 143 | */ 144 | public RetryerBuilder retryIfException() { 145 | rejectionPredicate = rejectionPredicate.or(new ExceptionClassPredicate<>(Exception.class)); 146 | return this; 147 | } 148 | 149 | /** 150 | * Configures the retryer to retry if a runtime exception (i.e. any RuntimeException or subclass 151 | * of RuntimeException) is thrown by the call. 152 | * 153 | * @return this 154 | */ 155 | public RetryerBuilder retryIfRuntimeException() { 156 | rejectionPredicate = rejectionPredicate.or(new ExceptionClassPredicate<>(RuntimeException.class)); 157 | return this; 158 | } 159 | 160 | /** 161 | * Configures the retryer to retry if an exception of the given class (or subclass of the given class) is 162 | * thrown by the call. 163 | * 164 | * @param exceptionClass the type of the exception which should cause the retryer to retry 165 | * @return this 166 | */ 167 | public RetryerBuilder retryIfExceptionOfType(@Nonnull Class exceptionClass) { 168 | Objects.requireNonNull(exceptionClass, "exceptionClass may not be null"); 169 | rejectionPredicate = rejectionPredicate.or(new ExceptionClassPredicate<>(exceptionClass)); 170 | return this; 171 | } 172 | 173 | /** 174 | * Configures the retryer to retry if an exception satisfying the given predicate is 175 | * thrown by the call. 176 | * 177 | * @param exceptionPredicate the predicate which causes a retry if satisfied 178 | * @return this 179 | */ 180 | public RetryerBuilder retryIfException(@Nonnull Predicate exceptionPredicate) { 181 | Objects.requireNonNull(exceptionPredicate, "exceptionPredicate may not be null"); 182 | rejectionPredicate = rejectionPredicate.or(new ExceptionPredicate<>(exceptionPredicate)); 183 | return this; 184 | } 185 | 186 | /** 187 | * Configures the retryer to retry if the result satisfies the given predicate. 188 | * 189 | * @param resultPredicate a predicate applied to the result, and which causes the retryer 190 | * to retry if the predicate is satisfied 191 | * @return this 192 | */ 193 | public RetryerBuilder retryIfResult(@Nonnull Predicate resultPredicate) { 194 | Objects.requireNonNull(resultPredicate, "resultPredicate may not be null"); 195 | rejectionPredicate = rejectionPredicate.or(new ResultPredicate<>(resultPredicate)); 196 | return this; 197 | } 198 | 199 | /** 200 | * Builds the retryer. 201 | * 202 | * @return the built retryer. 203 | */ 204 | public Retryer build() { 205 | AttemptTimeLimiter theAttemptTimeLimiter = attemptTimeLimiter == null ? AttemptTimeLimiters.noTimeLimit() : attemptTimeLimiter; 206 | StopStrategy theStopStrategy = stopStrategy == null ? StopStrategies.neverStop() : stopStrategy; 207 | WaitStrategy theWaitStrategy = waitStrategy == null ? WaitStrategies.noWait() : waitStrategy; 208 | BlockStrategy theBlockStrategy = blockStrategy == null ? BlockStrategies.threadSleepStrategy() : blockStrategy; 209 | 210 | return new Retryer<>(theAttemptTimeLimiter, theStopStrategy, theWaitStrategy, theBlockStrategy, rejectionPredicate, listeners); 211 | } 212 | 213 | private static final class ExceptionClassPredicate implements Predicate> { 214 | 215 | private final Class exceptionClass; 216 | 217 | public ExceptionClassPredicate(Class exceptionClass) { 218 | this.exceptionClass = exceptionClass; 219 | } 220 | 221 | @Override 222 | public boolean test(Attempt attempt) { 223 | if (!attempt.hasException()) { 224 | return false; 225 | } 226 | return exceptionClass.isAssignableFrom(attempt.getExceptionCause().getClass()); 227 | } 228 | } 229 | 230 | private static final class ResultPredicate implements Predicate> { 231 | 232 | private final Predicate delegate; 233 | 234 | public ResultPredicate(Predicate delegate) { 235 | this.delegate = delegate; 236 | } 237 | 238 | @Override 239 | public boolean test(Attempt attempt) { 240 | if (!attempt.hasResult()) { 241 | return false; 242 | } 243 | V result = attempt.getResult(); 244 | return delegate.test(result); 245 | } 246 | } 247 | 248 | private static final class ExceptionPredicate implements Predicate> { 249 | 250 | private final Predicate delegate; 251 | 252 | public ExceptionPredicate(Predicate delegate) { 253 | this.delegate = delegate; 254 | } 255 | 256 | @Override 257 | public boolean test(Attempt attempt) { 258 | if (!attempt.hasException()) { 259 | return false; 260 | } 261 | return delegate.test(attempt.getExceptionCause()); 262 | } 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /src/main/java/io/github/itning/retry/strategy/wait/WaitStrategies.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2015 Ray Holder 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.itning.retry.strategy.wait; 18 | 19 | import javax.annotation.Nonnull; 20 | import java.util.Arrays; 21 | import java.util.List; 22 | import java.util.Objects; 23 | import java.util.concurrent.TimeUnit; 24 | import java.util.function.Function; 25 | 26 | /** 27 | * Factory class for instances of {@link WaitStrategy}. 28 | * 29 | * @author JB 30 | */ 31 | public final class WaitStrategies { 32 | 33 | private static final WaitStrategy NO_WAIT_STRATEGY = new FixedWaitStrategy(0L); 34 | 35 | private WaitStrategies() { 36 | } 37 | 38 | /** 39 | * Returns a wait strategy that doesn't sleep at all before retrying. Use this at your own risk. 40 | * 41 | * @return a wait strategy that doesn't wait between retries 42 | */ 43 | public static WaitStrategy noWait() { 44 | return NO_WAIT_STRATEGY; 45 | } 46 | 47 | /** 48 | * Returns a wait strategy that sleeps a fixed amount of time before retrying. 49 | * 50 | * @param sleepTime the time to sleep 51 | * @param timeUnit the unit of the time to sleep 52 | * @return a wait strategy that sleeps a fixed amount of time 53 | * @throws IllegalStateException if the sleep time is < 0 54 | */ 55 | public static WaitStrategy fixedWait(long sleepTime, @Nonnull TimeUnit timeUnit) throws IllegalStateException { 56 | Objects.requireNonNull(timeUnit, "The time unit may not be null"); 57 | return new FixedWaitStrategy(timeUnit.toMillis(sleepTime)); 58 | } 59 | 60 | /** 61 | * Returns a strategy that sleeps a random amount of time before retrying. 62 | * 63 | * @param maximumTime the maximum time to sleep 64 | * @param timeUnit the unit of the maximum time 65 | * @return a wait strategy with a random wait time 66 | * @throws IllegalStateException if the maximum sleep time is <= 0. 67 | */ 68 | public static WaitStrategy randomWait(long maximumTime, @Nonnull TimeUnit timeUnit) { 69 | Objects.requireNonNull(timeUnit, "The time unit may not be null"); 70 | return new RandomWaitStrategy(0L, timeUnit.toMillis(maximumTime)); 71 | } 72 | 73 | /** 74 | * Returns a strategy that sleeps a random amount of time before retrying. 75 | * 76 | * @param minimumTime the minimum time to sleep 77 | * @param minimumTimeUnit the unit of the minimum time 78 | * @param maximumTime the maximum time to sleep 79 | * @param maximumTimeUnit the unit of the maximum time 80 | * @return a wait strategy with a random wait time 81 | * @throws IllegalStateException if the minimum sleep time is < 0, or if the 82 | * maximum sleep time is less than (or equals to) the minimum. 83 | */ 84 | public static WaitStrategy randomWait(long minimumTime, 85 | @Nonnull TimeUnit minimumTimeUnit, 86 | long maximumTime, 87 | @Nonnull TimeUnit maximumTimeUnit) { 88 | Objects.requireNonNull(minimumTimeUnit, "The minimum time unit may not be null"); 89 | Objects.requireNonNull(maximumTimeUnit, "The maximum time unit may not be null"); 90 | return new RandomWaitStrategy(minimumTimeUnit.toMillis(minimumTime), 91 | maximumTimeUnit.toMillis(maximumTime)); 92 | } 93 | 94 | /** 95 | * Returns a strategy that sleeps a fixed amount of time after the first 96 | * failed attempt and in incrementing amounts of time after each additional 97 | * failed attempt. 98 | * 99 | * @param initialSleepTime the time to sleep before retrying the first time 100 | * @param initialSleepTimeUnit the unit of the initial sleep time 101 | * @param increment the increment added to the previous sleep time after each failed attempt 102 | * @param incrementTimeUnit the unit of the increment 103 | * @return a wait strategy that incrementally sleeps an additional fixed time after each failed attempt 104 | */ 105 | public static WaitStrategy incrementingWait(long initialSleepTime, 106 | @Nonnull TimeUnit initialSleepTimeUnit, 107 | long increment, 108 | @Nonnull TimeUnit incrementTimeUnit) { 109 | Objects.requireNonNull(initialSleepTimeUnit, "The initial sleep time unit may not be null"); 110 | Objects.requireNonNull(incrementTimeUnit, "The increment time unit may not be null"); 111 | return new IncrementingWaitStrategy(initialSleepTimeUnit.toMillis(initialSleepTime), 112 | incrementTimeUnit.toMillis(increment)); 113 | } 114 | 115 | /** 116 | * Returns a strategy which sleeps for an exponential amount of time after the first failed attempt, 117 | * and in exponentially incrementing amounts after each failed attempt up to Long.MAX_VALUE. 118 | * 119 | * @return a wait strategy that increments with each failed attempt using exponential backoff 120 | */ 121 | public static WaitStrategy exponentialWait() { 122 | return new ExponentialWaitStrategy(1, Long.MAX_VALUE); 123 | } 124 | 125 | /** 126 | * Returns a strategy which sleeps for an exponential amount of time after the first failed attempt, 127 | * and in exponentially incrementing amounts after each failed attempt up to the maximumTime. 128 | * 129 | * @param maximumTime the maximum time to sleep 130 | * @param maximumTimeUnit the unit of the maximum time 131 | * @return a wait strategy that increments with each failed attempt using exponential backoff 132 | */ 133 | public static WaitStrategy exponentialWait(long maximumTime, 134 | @Nonnull TimeUnit maximumTimeUnit) { 135 | Objects.requireNonNull(maximumTimeUnit, "The maximum time unit may not be null"); 136 | return new ExponentialWaitStrategy(1, maximumTimeUnit.toMillis(maximumTime)); 137 | } 138 | 139 | /** 140 | * Returns a strategy which sleeps for an exponential amount of time after the first failed attempt, 141 | * and in exponentially incrementing amounts after each failed attempt up to the maximumTime. 142 | * The wait time between the retries can be controlled by the multiplier. 143 | * nextWaitTime = exponentialIncrement * {@code multiplier}. 144 | * 145 | * @param multiplier multiply the wait time calculated by this 146 | * @param maximumTime the maximum time to sleep 147 | * @param maximumTimeUnit the unit of the maximum time 148 | * @return a wait strategy that increments with each failed attempt using exponential backoff 149 | */ 150 | public static WaitStrategy exponentialWait(long multiplier, 151 | long maximumTime, 152 | @Nonnull TimeUnit maximumTimeUnit) { 153 | Objects.requireNonNull(maximumTimeUnit, "The maximum time unit may not be null"); 154 | return new ExponentialWaitStrategy(multiplier, maximumTimeUnit.toMillis(maximumTime)); 155 | } 156 | 157 | /** 158 | * Returns a strategy which sleeps for an increasing amount of time after the first failed attempt, 159 | * and in Fibonacci increments after each failed attempt up to {@link Long#MAX_VALUE}. 160 | * 161 | * @return a wait strategy that increments with each failed attempt using a Fibonacci sequence 162 | */ 163 | public static WaitStrategy fibonacciWait() { 164 | return new FibonacciWaitStrategy(1, Long.MAX_VALUE); 165 | } 166 | 167 | /** 168 | * Returns a strategy which sleeps for an increasing amount of time after the first failed attempt, 169 | * and in Fibonacci increments after each failed attempt up to the {@code maximumTime}. 170 | * 171 | * @param maximumTime the maximum time to sleep 172 | * @param maximumTimeUnit the unit of the maximum time 173 | * @return a wait strategy that increments with each failed attempt using a Fibonacci sequence 174 | */ 175 | public static WaitStrategy fibonacciWait(long maximumTime, 176 | @Nonnull TimeUnit maximumTimeUnit) { 177 | Objects.requireNonNull(maximumTimeUnit, "The maximum time unit may not be null"); 178 | return new FibonacciWaitStrategy(1, maximumTimeUnit.toMillis(maximumTime)); 179 | } 180 | 181 | /** 182 | * Returns a strategy which sleeps for an increasing amount of time after the first failed attempt, 183 | * and in Fibonacci increments after each failed attempt up to the {@code maximumTime}. 184 | * The wait time between the retries can be controlled by the multiplier. 185 | * nextWaitTime = fibonacciIncrement * {@code multiplier}. 186 | * 187 | * @param multiplier multiply the wait time calculated by this 188 | * @param maximumTime the maximum time to sleep 189 | * @param maximumTimeUnit the unit of the maximum time 190 | * @return a wait strategy that increments with each failed attempt using a Fibonacci sequence 191 | */ 192 | public static WaitStrategy fibonacciWait(long multiplier, 193 | long maximumTime, 194 | @Nonnull TimeUnit maximumTimeUnit) { 195 | Objects.requireNonNull(maximumTimeUnit, "The maximum time unit may not be null"); 196 | return new FibonacciWaitStrategy(multiplier, maximumTimeUnit.toMillis(maximumTime)); 197 | } 198 | 199 | /** 200 | * Returns a strategy which sleeps for an amount of time based on the Exception that occurred. The 201 | * {@code function} determines how the sleep time should be calculated for the given 202 | * {@code exceptionClass}. If the exception does not match, a wait time of 0 is returned. 203 | * 204 | * @param function function to calculate sleep time 205 | * @param exceptionClass class to calculate sleep time from 206 | * @return a wait strategy calculated from the failed attempt 207 | */ 208 | public static WaitStrategy exceptionWait(@Nonnull Class exceptionClass, 209 | @Nonnull Function function) { 210 | Objects.requireNonNull(exceptionClass, "exceptionClass may not be null"); 211 | Objects.requireNonNull(function, "function may not be null"); 212 | return new ExceptionWaitStrategy<>(exceptionClass, function); 213 | } 214 | 215 | /** 216 | * Joins one or more wait strategies to derive a composite wait strategy. 217 | * The new joined strategy will have a wait time which is total of all wait times computed one after another in order. 218 | * 219 | * @param waitStrategies Wait strategies that need to be applied one after another for computing the sleep time. 220 | * @return A composite wait strategy 221 | */ 222 | public static WaitStrategy join(WaitStrategy... waitStrategies) { 223 | if (waitStrategies.length <= 0) { 224 | throw new IllegalStateException("Must have at least one wait strategy"); 225 | } 226 | List waitStrategyList = Arrays.asList(waitStrategies); 227 | if (waitStrategyList.contains(null)) { 228 | throw new IllegalStateException("Cannot have a null wait strategy"); 229 | } 230 | return new CompositeWaitStrategy(waitStrategyList); 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /src/main/java/io/github/itning/retry/Retryer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2015 Ray Holder 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.itning.retry; 18 | 19 | import io.github.itning.retry.listener.RetryListener; 20 | import io.github.itning.retry.strategy.block.BlockStrategies; 21 | import io.github.itning.retry.strategy.block.BlockStrategy; 22 | import io.github.itning.retry.strategy.limit.AttemptTimeLimiter; 23 | import io.github.itning.retry.strategy.limit.AttemptTimeLimiters; 24 | import io.github.itning.retry.strategy.stop.StopStrategy; 25 | import io.github.itning.retry.strategy.wait.WaitStrategy; 26 | 27 | import javax.annotation.Nonnull; 28 | import javax.annotation.concurrent.Immutable; 29 | import java.util.ArrayList; 30 | import java.util.Collection; 31 | import java.util.Objects; 32 | import java.util.concurrent.Callable; 33 | import java.util.concurrent.ExecutionException; 34 | import java.util.concurrent.TimeUnit; 35 | import java.util.function.Predicate; 36 | 37 | /** 38 | * A retryer, which executes a call, and retries it until it succeeds, or 39 | * a stop strategy decides to stop retrying. A wait strategy is used to sleep 40 | * between attempts. The strategy to decide if the call succeeds or not is 41 | * also configurable. 42 | *

43 | * A retryer can also wrap the callable into a RetryerCallable, which can be submitted to an executor. 44 | *

45 | * Retryer instances are better constructed with a {@link RetryerBuilder}. A retryer 46 | * is thread-safe, provided the arguments passed to its constructor are thread-safe. 47 | * 48 | * @param the type of the call return value 49 | * @author JB 50 | * @author Jason Dunkelberger (dirkraft) 51 | */ 52 | public final class Retryer { 53 | private final StopStrategy stopStrategy; 54 | private final WaitStrategy waitStrategy; 55 | private final BlockStrategy blockStrategy; 56 | private final AttemptTimeLimiter attemptTimeLimiter; 57 | private final Predicate> rejectionPredicate; 58 | private final Collection> listeners; 59 | 60 | /** 61 | * Constructor 62 | * 63 | * @param stopStrategy the strategy used to decide when the retryer must stop retrying 64 | * @param waitStrategy the strategy used to decide how much time to sleep between attempts 65 | * @param rejectionPredicate the predicate used to decide if the attempt must be rejected 66 | * or not. If an attempt is rejected, the retryer will retry the call, unless the stop 67 | * strategy indicates otherwise or the thread is interrupted. 68 | */ 69 | public Retryer(@Nonnull StopStrategy stopStrategy, 70 | @Nonnull WaitStrategy waitStrategy, 71 | @Nonnull Predicate> rejectionPredicate) { 72 | 73 | this(AttemptTimeLimiters.noTimeLimit(), stopStrategy, waitStrategy, BlockStrategies.threadSleepStrategy(), rejectionPredicate); 74 | } 75 | 76 | /** 77 | * Constructor 78 | * 79 | * @param attemptTimeLimiter to prevent from any single attempt from spinning infinitely 80 | * @param stopStrategy the strategy used to decide when the retryer must stop retrying 81 | * @param waitStrategy the strategy used to decide how much time to sleep between attempts 82 | * @param rejectionPredicate the predicate used to decide if the attempt must be rejected 83 | * or not. If an attempt is rejected, the retryer will retry the call, unless the stop 84 | * strategy indicates otherwise or the thread is interrupted. 85 | */ 86 | public Retryer(@Nonnull AttemptTimeLimiter attemptTimeLimiter, 87 | @Nonnull StopStrategy stopStrategy, 88 | @Nonnull WaitStrategy waitStrategy, 89 | @Nonnull Predicate> rejectionPredicate) { 90 | this(attemptTimeLimiter, stopStrategy, waitStrategy, BlockStrategies.threadSleepStrategy(), rejectionPredicate); 91 | } 92 | 93 | /** 94 | * Constructor 95 | * 96 | * @param attemptTimeLimiter to prevent from any single attempt from spinning infinitely 97 | * @param stopStrategy the strategy used to decide when the retryer must stop retrying 98 | * @param waitStrategy the strategy used to decide how much time to sleep between attempts 99 | * @param blockStrategy the strategy used to decide how to block between retry attempts; eg, Thread#sleep(), latches, etc. 100 | * @param rejectionPredicate the predicate used to decide if the attempt must be rejected 101 | * or not. If an attempt is rejected, the retryer will retry the call, unless the stop 102 | * strategy indicates otherwise or the thread is interrupted. 103 | */ 104 | public Retryer(@Nonnull AttemptTimeLimiter attemptTimeLimiter, 105 | @Nonnull StopStrategy stopStrategy, 106 | @Nonnull WaitStrategy waitStrategy, 107 | @Nonnull BlockStrategy blockStrategy, 108 | @Nonnull Predicate> rejectionPredicate) { 109 | this(attemptTimeLimiter, stopStrategy, waitStrategy, blockStrategy, rejectionPredicate, new ArrayList<>()); 110 | } 111 | 112 | /** 113 | * Constructor 114 | * 115 | * @param attemptTimeLimiter to prevent from any single attempt from spinning infinitely 116 | * @param stopStrategy the strategy used to decide when the retryer must stop retrying 117 | * @param waitStrategy the strategy used to decide how much time to sleep between attempts 118 | * @param blockStrategy the strategy used to decide how to block between retry attempts; eg, Thread#sleep(), latches, etc. 119 | * @param rejectionPredicate the predicate used to decide if the attempt must be rejected 120 | * or not. If an attempt is rejected, the retryer will retry the call, unless the stop 121 | * strategy indicates otherwise or the thread is interrupted. 122 | * @param listeners collection of retry listeners 123 | */ 124 | public Retryer(@Nonnull AttemptTimeLimiter attemptTimeLimiter, 125 | @Nonnull StopStrategy stopStrategy, 126 | @Nonnull WaitStrategy waitStrategy, 127 | @Nonnull BlockStrategy blockStrategy, 128 | @Nonnull Predicate> rejectionPredicate, 129 | @Nonnull Collection> listeners) { 130 | Objects.requireNonNull(attemptTimeLimiter, "timeLimiter may not be null"); 131 | Objects.requireNonNull(stopStrategy, "stopStrategy may not be null"); 132 | Objects.requireNonNull(waitStrategy, "waitStrategy may not be null"); 133 | Objects.requireNonNull(blockStrategy, "blockStrategy may not be null"); 134 | Objects.requireNonNull(rejectionPredicate, "rejectionPredicate may not be null"); 135 | Objects.requireNonNull(listeners, "listeners may not null"); 136 | 137 | this.attemptTimeLimiter = attemptTimeLimiter; 138 | this.stopStrategy = stopStrategy; 139 | this.waitStrategy = waitStrategy; 140 | this.blockStrategy = blockStrategy; 141 | this.rejectionPredicate = rejectionPredicate; 142 | this.listeners = listeners; 143 | } 144 | 145 | /** 146 | * Executes the given callable. If the rejection predicate 147 | * accepts the attempt, the stop strategy is used to decide if a new attempt 148 | * must be made. Then the wait strategy is used to decide how much time to sleep 149 | * and a new attempt is made. 150 | * 151 | * @param callable the callable task to be executed 152 | * @return the computed result of the given callable 153 | * @throws ExecutionException if the given callable throws an exception, and the 154 | * rejection predicate considers the attempt as successful. The original exception 155 | * is wrapped into an ExecutionException. 156 | * @throws RetryException if all the attempts failed before the stop strategy decided 157 | * to abort, or the thread was interrupted. Note that if the thread is interrupted, 158 | * this exception is thrown and the thread's interrupt status is set. 159 | */ 160 | public V call(Callable callable) throws ExecutionException, RetryException { 161 | long startTime = System.nanoTime(); 162 | for (int attemptNumber = 1; ; attemptNumber++) { 163 | Attempt attempt; 164 | try { 165 | V result = attemptTimeLimiter.call(callable); 166 | attempt = new ResultAttempt<>(result, attemptNumber, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)); 167 | } catch (Throwable t) { 168 | attempt = new ExceptionAttempt<>(t, attemptNumber, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)); 169 | } 170 | 171 | if (!rejectionPredicate.test(attempt)) { 172 | return attempt.get(); 173 | } 174 | if (stopStrategy.shouldStop(attempt)) { 175 | throw new RetryException(attemptNumber, attempt); 176 | } else { 177 | 178 | for (RetryListener listener : listeners) { 179 | listener.onRetry(attempt); 180 | } 181 | 182 | long sleepTime = waitStrategy.computeSleepTime(attempt); 183 | try { 184 | blockStrategy.block(sleepTime); 185 | } catch (InterruptedException e) { 186 | Thread.currentThread().interrupt(); 187 | throw new RetryException(attemptNumber, attempt); 188 | } 189 | } 190 | } 191 | } 192 | 193 | /** 194 | * A call with no return value 195 | * 196 | * @param runnable the runnable task to be executed 197 | * @throws ExecutionException if the given callable throws an exception, and the 198 | * rejection predicate considers the attempt as successful. The original exception 199 | * is wrapped into an ExecutionException. 200 | * @throws RetryException if all the attempts failed before the stop strategy decided 201 | * to abort, or the thread was interrupted. Note that if the thread is interrupted, 202 | * this exception is thrown and the thread's interrupt status is set. 203 | * @see #call(Callable) 204 | */ 205 | public void call(Runnable runnable) throws ExecutionException, RetryException { 206 | this.call(() -> { 207 | runnable.run(); 208 | return null; 209 | }); 210 | } 211 | 212 | /** 213 | * Wraps the given {@link Callable} in a {@link RetryerCallable}, which can 214 | * be submitted to an executor. The returned {@link RetryerCallable} uses 215 | * this {@link Retryer} instance to call the given {@link Callable}. 216 | * 217 | * @param callable the callable to wrap 218 | * @return a {@link RetryerCallable} that behaves like the given {@link Callable} with retry behavior defined by this {@link Retryer} 219 | */ 220 | public RetryerCallable wrap(Callable callable) { 221 | return new RetryerCallable<>(this, callable); 222 | } 223 | 224 | @Immutable 225 | static final class ResultAttempt implements Attempt { 226 | private final R result; 227 | private final long attemptNumber; 228 | private final long delaySinceFirstAttempt; 229 | 230 | public ResultAttempt(R result, long attemptNumber, long delaySinceFirstAttempt) { 231 | this.result = result; 232 | this.attemptNumber = attemptNumber; 233 | this.delaySinceFirstAttempt = delaySinceFirstAttempt; 234 | } 235 | 236 | @Override 237 | public R get() throws ExecutionException { 238 | return result; 239 | } 240 | 241 | @Override 242 | public boolean hasResult() { 243 | return true; 244 | } 245 | 246 | @Override 247 | public boolean hasException() { 248 | return false; 249 | } 250 | 251 | @Override 252 | public R getResult() throws IllegalStateException { 253 | return result; 254 | } 255 | 256 | @Override 257 | public Throwable getExceptionCause() throws IllegalStateException { 258 | throw new IllegalStateException("The attempt resulted in a result, not in an exception"); 259 | } 260 | 261 | @Override 262 | public long getAttemptNumber() { 263 | return attemptNumber; 264 | } 265 | 266 | @Override 267 | public long getDelaySinceFirstAttempt() { 268 | return delaySinceFirstAttempt; 269 | } 270 | } 271 | 272 | @Immutable 273 | static final class ExceptionAttempt implements Attempt { 274 | private final ExecutionException e; 275 | private final long attemptNumber; 276 | private final long delaySinceFirstAttempt; 277 | 278 | public ExceptionAttempt(Throwable cause, long attemptNumber, long delaySinceFirstAttempt) { 279 | this.e = new ExecutionException(cause); 280 | this.attemptNumber = attemptNumber; 281 | this.delaySinceFirstAttempt = delaySinceFirstAttempt; 282 | } 283 | 284 | @Override 285 | public R get() throws ExecutionException { 286 | throw e; 287 | } 288 | 289 | @Override 290 | public boolean hasResult() { 291 | return false; 292 | } 293 | 294 | @Override 295 | public boolean hasException() { 296 | return true; 297 | } 298 | 299 | @Override 300 | public R getResult() throws IllegalStateException { 301 | throw new IllegalStateException("The attempt resulted in an exception, not in a result"); 302 | } 303 | 304 | @Override 305 | public Throwable getExceptionCause() throws IllegalStateException { 306 | return e.getCause(); 307 | } 308 | 309 | @Override 310 | public long getAttemptNumber() { 311 | return attemptNumber; 312 | } 313 | 314 | @Override 315 | public long getDelaySinceFirstAttempt() { 316 | return delaySinceFirstAttempt; 317 | } 318 | } 319 | 320 | /** 321 | * A {@link Callable} which wraps another {@link Callable} in order to add 322 | * retrying behavior from a given {@link Retryer} instance. 323 | * 324 | * @author JB 325 | */ 326 | public static class RetryerCallable implements Callable { 327 | private final Retryer retryer; 328 | private final Callable callable; 329 | 330 | private RetryerCallable(Retryer retryer, 331 | Callable callable) { 332 | this.retryer = retryer; 333 | this.callable = callable; 334 | } 335 | 336 | /** 337 | * Makes the enclosing retryer call the wrapped callable. 338 | * 339 | * @see Retryer#call(Callable) 340 | */ 341 | @Override 342 | public X call() throws ExecutionException, RetryException { 343 | return retryer.call(callable); 344 | } 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /src/test/java/io/github/itning/retry/RetryerBuilderTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2015 Ray Holder 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.itning.retry; 18 | 19 | import io.github.itning.retry.Retryer.RetryerCallable; 20 | import com.google.common.base.Predicate; 21 | import com.google.common.base.Predicates; 22 | import io.github.itning.retry.listener.RetryListener; 23 | import io.github.itning.retry.strategy.block.BlockStrategy; 24 | import io.github.itning.retry.strategy.stop.StopStrategies; 25 | import io.github.itning.retry.strategy.wait.WaitStrategies; 26 | import org.junit.jupiter.api.Test; 27 | 28 | import java.io.IOException; 29 | import java.util.HashMap; 30 | import java.util.Map; 31 | import java.util.Objects; 32 | import java.util.concurrent.Callable; 33 | import java.util.concurrent.CountDownLatch; 34 | import java.util.concurrent.ExecutionException; 35 | import java.util.concurrent.TimeUnit; 36 | import java.util.concurrent.atomic.AtomicBoolean; 37 | import java.util.concurrent.atomic.AtomicInteger; 38 | 39 | import static org.junit.jupiter.api.Assertions.*; 40 | 41 | 42 | public class RetryerBuilderTest { 43 | 44 | @Test 45 | public void testWithWaitStrategy() throws ExecutionException, RetryException { 46 | Callable callable = notNullAfter5Attempts(); 47 | Retryer retryer = RetryerBuilder.newBuilder() 48 | .withWaitStrategy(WaitStrategies.fixedWait(50L, TimeUnit.MILLISECONDS)) 49 | .retryIfResult(Predicates.isNull()) 50 | .build(); 51 | long start = System.currentTimeMillis(); 52 | boolean result = retryer.call(callable); 53 | assertTrue(System.currentTimeMillis() - start >= 250L); 54 | assertTrue(result); 55 | } 56 | 57 | @Test 58 | public void testWithMoreThanOneWaitStrategyOneBeingFixed() throws ExecutionException, RetryException { 59 | Callable callable = notNullAfter5Attempts(); 60 | Retryer retryer = RetryerBuilder.newBuilder() 61 | .withWaitStrategy(WaitStrategies.join( 62 | WaitStrategies.fixedWait(50L, TimeUnit.MILLISECONDS), 63 | WaitStrategies.fibonacciWait(10, Long.MAX_VALUE, TimeUnit.MILLISECONDS))) 64 | .retryIfResult(Predicates.isNull()) 65 | .build(); 66 | long start = System.currentTimeMillis(); 67 | boolean result = retryer.call(callable); 68 | assertTrue(System.currentTimeMillis() - start >= 370L); 69 | assertTrue(result); 70 | } 71 | 72 | @Test 73 | public void testWithMoreThanOneWaitStrategyOneBeingIncremental() throws ExecutionException, RetryException { 74 | Callable callable = notNullAfter5Attempts(); 75 | Retryer retryer = RetryerBuilder.newBuilder() 76 | .withWaitStrategy(WaitStrategies.join( 77 | WaitStrategies.incrementingWait(10L, TimeUnit.MILLISECONDS, 10L, TimeUnit.MILLISECONDS), 78 | WaitStrategies.fibonacciWait(10, Long.MAX_VALUE, TimeUnit.MILLISECONDS))) 79 | .retryIfResult(Predicates.isNull()) 80 | .build(); 81 | long start = System.currentTimeMillis(); 82 | boolean result = retryer.call(callable); 83 | assertTrue(System.currentTimeMillis() - start >= 270L); 84 | assertTrue(result); 85 | } 86 | 87 | private Callable notNullAfter5Attempts() { 88 | return new Callable() { 89 | int counter = 0; 90 | 91 | @Override 92 | public Boolean call() throws Exception { 93 | if (counter < 5) { 94 | counter++; 95 | return null; 96 | } 97 | return true; 98 | } 99 | }; 100 | } 101 | 102 | @Test 103 | public void testWithStopStrategy() throws ExecutionException { 104 | Callable callable = notNullAfter5Attempts(); 105 | Retryer retryer = RetryerBuilder.newBuilder() 106 | .withStopStrategy(StopStrategies.stopAfterAttempt(3)) 107 | .retryIfResult(Predicates.isNull()) 108 | .build(); 109 | try { 110 | retryer.call(callable); 111 | fail("RetryException expected"); 112 | } catch (RetryException e) { 113 | assertEquals(3, e.getNumberOfFailedAttempts()); 114 | } 115 | } 116 | 117 | @Test 118 | public void testWithBlockStrategy() throws ExecutionException, RetryException { 119 | Callable callable = notNullAfter5Attempts(); 120 | final AtomicInteger counter = new AtomicInteger(); 121 | BlockStrategy blockStrategy = new BlockStrategy() { 122 | @Override 123 | public void block(long sleepTime) throws InterruptedException { 124 | counter.incrementAndGet(); 125 | } 126 | }; 127 | 128 | Retryer retryer = RetryerBuilder.newBuilder() 129 | .withBlockStrategy(blockStrategy) 130 | .retryIfResult(Predicates.isNull()) 131 | .build(); 132 | final int retryCount = 5; 133 | boolean result = retryer.call(callable); 134 | assertTrue(result); 135 | assertEquals(counter.get(), retryCount); 136 | } 137 | 138 | @Test 139 | public void testRetryIfException() throws ExecutionException, RetryException { 140 | Callable callable = noIOExceptionAfter5Attempts(); 141 | Retryer retryer = RetryerBuilder.newBuilder() 142 | .retryIfException() 143 | .build(); 144 | boolean result = retryer.call(callable); 145 | assertTrue(result); 146 | 147 | callable = noIOExceptionAfter5Attempts(); 148 | retryer = RetryerBuilder.newBuilder() 149 | .retryIfException() 150 | .withStopStrategy(StopStrategies.stopAfterAttempt(3)) 151 | .build(); 152 | try { 153 | retryer.call(callable); 154 | fail("RetryException expected"); 155 | } catch (RetryException e) { 156 | assertEquals(3, e.getNumberOfFailedAttempts()); 157 | assertTrue(e.getLastFailedAttempt().hasException()); 158 | assertTrue(e.getLastFailedAttempt().getExceptionCause() instanceof IOException); 159 | assertTrue(e.getCause() instanceof IOException); 160 | } 161 | 162 | callable = noIllegalStateExceptionAfter5Attempts(); 163 | retryer = RetryerBuilder.newBuilder() 164 | .retryIfException() 165 | .withStopStrategy(StopStrategies.stopAfterAttempt(3)) 166 | .build(); 167 | try { 168 | retryer.call(callable); 169 | fail("RetryException expected"); 170 | } catch (RetryException e) { 171 | assertEquals(3, e.getNumberOfFailedAttempts()); 172 | assertTrue(e.getLastFailedAttempt().hasException()); 173 | assertTrue(e.getLastFailedAttempt().getExceptionCause() instanceof IllegalStateException); 174 | assertTrue(e.getCause() instanceof IllegalStateException); 175 | } 176 | } 177 | 178 | private Callable noIllegalStateExceptionAfter5Attempts() { 179 | return new Callable() { 180 | int counter = 0; 181 | 182 | @Override 183 | public Boolean call() throws Exception { 184 | if (counter < 5) { 185 | counter++; 186 | throw new IllegalStateException(); 187 | } 188 | return true; 189 | } 190 | }; 191 | } 192 | 193 | private Callable noIOExceptionAfter5Attempts() { 194 | return new Callable() { 195 | int counter = 0; 196 | 197 | @Override 198 | public Boolean call() throws IOException { 199 | if (counter < 5) { 200 | counter++; 201 | throw new IOException(); 202 | } 203 | return true; 204 | } 205 | }; 206 | } 207 | 208 | @Test 209 | public void testRetryIfRuntimeException() throws ExecutionException, RetryException { 210 | Callable callable = noIOExceptionAfter5Attempts(); 211 | Retryer retryer = RetryerBuilder.newBuilder() 212 | .retryIfRuntimeException() 213 | .build(); 214 | try { 215 | retryer.call(callable); 216 | fail("ExecutionException expected"); 217 | } catch (ExecutionException e) { 218 | assertTrue(e.getCause() instanceof IOException); 219 | } 220 | 221 | callable = noIllegalStateExceptionAfter5Attempts(); 222 | assertTrue(retryer.call(callable)); 223 | 224 | callable = noIllegalStateExceptionAfter5Attempts(); 225 | retryer = RetryerBuilder.newBuilder() 226 | .retryIfRuntimeException() 227 | .withStopStrategy(StopStrategies.stopAfterAttempt(3)) 228 | .build(); 229 | try { 230 | retryer.call(callable); 231 | fail("RetryException expected"); 232 | } catch (RetryException e) { 233 | assertEquals(3, e.getNumberOfFailedAttempts()); 234 | assertTrue(e.getLastFailedAttempt().hasException()); 235 | assertTrue(e.getLastFailedAttempt().getExceptionCause() instanceof IllegalStateException); 236 | assertTrue(e.getCause() instanceof IllegalStateException); 237 | } 238 | } 239 | 240 | @Test 241 | public void testRetryIfExceptionOfType() throws RetryException, ExecutionException { 242 | Callable callable = noIOExceptionAfter5Attempts(); 243 | Retryer retryer = RetryerBuilder.newBuilder() 244 | .retryIfExceptionOfType(IOException.class) 245 | .build(); 246 | assertTrue(retryer.call(callable)); 247 | 248 | callable = noIllegalStateExceptionAfter5Attempts(); 249 | try { 250 | retryer.call(callable); 251 | fail("ExecutionException expected"); 252 | } catch (ExecutionException e) { 253 | assertTrue(e.getCause() instanceof IllegalStateException); 254 | } 255 | 256 | callable = noIOExceptionAfter5Attempts(); 257 | retryer = RetryerBuilder.newBuilder() 258 | .retryIfExceptionOfType(IOException.class) 259 | .withStopStrategy(StopStrategies.stopAfterAttempt(3)) 260 | .build(); 261 | try { 262 | retryer.call(callable); 263 | fail("RetryException expected"); 264 | } catch (RetryException e) { 265 | assertEquals(3, e.getNumberOfFailedAttempts()); 266 | assertTrue(e.getLastFailedAttempt().hasException()); 267 | assertTrue(e.getLastFailedAttempt().getExceptionCause() instanceof IOException); 268 | assertTrue(e.getCause() instanceof IOException); 269 | } 270 | } 271 | 272 | @Test 273 | public void testRetryIfExceptionWithPredicate() throws RetryException, ExecutionException { 274 | Callable callable = noIOExceptionAfter5Attempts(); 275 | Retryer retryer = RetryerBuilder.newBuilder() 276 | .retryIfException(new Predicate() { 277 | @Override 278 | public boolean apply(Throwable t) { 279 | return t instanceof IOException; 280 | } 281 | }) 282 | .build(); 283 | assertTrue(retryer.call(callable)); 284 | 285 | callable = noIllegalStateExceptionAfter5Attempts(); 286 | try { 287 | retryer.call(callable); 288 | fail("ExecutionException expected"); 289 | } catch (ExecutionException e) { 290 | assertTrue(e.getCause() instanceof IllegalStateException); 291 | } 292 | 293 | callable = noIOExceptionAfter5Attempts(); 294 | retryer = RetryerBuilder.newBuilder() 295 | .retryIfException(new Predicate() { 296 | @Override 297 | public boolean apply(Throwable t) { 298 | return t instanceof IOException; 299 | } 300 | }) 301 | .withStopStrategy(StopStrategies.stopAfterAttempt(3)) 302 | .build(); 303 | try { 304 | retryer.call(callable); 305 | fail("RetryException expected"); 306 | } catch (RetryException e) { 307 | assertEquals(3, e.getNumberOfFailedAttempts()); 308 | assertTrue(e.getLastFailedAttempt().hasException()); 309 | assertTrue(e.getLastFailedAttempt().getExceptionCause() instanceof IOException); 310 | assertTrue(e.getCause() instanceof IOException); 311 | } 312 | } 313 | 314 | @Test 315 | public void testRetryIfResult() throws ExecutionException, RetryException { 316 | Callable callable = notNullAfter5Attempts(); 317 | Retryer retryer = RetryerBuilder.newBuilder() 318 | .retryIfResult(Predicates.isNull()) 319 | .build(); 320 | assertTrue(retryer.call(callable)); 321 | 322 | callable = notNullAfter5Attempts(); 323 | retryer = RetryerBuilder.newBuilder() 324 | .retryIfResult(Predicates.isNull()) 325 | .withStopStrategy(StopStrategies.stopAfterAttempt(3)) 326 | .build(); 327 | try { 328 | retryer.call(callable); 329 | fail("RetryException expected"); 330 | } catch (RetryException e) { 331 | assertEquals(3, e.getNumberOfFailedAttempts()); 332 | assertTrue(e.getLastFailedAttempt().hasResult()); 333 | assertNull(e.getLastFailedAttempt().getResult()); 334 | assertNull(e.getCause()); 335 | } 336 | } 337 | 338 | @Test 339 | public void testMultipleRetryConditions() throws ExecutionException, RetryException { 340 | Callable callable = notNullResultOrIOExceptionOrRuntimeExceptionAfter5Attempts(); 341 | Retryer retryer = RetryerBuilder.newBuilder() 342 | .retryIfResult(Predicates.isNull()) 343 | .retryIfExceptionOfType(IOException.class) 344 | .retryIfRuntimeException() 345 | .withStopStrategy(StopStrategies.stopAfterAttempt(3)) 346 | .build(); 347 | try { 348 | retryer.call(callable); 349 | fail("RetryException expected"); 350 | } catch (RetryException e) { 351 | assertTrue(e.getLastFailedAttempt().hasException()); 352 | assertTrue(e.getLastFailedAttempt().getExceptionCause() instanceof IllegalStateException); 353 | assertTrue(e.getCause() instanceof IllegalStateException); 354 | } 355 | 356 | callable = notNullResultOrIOExceptionOrRuntimeExceptionAfter5Attempts(); 357 | retryer = RetryerBuilder.newBuilder() 358 | .retryIfResult(Predicates.isNull()) 359 | .retryIfExceptionOfType(IOException.class) 360 | .retryIfRuntimeException() 361 | .build(); 362 | assertTrue(retryer.call(callable)); 363 | } 364 | 365 | private Callable notNullResultOrIOExceptionOrRuntimeExceptionAfter5Attempts() { 366 | return new Callable() { 367 | int counter = 0; 368 | 369 | @Override 370 | public Boolean call() throws IOException { 371 | if (counter < 1) { 372 | counter++; 373 | return null; 374 | } else if (counter < 2) { 375 | counter++; 376 | throw new IOException(); 377 | } else if (counter < 5) { 378 | counter++; 379 | throw new IllegalStateException(); 380 | } 381 | return true; 382 | } 383 | }; 384 | } 385 | 386 | @Test 387 | public void testInterruption() throws InterruptedException, ExecutionException { 388 | final AtomicBoolean result = new AtomicBoolean(false); 389 | final CountDownLatch latch = new CountDownLatch(1); 390 | Runnable r = new Runnable() { 391 | @Override 392 | public void run() { 393 | Retryer retryer = RetryerBuilder.newBuilder() 394 | .withWaitStrategy(WaitStrategies.fixedWait(1000L, TimeUnit.MILLISECONDS)) 395 | .retryIfResult(Predicates.isNull()) 396 | .build(); 397 | try { 398 | retryer.call(alwaysNull(latch)); 399 | fail("RetryException expected"); 400 | } catch (RetryException e) { 401 | assertFalse(e.getLastFailedAttempt().hasException()); 402 | assertNull(e.getCause()); 403 | assertTrue(Thread.currentThread().isInterrupted()); 404 | result.set(true); 405 | } catch (ExecutionException e) { 406 | fail("RetryException expected"); 407 | } 408 | } 409 | }; 410 | Thread t = new Thread(r); 411 | t.start(); 412 | latch.countDown(); 413 | t.interrupt(); 414 | t.join(); 415 | assertTrue(result.get()); 416 | } 417 | 418 | @Test 419 | public void testWrap() throws ExecutionException, RetryException { 420 | Callable callable = notNullAfter5Attempts(); 421 | Retryer retryer = RetryerBuilder.newBuilder() 422 | .retryIfResult(Predicates.isNull()) 423 | .build(); 424 | RetryerCallable wrapped = retryer.wrap(callable); 425 | assertTrue(wrapped.call()); 426 | } 427 | 428 | @Test 429 | public void testWhetherBuilderFailsForNullStopStrategy() { 430 | try { 431 | RetryerBuilder.newBuilder() 432 | .withStopStrategy(null) 433 | .build(); 434 | fail("Exepcted to fail for null stop strategy"); 435 | } catch (NullPointerException exception) { 436 | assertTrue(exception.getMessage().contains("stopStrategy may not be null")); 437 | } 438 | } 439 | 440 | @Test 441 | public void testWhetherBuilderFailsForNullWaitStrategy() { 442 | try { 443 | RetryerBuilder.newBuilder() 444 | .withWaitStrategy(null) 445 | .build(); 446 | fail("Exepcted to fail for null wait strategy"); 447 | } catch (NullPointerException exception) { 448 | assertTrue(exception.getMessage().contains("waitStrategy may not be null")); 449 | } 450 | } 451 | 452 | @Test 453 | public void testWhetherBuilderFailsForNullWaitStrategyWithCompositeStrategies() { 454 | try { 455 | RetryerBuilder.newBuilder() 456 | .withWaitStrategy(WaitStrategies.join(null, null)) 457 | .build(); 458 | fail("Exepcted to fail for null wait strategy"); 459 | } catch (IllegalStateException exception) { 460 | assertTrue(exception.getMessage().contains("Cannot have a null wait strategy")); 461 | } 462 | } 463 | 464 | @Test 465 | public void testRetryListener_SuccessfulAttempt() throws Exception { 466 | final Map attempts = new HashMap(); 467 | 468 | RetryListener listener = new RetryListener() { 469 | @Override 470 | public void onRetry(Attempt attempt) { 471 | attempts.put(attempt.getAttemptNumber(), attempt); 472 | } 473 | }; 474 | 475 | Callable callable = notNullAfter5Attempts(); 476 | 477 | Retryer retryer = RetryerBuilder.newBuilder() 478 | .retryIfResult(Objects::isNull) 479 | .withRetryListener(listener) 480 | .build(); 481 | assertTrue(retryer.call(callable)); 482 | 483 | assertEquals(5, attempts.size()); 484 | 485 | assertResultAttempt(attempts.get(1L), true, null); 486 | assertResultAttempt(attempts.get(2L), true, null); 487 | assertResultAttempt(attempts.get(3L), true, null); 488 | assertResultAttempt(attempts.get(4L), true, null); 489 | assertResultAttempt(attempts.get(5L), true, null); 490 | } 491 | 492 | @Test 493 | public void testRetryListener_WithException() throws Exception { 494 | final Map attempts = new HashMap<>(); 495 | 496 | RetryListener listener = new RetryListener() { 497 | @Override 498 | public void onRetry(Attempt attempt) { 499 | attempts.put(attempt.getAttemptNumber(), attempt); 500 | } 501 | }; 502 | 503 | Callable callable = noIOExceptionAfter5Attempts(); 504 | 505 | Retryer retryer = RetryerBuilder.newBuilder() 506 | .retryIfResult(Objects::isNull) 507 | .retryIfException() 508 | .withRetryListener(listener) 509 | .build(); 510 | assertTrue(retryer.call(callable)); 511 | 512 | assertEquals(5, attempts.size()); 513 | 514 | assertExceptionAttempt(attempts.get(1L), true, IOException.class); 515 | assertExceptionAttempt(attempts.get(2L), true, IOException.class); 516 | assertExceptionAttempt(attempts.get(3L), true, IOException.class); 517 | assertExceptionAttempt(attempts.get(4L), true, IOException.class); 518 | assertExceptionAttempt(attempts.get(5L), true, IOException.class); 519 | } 520 | 521 | @Test 522 | public void testMultipleRetryListeners() throws Exception { 523 | Callable callable = () -> true; 524 | 525 | final AtomicBoolean listenerOne = new AtomicBoolean(false); 526 | final AtomicBoolean listenerTwo = new AtomicBoolean(false); 527 | 528 | Retryer retryer = RetryerBuilder.newBuilder() 529 | .withRetryListener(attempt -> listenerOne.set(true)) 530 | .withRetryListener(attempt -> listenerTwo.set(true)) 531 | .build(); 532 | 533 | assertTrue(retryer.call(callable)); 534 | assertFalse(listenerOne.get()); 535 | assertFalse(listenerTwo.get()); 536 | } 537 | 538 | private void assertResultAttempt(Attempt actualAttempt, boolean expectedHasResult, Object expectedResult) { 539 | assertFalse(actualAttempt.hasException()); 540 | assertEquals(expectedHasResult, actualAttempt.hasResult()); 541 | assertEquals(expectedResult, actualAttempt.getResult()); 542 | } 543 | 544 | private void assertExceptionAttempt(Attempt actualAttempt, boolean expectedHasException, Class expectedExceptionClass) { 545 | assertFalse(actualAttempt.hasResult()); 546 | assertEquals(expectedHasException, actualAttempt.hasException()); 547 | assertTrue(expectedExceptionClass.isInstance(actualAttempt.getExceptionCause())); 548 | } 549 | 550 | private Callable alwaysNull(final CountDownLatch latch) { 551 | return new Callable() { 552 | @Override 553 | public Boolean call() throws Exception { 554 | latch.countDown(); 555 | return null; 556 | } 557 | }; 558 | } 559 | } 560 | --------------------------------------------------------------------------------