/*
* 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 net.jodah.failsafe.Testing.ignoreExceptions;
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 static org.testng.Assert.assertNull;
import static org.testng.Assert.assertTrue;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import net.jodah.failsafe.function.CheckedRunnable;
import net.jodah.failsafe.function.ContextualCallable;
import net.jodah.failsafe.function.ContextualRunnable;
@Test
public class SyncFailsafeTest extends AbstractFailsafeTest {
// Results from a synchronous Failsafe call
private @SuppressWarnings("unchecked") Class<? extends Throwable>[] syncThrowables = new Class[] {
ConnectException.class };
// Results from a get against a future that wraps a synchronous Failsafe call
private @SuppressWarnings("unchecked") Class<? extends Throwable>[] futureSyncThrowables = new Class[] {
ExecutionException.class, ConnectException.class };
@BeforeMethod
protected void beforeMethod() {
reset(service);
counter = new AtomicInteger();
}
@Override
ScheduledExecutorService getExecutor() {
return null;
}
private void assertRun(Object runnable) throws Throwable {
// Given - Fail twice then succeed
when(service.connect()).thenThrow(failures(2, new ConnectException())).thenReturn(true);
// When
run(Failsafe.with(retryAlways), runnable);
// Then
verify(service, times(3)).connect();
// Given - Fail three times
reset(service);
counter.set(0);
when(service.connect()).thenThrow(failures(10, new ConnectException()));
// When / Then
assertThrows(() -> {
run(Failsafe.with(retryTwice), runnable);
}, syncThrowables);
verify(service, times(3)).connect();
}
public void shouldRun() throws Throwable {
assertRun((CheckedRunnable) () -> service.connect());
}
public void shouldRunContextual() throws Throwable {
assertRun((ContextualRunnable) context -> {
assertEquals(context.getExecutions(), counter.getAndIncrement());
service.connect();
});
}
private void assertGet(Object callable) throws Throwable {
// Given - Fail twice then succeed
when(service.connect()).thenThrow(failures(2, new ConnectException())).thenReturn(false, false, true);
RetryPolicy retryPolicy = new RetryPolicy().retryWhen(false);
assertEquals(get(Failsafe.with(retryPolicy), callable), Boolean.TRUE);
verify(service, times(5)).connect();
// Given - Fail three times
reset(service);
counter.set(0);
when(service.connect()).thenThrow(failures(10, new ConnectException()));
// When / Then
assertThrows(() -> get(Failsafe.with(retryTwice), callable), syncThrowables);
verify(service, times(3)).connect();
}
public void shouldGet() throws Throwable {
assertGet((Callable<Boolean>) () -> service.connect());
}
public void shouldGetContextual() throws Throwable {
assertGet((ContextualCallable<Boolean>) context -> {
assertEquals(context.getExecutions(), counter.getAndIncrement());
return service.connect();
});
}
public void testPerStageRetries() throws Throwable {
// Given - Fail twice then succeed
when(service.connect()).thenThrow(failures(2, new ConnectException())).thenReturn(false, true);
when(service.disconnect()).thenThrow(failures(2, new ConnectException())).thenReturn(false, true);
RetryPolicy retryPolicy = new RetryPolicy().retryWhen(false);
// When
CompletableFuture.supplyAsync(() -> Failsafe.with(retryPolicy).get(() -> service.connect()))
.thenRun(() -> Failsafe.with(retryPolicy).get(() -> service.disconnect()))
.get();
// Then
verify(service, times(4)).connect();
verify(service, times(4)).disconnect();
// Given - Fail three times
reset(service);
when(service.connect()).thenThrow(failures(10, new ConnectException()));
// When / Then
assertThrows(
() -> CompletableFuture.supplyAsync(() -> Failsafe.with(retryTwice).get(() -> service.connect())).get(),
futureSyncThrowables);
verify(service, times(3)).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(() -> Failsafe.with(retryPolicy).get(() -> service.connect()), IllegalStateException.class);
verify(service, times(3)).connect();
}
public void shouldOpenCircuitWhenTimeoutExceeded() throws Throwable {
// Given
CircuitBreaker breaker = new CircuitBreaker().withTimeout(10, TimeUnit.MILLISECONDS);
assertTrue(breaker.isClosed());
// When
Failsafe.with(breaker).run(() -> {
Thread.sleep(20);
});
// Then
assertTrue(breaker.isOpen());
}
/**
* Asserts that Failsafe throws when interrupting a waiting thread.
*/
public void shouldThrowWhenInterruptedDuringSynchronousDelay() throws Throwable {
Thread mainThread = Thread.currentThread();
new Thread(() -> {
try {
Thread.sleep(100);
mainThread.interrupt();
} catch (Exception e) {
}
}).start();
try {
Failsafe.with(new RetryPolicy().withDelay(5, TimeUnit.SECONDS)).run(() -> {
throw new Exception();
});
} catch (Exception e) {
assertTrue(e instanceof FailsafeException);
assertTrue(e.getCause() instanceof InterruptedException);
// Clear interrupt flag
Thread.interrupted();
}
}
public void shouldRetryAndOpenCircuit() {
CircuitBreaker circuit = new CircuitBreaker().withFailureThreshold(3).withDelay(10, TimeUnit.MINUTES);
// Given - Fail twice then succeed
when(service.connect()).thenThrow(failures(20, new ConnectException())).thenReturn(true);
// When
assertThrows(() -> Failsafe.with(retryAlways).with(circuit).run(() -> service.connect()),
CircuitBreakerOpenException.class);
// Then
verify(service, times(3)).connect();
}
public void shouldThrowCircuitBreakerOpenExceptionAfterFailuresExceeded() {
// Given
CircuitBreaker breaker = new CircuitBreaker().withFailureThreshold(2).withDelay(10, TimeUnit.SECONDS);
AtomicInteger counter = new AtomicInteger();
CheckedRunnable runnable = () -> Failsafe.with(breaker).run(() -> {
counter.incrementAndGet();
throw new Exception();
});
// When
ignoreExceptions(runnable);
ignoreExceptions(runnable);
// Then
assertThrows(runnable, CircuitBreakerOpenException.class);
assertEquals(counter.get(), 2);
}
/**
* Asserts that an execution is failed when the max duration is exceeded.
*/
public void shouldCompleteWhenMaxDurationExceeded() throws Throwable {
when(service.connect()).thenReturn(false);
RetryPolicy retryPolicy = new RetryPolicy().retryWhen(false).withMaxDuration(100, TimeUnit.MILLISECONDS);
assertEquals(Failsafe.with(retryPolicy).onFailure((r, f) -> {
assertEquals(r, Boolean.FALSE);
assertNull(f);
}).get(() -> {
Testing.sleep(120);
return service.connect();
}), Boolean.FALSE);
verify(service).connect();
}
public void shouldWrapCheckedExceptions() throws Throwable {
assertThrows(() -> Failsafe.with(new RetryPolicy().withMaxRetries(1)).run(() -> {
throw new TimeoutException();
}), FailsafeException.class, TimeoutException.class);
}
private void run(SyncFailsafe<?> failsafe, Object runnable) {
if (runnable instanceof CheckedRunnable)
failsafe.run((CheckedRunnable) runnable);
else if (runnable instanceof ContextualRunnable)
failsafe.run((ContextualRunnable) runnable);
}
@SuppressWarnings("unchecked")
private <T> T get(SyncFailsafe<?> failsafe, Object callable) {
if (callable instanceof Callable)
return (T) failsafe.get((Callable<T>) callable);
else
return (T) failsafe.get((ContextualCallable<T>) callable);
}
}