/* * 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.Testing.failures; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.when; import static org.testng.Assert.assertFalse; import static org.testng.Assert.fail; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import org.testng.Assert; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import net.jodah.concurrentunit.Waiter; @Test public class FailsafeConfigTest { private Service service = mock(Service.class); ExecutorService executor; ListenerCounter abort; ListenerCounter complete; ListenerCounter failedAttempt; ListenerCounter failure; ListenerCounter retriesExceeded; ListenerCounter retry; ListenerCounter success; static class ListenerCounter { Waiter waiter = new Waiter(); int asyncListeners; /** Per listener invocations */ Map<Object, AtomicInteger> invocations = new ConcurrentHashMap<>(); /** Records a sync invocation of the {@code listener}. */ void sync(Object listener) { invocations.computeIfAbsent(listener, l -> new AtomicInteger()).incrementAndGet(); } /** Records a sync invocation of the {@code listener} and asserts the {@code context}'s execution count. */ void sync(Object listener, ExecutionContext context) { waiter.assertEquals(context.getExecutions(), invocations.computeIfAbsent(listener, l -> new AtomicInteger()).incrementAndGet()); } /** Records an async invocation of the {@code listener}. */ synchronized void async(Object listener) { sync(listener); waiter.resume(); } /** Records an async invocation of the {@code listener} and asserts the {@code context}'s execution count. */ synchronized void async(Object listener, ExecutionContext context) { sync(listener, context); waiter.resume(); } /** Waits for the expected async invocations and asserts the expected {@code expectedInvocations}. */ void assertEquals(int expectedInvocations) throws Throwable { if (expectedInvocations > 0) waiter.await(1000, asyncListeners * expectedInvocations); if (invocations.isEmpty()) Assert.assertEquals(expectedInvocations, 0); for (AtomicInteger counter : invocations.values()) Assert.assertEquals(counter.get(), expectedInvocations); } } public interface Service { boolean connect(); } @BeforeMethod void beforeMethod() { executor = Executors.newFixedThreadPool(2); reset(service); abort = new ListenerCounter(); complete = new ListenerCounter(); failedAttempt = new ListenerCounter(); failure = new ListenerCounter(); retriesExceeded = new ListenerCounter(); retry = new ListenerCounter(); success = new ListenerCounter(); } @AfterMethod void afterMethod() throws Throwable { executor.shutdownNow(); executor.awaitTermination(5, TimeUnit.SECONDS); } <T> SyncFailsafe<T> registerListeners(SyncFailsafe<T> failsafe) { failsafe.onAbort(e -> abort.sync(1)); failsafe.onAbort((r, e) -> abort.sync(2)); failsafe.onAbort((r, e, c) -> abort.sync(3)); failsafe.onAbortAsync(e -> abort.async(4), executor); failsafe.onAbortAsync((r, e) -> abort.async(5), executor); failsafe.onAbortAsync((r, e, c) -> abort.async(6), executor); abort.asyncListeners = 3; failsafe.onComplete((r, f) -> complete.sync(1)); failsafe.onComplete((r, f, s) -> complete.sync(2)); failsafe.onCompleteAsync((r, f) -> complete.async(3), executor); failsafe.onCompleteAsync((r, f, c) -> complete.async(4), executor); complete.asyncListeners = 2; failsafe.onFailedAttempt(e -> failedAttempt.sync(1)); failsafe.onFailedAttempt((r, f) -> failedAttempt.sync(2)); failsafe.onFailedAttempt((r, f, c) -> failedAttempt.sync(3, c)); failsafe.onFailedAttemptAsync(e -> failedAttempt.async(4), executor); failsafe.onFailedAttemptAsync((r, f) -> failedAttempt.async(5), executor); failsafe.onFailedAttemptAsync((r, f, c) -> failedAttempt.async(6, c), executor); failedAttempt.asyncListeners = 3; failsafe.onFailure(e -> failure.sync(1)); failsafe.onFailure((r, f) -> failure.sync(2)); failsafe.onFailure((r, f, s) -> failure.sync(3)); failsafe.onFailureAsync(e -> failure.async(4), executor); failsafe.onFailureAsync((r, f) -> failure.async(5), executor); failsafe.onFailureAsync((r, f, s) -> failure.async(6), executor); failure.asyncListeners = 3; failsafe.onRetriesExceeded(e -> retriesExceeded.sync(1)); failsafe.onRetriesExceeded((r, f) -> retriesExceeded.sync(2)); failsafe.onRetriesExceededAsync(e -> retriesExceeded.async(3), executor); failsafe.onRetriesExceededAsync((r, f) -> retriesExceeded.async(4), executor); retriesExceeded.asyncListeners = 2; failsafe.onRetry(e -> retry.sync(1)); failsafe.onRetry((r, f) -> retry.sync(2)); failsafe.onRetry((r, f, s) -> retry.sync(3, s)); failsafe.onRetryAsync(e -> retry.async(4), executor); failsafe.onRetryAsync((r, f) -> retry.async(5), executor); failsafe.onRetryAsync((r, f, s) -> retry.async(6), executor); retry.asyncListeners = 3; failsafe.onSuccess((r) -> success.sync(1)); failsafe.onSuccess((r, s) -> success.sync(2)); failsafe.onSuccessAsync((r) -> success.async(3), executor); failsafe.onSuccessAsync((r, s) -> success.async(4), executor); success.asyncListeners = 2; return failsafe; } /** * Asserts that listeners are called the expected number of times for a successful completion. */ public void testListenersForSuccess() throws Throwable { Callable<Boolean> callable = () -> service.connect(); // Given - Fail 4 times then succeed when(service.connect()).thenThrow(failures(2, new IllegalStateException())).thenReturn(false, false, true); RetryPolicy retryPolicy = new RetryPolicy().retryWhen(false); // When registerListeners(Failsafe.with(retryPolicy)).get(callable); // Then abort.assertEquals(0); complete.assertEquals(1); failedAttempt.assertEquals(4); failure.assertEquals(0); retriesExceeded.assertEquals(0); retry.assertEquals(4); success.assertEquals(1); } /** * Asserts that listeners are called the expected number of times for an unhandled failure. */ public void testListenersForUnhandledFailure() throws Throwable { Callable<Boolean> callable = () -> service.connect(); // Given - Fail 2 times then don't match policy when(service.connect()).thenThrow(failures(2, new IllegalStateException())) .thenThrow(IllegalArgumentException.class); RetryPolicy retryPolicy = new RetryPolicy().retryOn(IllegalStateException.class).withMaxRetries(10); // When Asserts.assertThrows(() -> registerListeners(Failsafe.with(retryPolicy)).get(callable), IllegalArgumentException.class); // Then abort.assertEquals(0); complete.assertEquals(1); failedAttempt.assertEquals(3); failure.assertEquals(1); retriesExceeded.assertEquals(0); retry.assertEquals(2); success.assertEquals(0); } /** * Asserts that listeners are called the expected number of times when retries are exceeded. */ public void testListenersForRetriesExceeded() throws Throwable { Callable<Boolean> callable = () -> service.connect(); // Given - Fail 4 times and exceed retries when(service.connect()).thenThrow(failures(10, new IllegalStateException())); RetryPolicy retryPolicy = new RetryPolicy().abortOn(IllegalArgumentException.class).withMaxRetries(3); // When Asserts.assertThrows(() -> registerListeners(Failsafe.with(retryPolicy)).get(callable), IllegalStateException.class); // Then abort.assertEquals(0); complete.assertEquals(1); failedAttempt.assertEquals(4); failure.assertEquals(1); retriesExceeded.assertEquals(1); retry.assertEquals(3); success.assertEquals(0); } /** * Asserts that listeners are called the expected number of times for an aborted execution. */ public void testListenersForAbort() throws Throwable { Callable<Boolean> callable = () -> service.connect(); // Given - Fail twice then abort when(service.connect()).thenThrow(failures(3, new IllegalStateException())) .thenThrow(new IllegalArgumentException()); RetryPolicy retryPolicy = new RetryPolicy().abortOn(IllegalArgumentException.class).withMaxRetries(3); // When Asserts.assertThrows(() -> registerListeners(Failsafe.with(retryPolicy)).get(callable), IllegalArgumentException.class); // Then abort.assertEquals(1); complete.assertEquals(0); failedAttempt.assertEquals(4); failure.assertEquals(0); retriesExceeded.assertEquals(0); retry.assertEquals(3); success.assertEquals(0); } /** * Asserts that a failure listener is not called on an abort. */ public void testFailureListenerNotCalledOnAbort() { // Given RetryPolicy retryPolicy = new RetryPolicy().abortOn(IllegalArgumentException.class); AtomicBoolean called = new AtomicBoolean(); // When try { Failsafe.with(retryPolicy).onFailure(e -> { called.set(true); }).run(() -> { throw new IllegalArgumentException(); }); fail("Expected exception"); } catch (Exception expected) { } assertFalse(called.get()); } }