/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package net.jodah.failsafe;
import static net.jodah.failsafe.Asserts.assertThrows;
import static net.jodah.failsafe.Testing.failures;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertEquals;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.testng.annotations.Test;
import net.jodah.concurrentunit.Waiter;
import net.jodah.failsafe.function.CheckedBiFunction;
import net.jodah.failsafe.function.CheckedRunnable;
@Test
public abstract class AbstractFailsafeTest {
RetryPolicy retryAlways = new RetryPolicy();
RetryPolicy retryNever = new RetryPolicy().withMaxRetries(0);
RetryPolicy retryTwice = new RetryPolicy().withMaxRetries(2);
Service service = mock(Service.class);
AtomicInteger counter;
public static class ConnectException extends RuntimeException {
}
public interface Service {
boolean connect();
boolean disconnect();
}
public interface FastService extends Service {
}
abstract ScheduledExecutorService getExecutor();
/**
* Does a failsafe get with an optional executor.
*/
<T> T failsafeGet(RetryPolicy retryPolicy, Callable<T> callable) throws ExecutionException, InterruptedException {
ScheduledExecutorService executor = getExecutor();
return unwrapExceptions(() -> executor == null ? (T) Failsafe.with(retryPolicy).get(callable)
: (T) Failsafe.with(retryPolicy).with(executor).get(callable).get());
}
/**
* Does a failsafe get with an optional executor.
*/
<T> T failsafeGet(CircuitBreaker circuitBreaker, Callable<T> callable) throws ExecutionException, InterruptedException {
ScheduledExecutorService executor = getExecutor();
return unwrapExceptions(() -> executor == null ? (T) Failsafe.with(circuitBreaker).get(callable)
: (T) Failsafe.with(circuitBreaker).with(executor).get(callable).get());
}
/**
* Does a failsafe run with an optional executor.
*/
void failsafeRun(CircuitBreaker breaker, CheckedRunnable runnable) throws ExecutionException, InterruptedException {
ScheduledExecutorService executor = getExecutor();
if (executor == null)
Failsafe.with(breaker).run(runnable);
else
Failsafe.with(breaker).with(executor).run(runnable);
}
/**
* Does a failsafe get with an optional executor.
*/
<T> T failsafeGet(CircuitBreaker breaker, CheckedBiFunction<T, Throwable, T> fallback, Callable<T> callable)
throws ExecutionException, InterruptedException {
ScheduledExecutorService executor = getExecutor();
return unwrapExceptions(() -> executor == null ? (T) Failsafe.with(breaker).withFallback(fallback).get(callable)
: (T) Failsafe.with(breaker).with(executor).withFallback(fallback).get(callable).get());
}
/**
* Does a failsafe get with an optional executor.
*/
<T> T failsafeGet(RetryPolicy retryPolicy, CheckedBiFunction<T, Throwable, T> fallback, Callable<T> callable)
throws ExecutionException, InterruptedException {
ScheduledExecutorService executor = getExecutor();
return unwrapExceptions(() -> executor == null ? (T) Failsafe.with(retryPolicy).withFallback(fallback).get(callable)
: (T) Failsafe.with(retryPolicy).with(executor).withFallback(fallback).get(callable).get());
}
/**
* Asserts that retries are not attempted after a successful execution.
*/
public void shouldSucceedWithoutRetries() throws Throwable {
// Given retries not allowed
reset(service);
when(service.connect()).thenReturn(false);
// When / Then
assertEquals(failsafeGet(retryNever, service::connect), Boolean.FALSE);
verify(service).connect();
}
/**
* Asserts that retries are performed then a non-retryable failure is thrown.
*/
@SuppressWarnings("unchecked")
public void shouldThrowOnNonRetriableFailure() throws Throwable {
// Given
when(service.connect()).thenThrow(ConnectException.class, ConnectException.class, IllegalStateException.class);
RetryPolicy retryPolicy = new RetryPolicy().retryOn(ConnectException.class);
// When / Then
assertThrows(() -> failsafeGet(retryPolicy, service::connect), IllegalStateException.class);
verify(service, times(3)).connect();
}
/**
* Should throw CircuitBreakerOpenException when max half-open executions are occurring.
*/
public void shouldRejectExcessiveExecutionsThroughHalfOpenCircuit() throws Throwable {
// Given
CircuitBreaker breaker = new CircuitBreaker().withSuccessThreshold(3);
breaker.halfOpen();
Waiter waiter = new Waiter();
for (int i = 0; i < 3; i++)
Testing.runInThread(() -> failsafeRun(breaker, () -> {
waiter.resume();
Thread.sleep(1000);
}));
// When / Then
waiter.await(10000, 3);
for (int i = 0; i < 5; i++)
assertThrows(() -> failsafeGet(breaker, () -> null), CircuitBreakerOpenException.class);
}
/**
* Asserts that fallback works as expected after retries.
*/
public void shouldFallbackAfterFailureWithRetries() throws Throwable {
// Given
RetryPolicy retryPolicy = new RetryPolicy().withMaxRetries(2);
Exception failure = new ConnectException();
when(service.connect()).thenThrow(failures(3, failure));
Waiter waiter = new Waiter();
// When / Then
assertEquals(failsafeGet(retryPolicy, (r, f) -> {
waiter.assertNull(r);
waiter.assertEquals(failure, f);
return false;
}, () -> service.connect()), Boolean.FALSE);
verify(service, times(3)).connect();
// Given
reset(service);
when(service.connect()).thenThrow(failures(3, failure));
// When / Then
assertThrows(() -> failsafeGet(retryPolicy, (r, f) -> {
waiter.assertNull(r);
waiter.assertEquals(failure, f);
throw new RuntimeException(f);
}, () -> service.connect()), RuntimeException.class, ConnectException.class);
verify(service, times(3)).connect();
}
/**
* Asserts that fallback works after a failure with a breaker configured.
*/
public void shouldFallbackAfterFailureWithCircuitBreaker() throws Throwable {
// Given
CircuitBreaker breaker = new CircuitBreaker().withSuccessThreshold(3).withDelay(1, TimeUnit.MINUTES);
Exception failure = new ConnectException();
when(service.connect()).thenThrow(failure);
Waiter waiter = new Waiter();
// When / Then
assertEquals(failsafeGet(breaker, (r, f) -> {
waiter.assertNull(r);
waiter.assertEquals(failure, f);
return false;
}, () -> service.connect()), Boolean.FALSE);
verify(service).connect();
// Given
reset(service);
breaker.close();
when(service.connect()).thenThrow(failure);
// When / Then
assertThrows(() -> failsafeGet(breaker, (r, f) -> {
waiter.assertNull(r);
waiter.assertEquals(failure, f);
throw new RuntimeException(f);
}, () -> service.connect()), RuntimeException.class, ConnectException.class);
verify(service).connect();
}
/**
* Asserts that fallback works when a circuit breaker is open.
*/
public void shouldFallbackWhenCircuitBreakerIsOpen() throws Throwable {
// Given
CircuitBreaker breaker = new CircuitBreaker().withSuccessThreshold(3).withDelay(1, TimeUnit.MINUTES);
breaker.open();
Exception failure = new ConnectException();
when(service.connect()).thenThrow(failure);
Waiter waiter = new Waiter();
// When / Then
assertEquals(failsafeGet(breaker, (r, f) -> {
waiter.assertNull(r);
waiter.assertTrue(f instanceof CircuitBreakerOpenException);
return false;
}, service::connect), Boolean.FALSE);
verify(service, times(0)).connect();
}
private <T> T unwrapExceptions(Callable<T> callable) {
try {
return callable.call();
} catch (ExecutionException e) {
throw (RuntimeException) e.getCause();
} catch (FailsafeException e) {
RuntimeException cause = (RuntimeException) e.getCause();
throw cause == null ? e : cause;
} catch (Exception e) {
throw (RuntimeException) e;
}
}
}