/* * 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.Asserts.matches; import static net.jodah.failsafe.Testing.failures; import static net.jodah.failsafe.Testing.ignoreExceptions; 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 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.Executors; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import net.jodah.concurrentunit.Waiter; import net.jodah.failsafe.function.AsyncCallable; import net.jodah.failsafe.function.AsyncRunnable; import net.jodah.failsafe.function.CheckedRunnable; import net.jodah.failsafe.function.ContextualCallable; import net.jodah.failsafe.function.ContextualRunnable; @Test public class AsyncFailsafeTest extends AbstractFailsafeTest { private ScheduledExecutorService executor = Executors.newScheduledThreadPool(5); private Waiter waiter; // Results from a get against a future that wraps an asynchronous Failsafe call private @SuppressWarnings("unchecked") Class<? extends Throwable>[] futureAsyncThrowables = new Class[] { ExecutionException.class, ConnectException.class }; @BeforeMethod protected void beforeMethod() { reset(service); waiter = new Waiter(); counter = new AtomicInteger(); } @Override ScheduledExecutorService getExecutor() { return executor; } private void assertRunWithExecutor(Object runnable) throws Throwable { // Given - Fail twice then succeed when(service.connect()).thenThrow(failures(2, new ConnectException())).thenReturn(true); // When / Then FailsafeFuture<?> future = run(Failsafe.with(retryAlways).with(executor).onComplete((result, failure) -> { waiter.assertNull(result); waiter.assertNull(failure); waiter.resume(); }), runnable); assertNull(future.get()); waiter.await(3000); verify(service, times(3)).connect(); // Given - Fail three times reset(service); counter.set(0); when(service.connect()).thenThrow(failures(10, new ConnectException())); // When FailsafeFuture<?> future2 = run(Failsafe.with(retryTwice).with(executor).onComplete((result, failure) -> { waiter.assertNull(result); waiter.assertTrue(failure instanceof ConnectException); waiter.resume(); }), runnable); // Then assertThrows(() -> future2.get(), futureAsyncThrowables); waiter.await(3000); verify(service, times(3)).connect(); } public void shouldRunWithExecutor() throws Throwable { assertRunWithExecutor((CheckedRunnable) service::connect); } public void shouldRunContextualWithExecutor() throws Throwable { assertRunWithExecutor((ContextualRunnable) context -> { assertEquals(context.getExecutions(), counter.getAndIncrement()); service.connect(); }); } public void shouldRunAsync() throws Throwable { assertRunWithExecutor((AsyncRunnable) exec -> { try { service.connect(); exec.complete(); } catch (Exception failure) { // Alternate between automatic and manual retries if (exec.getExecutions() % 2 == 0) throw failure; if (!exec.retryOn(failure)) throw failure; } }); } private void assertGetWithExecutor(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); // When / Then FailsafeFuture<Boolean> future = get( Failsafe.<Boolean>with(retryPolicy).with(executor).onComplete((result, failure) -> { waiter.assertTrue(result); waiter.assertNull(failure); waiter.resume(); }), callable); assertTrue(future.get()); waiter.await(3000); verify(service, times(5)).connect(); // Given - Fail three times reset(service); counter.set(0); when(service.connect()).thenThrow(failures(10, new ConnectException())); // When / Then FailsafeFuture<Boolean> future2 = get(Failsafe.with(retryTwice).with(executor).onComplete((result, failure) -> { waiter.assertNull(result); waiter.assertTrue(failure instanceof ConnectException); waiter.resume(); }), callable); assertThrows(() -> future2.get(), futureAsyncThrowables); waiter.await(3000); verify(service, times(3)).connect(); } public void shouldGetWithExecutor() throws Throwable { assertGetWithExecutor((Callable<?>) service::connect); } public void shouldGetContextualWithExecutor() throws Throwable { assertGetWithExecutor((ContextualCallable<Boolean>) context -> { assertEquals(context.getExecutions(), counter.getAndIncrement()); return service.connect(); }); } public void shouldGetAsync() throws Throwable { assertGetWithExecutor((AsyncCallable<?>) exec -> { try { boolean result = service.connect(); if (!exec.complete(result)) exec.retryFor(result); return result; } catch (Exception failure) { // Alternate between automatic and manual retries if (exec.getExecutions() % 2 == 0) throw failure; if (!exec.retryOn(failure)) throw failure; return null; } }); } private void assertGetFuture(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); // When CompletableFuture<Boolean> future = future(Failsafe.with(retryPolicy).with(executor), callable); // Then future.whenComplete((result, failure) -> { waiter.assertTrue(result); waiter.assertNull(failure); waiter.resume(); }); assertTrue(future.get()); waiter.await(3000); verify(service, times(5)).connect(); // Given - Fail three times reset(service); when(service.connect()).thenThrow(failures(10, new ConnectException())); // When CompletableFuture<Boolean> future2 = future(Failsafe.with(retryTwice).with(executor), callable); // Then future2.whenComplete((result, failure) -> { waiter.assertNull(result); waiter.assertTrue(matches(failure, ConnectException.class)); waiter.resume(); }); assertThrows(() -> future2.get(), futureAsyncThrowables); waiter.await(3000); verify(service, times(3)).connect(); } public void testFuture() throws Throwable { assertGetFuture((Callable<?>) () -> CompletableFuture.supplyAsync(service::connect)); } public void testFutureContextual() throws Throwable { assertGetFuture((ContextualCallable<?>) context -> CompletableFuture.supplyAsync(service::connect)); } public void testFutureAsync() throws Throwable { assertGetFuture((AsyncCallable<?>) exec -> CompletableFuture.supplyAsync(() -> { try { boolean result = service.connect(); if (!exec.complete(result)) exec.retryFor(result); return result; } catch (Exception failure) { // Alternate between automatic and manual retries if (exec.getExecutions() % 2 == 0) throw failure; if (!exec.retryOn(failure)) throw failure; return null; } })); } public void shouldCancelFuture() throws Throwable { FailsafeFuture<?> future = Failsafe.with(retryAlways) .with(executor) .run(() -> ignoreExceptions(() -> Thread.sleep(10000))); future.cancel(true); assertTrue(future.isCancelled()); } public void shouldManuallyRetryAndComplete() throws Throwable { Failsafe.<Boolean>with(retryAlways).with(executor).onComplete((result, failure) -> { waiter.assertTrue(result); waiter.assertNull(failure); waiter.resume(); }).getAsync(exec -> { if (exec.getExecutions() < 2) exec.retryOn(new ConnectException()); else exec.complete(true); return true; }); waiter.await(3000); } /** * Assert handles a callable that throws instead of returning a future. */ public void shouldHandleThrowingFutureCallable() { assertThrows(() -> Failsafe.with(retryTwice).with(executor).future(() -> { throw new IllegalArgumentException(); }).get(), ExecutionException.class, IllegalArgumentException.class); assertThrows(() -> Failsafe.with(retryTwice).with(executor).future(context -> { throw new IllegalArgumentException(); }).get(), ExecutionException.class, IllegalArgumentException.class); assertThrows(() -> Failsafe.with(retryTwice).with(executor).futureAsync(exec -> { throw new IllegalArgumentException(); }).get(), ExecutionException.class, IllegalArgumentException.class); } /** * Asserts that asynchronous completion via an execution is supported. */ public void shouldCompleteAsync() throws Throwable { Waiter waiter = new Waiter(); Failsafe.with(retryAlways).with(executor).runAsync(exec -> executor.schedule(() -> { try { exec.complete(); waiter.resume(); } catch (Exception e) { waiter.fail(e); } } , 100, TimeUnit.MILLISECONDS)); waiter.await(5000); } public void shouldOpenCircuitWhenTimeoutExceeded() throws Throwable { // Given CircuitBreaker breaker = new CircuitBreaker().withTimeout(10, TimeUnit.MILLISECONDS); assertTrue(breaker.isClosed()); // When Failsafe.with(breaker).with(executor).runAsync(exec -> { Thread.sleep(20); exec.complete(); waiter.resume(); }); // Then waiter.await(1000); assertTrue(breaker.isOpen()); } /** * Asserts that Failsafe handles an initial scheduling failure. */ public void shouldHandleInitialSchedulingFailure() throws Throwable { // Given ScheduledExecutorService executor = Executors.newScheduledThreadPool(0); executor.shutdownNow(); Waiter waiter = new Waiter(); // When FailsafeFuture<Void> future = Failsafe.with(new RetryPolicy().retryWhen(null).retryOn(Exception.class)) .with(executor) .run(() -> waiter.fail("Should not execute callable since executor has been shutdown")); assertThrows(() -> future.get(), ExecutionException.class, RejectedExecutionException.class); } /** * Asserts that Failsafe handles a retry scheduling failure. */ public void shouldHandleRejectedRetryExecution() throws Throwable { // Given ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); AtomicInteger counter = new AtomicInteger(); // When FailsafeFuture<String> future = Failsafe.with(new RetryPolicy().retryWhen(null).retryOn(Exception.class)) .with(executor) .get(() -> { counter.incrementAndGet(); Thread.sleep(200); return null; }); Thread.sleep(150); executor.shutdownNow(); assertThrows(() -> future.get(), ExecutionException.class, RejectedExecutionException.class); assertEquals(counter.get(), 1, "Callable should have been executed before executor was shutdown"); } @SuppressWarnings("unused") public void shouldSupportCovariance() { FastService fastService = mock(FastService.class); FailsafeFuture<Service> future = Failsafe.with(new RetryPolicy()).with(executor).get(() -> fastService); } private FailsafeFuture<?> run(AsyncFailsafe<?> failsafe, Object runnable) { if (runnable instanceof CheckedRunnable) return failsafe.run((CheckedRunnable) runnable); else if (runnable instanceof ContextualRunnable) return failsafe.run((ContextualRunnable) runnable); else return failsafe.runAsync((AsyncRunnable) runnable); } @SuppressWarnings("unchecked") private <T> FailsafeFuture<T> get(AsyncFailsafe<?> failsafe, Object callable) { if (callable instanceof Callable) return failsafe.get((Callable<T>) callable); else if (callable instanceof ContextualCallable) return failsafe.get((ContextualCallable<T>) callable); else return failsafe.getAsync((AsyncCallable<T>) callable); } @SuppressWarnings("unchecked") private <T> CompletableFuture<T> future(AsyncFailsafe<?> failsafe, Object callable) { if (callable instanceof Callable) return failsafe.future((Callable<CompletableFuture<T>>) callable); else if (callable instanceof ContextualCallable) return failsafe.future((ContextualCallable<CompletableFuture<T>>) callable); else return failsafe.futureAsync((AsyncCallable<CompletableFuture<T>>) callable); } }