package com.bazaarvoice.ostrich.pool; import com.bazaarvoice.ostrich.PartitionContext; import com.bazaarvoice.ostrich.RetryPolicy; import com.bazaarvoice.ostrich.ServiceCallback; import com.bazaarvoice.ostrich.ServiceEndPoint; import com.bazaarvoice.ostrich.ServiceEndPointPredicate; import com.bazaarvoice.ostrich.exceptions.MaxRetriesException; import com.codahale.metrics.MetricRegistry; import com.google.common.base.Ticker; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.common.io.Closeables; import com.google.common.util.concurrent.MoreExecutors; import org.junit.After; import org.junit.Before; import org.junit.Test; import java.io.IOException; import java.util.Collection; import java.util.List; import java.util.Random; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.anyLong; import static org.mockito.Matchers.same; import static org.mockito.Mockito.RETURNS_MOCKS; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; public class AsyncServicePoolTest { private static final RetryPolicy NEVER_RETRY = mock(RetryPolicy.class); @SuppressWarnings("unchecked") private final ServicePool<Service> _mockPool = mock(ServicePool.class); private final Ticker _mockTicker = mock(Ticker.class); private final ExecutorService _mockExecutor = mock(ExecutorService.class); private final Collection<AsyncServicePool<Service>> _asyncServicePools = Lists.newArrayList(); private final MetricRegistry _metricRegistry = mock(MetricRegistry.class, RETURNS_MOCKS); @Before public void setup() { when(_mockPool.getServiceName()).thenReturn(Service.class.getSimpleName()); } @After public void teardown() throws Exception { for (AsyncServicePool<Service> pool : _asyncServicePools) { Closeables.close(pool, true); } } @Test(expected = NullPointerException.class) public void testNullTicker() { new AsyncServicePool<>(null, _mockPool, true, _mockExecutor, true, _metricRegistry); } @Test(expected = NullPointerException.class) public void testNullServicePool() { new AsyncServicePool<>(_mockTicker, null, true, _mockExecutor, true, _metricRegistry); } @SuppressWarnings("unchecked") @Test(expected = NullPointerException.class) public void testNullExecutorService() { new AsyncServicePool<>(_mockTicker, _mockPool, true, null, true, _metricRegistry); } @SuppressWarnings("unchecked") @Test public void testSubmitsCallableToExecutor() { AsyncServicePool<Service> pool = newAsyncPool(); pool.execute(NEVER_RETRY, mock(ServiceCallback.class)); verify(_mockExecutor).submit(any(Callable.class)); } @SuppressWarnings("unchecked") @Test public void testExecutesCallbackInPool() { // Use a real executor so that it can actually call into the callback AsyncServicePool<Service> pool = newAsyncPool(MoreExecutors.sameThreadExecutor()); ServiceCallback<Service, Void> callback = (ServiceCallback<Service, Void>) mock(ServiceCallback.class); pool.execute(NEVER_RETRY, callback); verify(_mockPool).execute(same(NEVER_RETRY), same(callback)); } @SuppressWarnings("unchecked") @Test public void testExecutesPartitionContextInPool() { AsyncServicePool<Service> pool = newAsyncPool(MoreExecutors.sameThreadExecutor()); ServiceCallback<Service, Void> callback = (ServiceCallback<Service, Void>) mock(ServiceCallback.class); PartitionContext context = mock(PartitionContext.class); pool.execute(context, NEVER_RETRY, callback); verify(_mockPool).execute(same(context), same(NEVER_RETRY), same(callback)); } @SuppressWarnings("unchecked") @Test public void testExecuteAllSubmitsMultipleCallablesToExecutor() { List<ServiceEndPoint> endPoints = Lists.newArrayList( mock(ServiceEndPoint.class), mock(ServiceEndPoint.class), mock(ServiceEndPoint.class) ); when(_mockPool.getAllEndPoints()).thenReturn(endPoints); AsyncServicePool<Service> pool = newAsyncPool(); pool.executeOnAll(NEVER_RETRY, mock(ServiceCallback.class)); verify(_mockExecutor, times(endPoints.size())).submit(any(Callable.class)); } @SuppressWarnings("unchecked") @Test public void testExecuteAllExecutesCallbacksInPool() throws Exception { ServiceEndPoint FOO = mock(ServiceEndPoint.class); ServiceEndPoint BAR = mock(ServiceEndPoint.class); ServiceEndPoint BAZ = mock(ServiceEndPoint.class); when(_mockPool.getAllEndPoints()).thenReturn(Lists.newArrayList(FOO, BAR, BAZ)); // Use a real executor so that it can actually call into the callback AsyncServicePool<Service> pool = newAsyncPool(MoreExecutors.sameThreadExecutor()); ServiceCallback<Service, Void> callback = (ServiceCallback<Service, Void>) mock(ServiceCallback.class); pool.executeOnAll(NEVER_RETRY, callback); verify(_mockPool).executeOnEndPoint(same(FOO), same(callback)); verify(_mockPool).executeOnEndPoint(same(BAR), same(callback)); verify(_mockPool).executeOnEndPoint(same(BAZ), same(callback)); } @SuppressWarnings("unchecked") @Test public void testExecuteAllReturnsValueInFuture() throws Exception { ServiceEndPoint FOO = mock(ServiceEndPoint.class); ServiceEndPoint BAR = mock(ServiceEndPoint.class); ServiceEndPoint BAZ = mock(ServiceEndPoint.class); when(_mockPool.getAllEndPoints()).thenReturn(Lists.newArrayList(FOO, BAR, BAZ)); when(_mockPool.executeOnEndPoint(same(FOO), any(ServiceCallback.class))).thenReturn("FOO"); when(_mockPool.executeOnEndPoint(same(BAR), any(ServiceCallback.class))).thenReturn("BAR"); when(_mockPool.executeOnEndPoint(same(BAZ), any(ServiceCallback.class))).thenReturn("BAZ"); // Use a real executor so that it can actually call into the callback AsyncServicePool<Service> pool = newAsyncPool(MoreExecutors.sameThreadExecutor()); Collection<Future<String>> futures = pool.executeOnAll(NEVER_RETRY, mock(ServiceCallback.class)); assertEquals(3, futures.size()); Set<String> results = Sets.newHashSet(); for (Future<String> future : futures) { results.add(future.get()); } assertEquals(Sets.newHashSet("FOO", "BAR", "BAZ"), results); } @SuppressWarnings("unchecked") @Test public void testExecuteAllWrapsNonRetriableExceptionInFuture() throws Exception { RuntimeException exception = mock(RuntimeException.class); when(_mockPool.getAllEndPoints()).thenReturn(Lists.newArrayList(mock(ServiceEndPoint.class))); when(_mockPool.executeOnEndPoint(any(ServiceEndPoint.class), any(ServiceCallback.class))).thenThrow(exception); when(_mockPool.isRetriableException(any(Exception.class))).thenReturn(false); // Use a real executor so that it can actually call into the callback AsyncServicePool<Service> pool = newAsyncPool(MoreExecutors.sameThreadExecutor()); Collection<Future<Void>> futures = pool.executeOnAll(NEVER_RETRY, mock(ServiceCallback.class)); assertEquals(1, futures.size()); Future<Void> future = futures.iterator().next(); try { future.get(10, TimeUnit.SECONDS); fail(); } catch(ExecutionException e) { assertSame(exception, e.getCause()); } } @SuppressWarnings("unchecked") @Test public void testExecuteAllPropagatesMaxRetriesExceptionWhenOutOfRetries() throws Exception { IllegalArgumentException exception = mock(IllegalArgumentException.class); when(_mockPool.getAllEndPoints()).thenReturn(Lists.newArrayList(mock(ServiceEndPoint.class))); when(_mockPool.executeOnEndPoint(any(ServiceEndPoint.class), any(ServiceCallback.class))).thenThrow(exception); when(_mockPool.isRetriableException(any(Exception.class))).thenReturn(true); // Use a real executor so that it can actually call into the callback AsyncServicePool<Service> pool = newAsyncPool(MoreExecutors.sameThreadExecutor()); Collection<Future<Void>> futures = pool.executeOnAll(NEVER_RETRY, mock(ServiceCallback.class)); assertEquals(1, futures.size()); Future<Void> future = futures.iterator().next(); try { future.get(10, TimeUnit.SECONDS); fail(); } catch(ExecutionException e) { assertTrue(e.getCause() instanceof MaxRetriesException); // Verify that the MaxRetriesException propagates the underlying exception from the service pool MaxRetriesException mre = (MaxRetriesException) e.getCause(); assertTrue(mre.getCause() instanceof IllegalArgumentException); } } @SuppressWarnings("unchecked") @Test public void testExecuteOnSubmitsValidCallablesToExecutor() { ServiceEndPoint FOO = mock(ServiceEndPoint.class); ServiceEndPoint BAR = mock(ServiceEndPoint.class); ServiceEndPoint BAZ = mock(ServiceEndPoint.class); when(_mockPool.getAllEndPoints()).thenReturn(Lists.newArrayList(FOO, BAR, BAZ)); ServiceEndPointPredicate predicate = mock(ServiceEndPointPredicate.class); when(predicate.apply(same(FOO))).thenReturn(false); when(predicate.apply(same(BAR))).thenReturn(true); when(predicate.apply(same(BAZ))).thenReturn(true); AsyncServicePool<Service> pool = newAsyncPool(); pool.executeOn(predicate, NEVER_RETRY, mock(ServiceCallback.class)); verify(_mockExecutor, times(2)).submit(any(Callable.class)); } @SuppressWarnings("unchecked") @Test public void testExecuteOnSubmitsNoCallablesToExecutor() { ServiceEndPoint FOO = mock(ServiceEndPoint.class); ServiceEndPoint BAR = mock(ServiceEndPoint.class); ServiceEndPoint BAZ = mock(ServiceEndPoint.class); when(_mockPool.getAllEndPoints()).thenReturn(Lists.newArrayList(FOO, BAR, BAZ)); ServiceEndPointPredicate predicate = mock(ServiceEndPointPredicate.class); when(predicate.apply(same(FOO))).thenReturn(false); when(predicate.apply(same(BAR))).thenReturn(false); when(predicate.apply(same(BAZ))).thenReturn(false); AsyncServicePool<Service> pool = newAsyncPool(); pool.executeOn(predicate, NEVER_RETRY, mock(ServiceCallback.class)); verify(_mockExecutor, never()).submit(any(Callable.class)); } @SuppressWarnings("unchecked") @Test public void testExecuteOnExecutesValidCallbacksInPool() throws Exception { ServiceEndPoint FOO = mock(ServiceEndPoint.class); ServiceEndPoint BAR = mock(ServiceEndPoint.class); ServiceEndPoint BAZ = mock(ServiceEndPoint.class); when(_mockPool.getAllEndPoints()).thenReturn(Lists.newArrayList(FOO, BAR, BAZ)); ServiceEndPointPredicate predicate = mock(ServiceEndPointPredicate.class); when(predicate.apply(same(FOO))).thenReturn(false); when(predicate.apply(same(BAR))).thenReturn(true); when(predicate.apply(same(BAZ))).thenReturn(true); // Use a real executor so that it can actually call into the callback AsyncServicePool<Service> pool = newAsyncPool(MoreExecutors.sameThreadExecutor()); ServiceCallback<Service, Void> callback = (ServiceCallback<Service, Void>) mock(ServiceCallback.class); pool.executeOn(predicate, NEVER_RETRY, callback); verify(_mockPool, never()).executeOnEndPoint(same(FOO), same(callback)); verify(_mockPool).executeOnEndPoint(same(BAR), same(callback)); verify(_mockPool).executeOnEndPoint(same(BAZ), same(callback)); } @SuppressWarnings("unchecked") @Test public void testExecuteOnExecutesNoCallbacksInPool() throws Exception { ServiceEndPoint FOO = mock(ServiceEndPoint.class); ServiceEndPoint BAR = mock(ServiceEndPoint.class); ServiceEndPoint BAZ = mock(ServiceEndPoint.class); when(_mockPool.getAllEndPoints()).thenReturn(Lists.newArrayList(FOO, BAR, BAZ)); ServiceEndPointPredicate predicate = mock(ServiceEndPointPredicate.class); when(predicate.apply(same(FOO))).thenReturn(false); when(predicate.apply(same(BAR))).thenReturn(false); when(predicate.apply(same(BAZ))).thenReturn(false); // Use a real executor so that it can actually call into the callback AsyncServicePool<Service> pool = newAsyncPool(MoreExecutors.sameThreadExecutor()); pool.executeOn(predicate, NEVER_RETRY, mock(ServiceCallback.class)); verify(_mockPool, never()).executeOnEndPoint(any(ServiceEndPoint.class), any(ServiceCallback.class)); } @SuppressWarnings("unchecked") @Test public void testExecuteOnCanFilterEndPoints() throws Exception { ServiceEndPoint FOO = mock(ServiceEndPoint.class); ServiceEndPoint BAR = mock(ServiceEndPoint.class); ServiceEndPoint BAZ = mock(ServiceEndPoint.class); when(_mockPool.getAllEndPoints()).thenReturn(Lists.newArrayList(FOO, BAR, BAZ)); when(_mockPool.executeOnEndPoint(same(FOO), any(ServiceCallback.class))).thenReturn("FOO"); when(_mockPool.executeOnEndPoint(same(BAR), any(ServiceCallback.class))).thenReturn("BAR"); when(_mockPool.executeOnEndPoint(same(BAZ), any(ServiceCallback.class))).thenReturn("BAZ"); // Use a real executor so that it can actually call into the callback AsyncServicePool<Service> pool = newAsyncPool(MoreExecutors.sameThreadExecutor()); ServiceEndPointPredicate predicate = mock(ServiceEndPointPredicate.class); when(predicate.apply(same(FOO))).thenReturn(false); when(predicate.apply(same(BAR))).thenReturn(true); when(predicate.apply(same(BAZ))).thenReturn(false); Collection<Future<String>> futures = pool.executeOn(predicate, NEVER_RETRY, mock(ServiceCallback.class)); assertEquals(1, futures.size()); assertEquals("BAR", futures.iterator().next().get()); } @SuppressWarnings("unchecked") @Test public void testExecuteOnChecksRetriability() throws Exception { RuntimeException exception = mock(RuntimeException.class); when(_mockPool.getAllEndPoints()).thenReturn(Lists.newArrayList(mock(ServiceEndPoint.class))); when(_mockPool.executeOnEndPoint(any(ServiceEndPoint.class), any(ServiceCallback.class))).thenThrow(exception); ServiceEndPointPredicate predicate = mock(ServiceEndPointPredicate.class); when(predicate.apply(any(ServiceEndPoint.class))).thenReturn(true); // Use a real executor so that it can actually call into the callback AsyncServicePool<Service> pool = newAsyncPool(MoreExecutors.sameThreadExecutor()); pool.executeOn(predicate, NEVER_RETRY, mock(ServiceCallback.class)); verify(_mockPool).isRetriableException(exception); } @SuppressWarnings("unchecked") @Test public void testRetriableExceptionIsRetried() throws Exception { when(_mockPool.getAllEndPoints()).thenReturn(Lists.newArrayList(mock(ServiceEndPoint.class))); // Set things up to throw and exception on first run. when(_mockPool.executeOnEndPoint(any(ServiceEndPoint.class), any(ServiceCallback.class))) .thenThrow(RuntimeException.class).thenReturn(null); // Allow retry. when(_mockPool.isRetriableException(any(Exception.class))).thenReturn(true); // Use a real executor so that it can actually call into the callback AsyncServicePool<Service> pool = newAsyncPool(MoreExecutors.sameThreadExecutor()); RetryPolicy retry = mock(RetryPolicy.class); when(retry.allowRetry(anyInt(), anyLong())).thenReturn(true); Collection<Future<Void>> futures = pool.executeOnAll(retry, mock(ServiceCallback.class)); assertEquals(1, futures.size()); Future<Void> future = futures.iterator().next(); future.get(10, TimeUnit.SECONDS); } @Test public void testCloseDoesShutdownExecutor() throws IOException { AsyncServicePool<Service> pool = newAsyncPool(_mockExecutor, true); pool.close(); verify(_mockExecutor).shutdown(); } @Test public void testCloseDoesNotShutdownExecutor() throws IOException { AsyncServicePool<Service> pool = newAsyncPool(_mockExecutor, false); pool.close(); verify(_mockExecutor, never()).shutdown(); } @Test public void testCloseDoesShutdownPool() throws IOException { AsyncServicePool<Service> pool = newAsyncPool(_mockPool, true); pool.close(); verify(_mockPool).close(); } @Test public void testCloseDoesNotShutdownPool() throws IOException { AsyncServicePool<Service> pool = newAsyncPool(_mockPool, false); pool.close(); verify(_mockPool, never()).close(); } @Test public void testGetNumValidEndPoints() { int expected = new Random().nextInt(); AsyncServicePool<Service> pool = newAsyncPool(); when(_mockPool.getNumValidEndPoints()).thenReturn(expected); assertEquals(expected, pool.getNumValidEndPoints()); verify(_mockPool).getNumValidEndPoints(); } @Test public void testGetNumBadEndPoints() { int expected = new Random().nextInt(); AsyncServicePool<Service> pool = newAsyncPool(); when(_mockPool.getNumBadEndPoints()).thenReturn(expected); assertEquals(expected, pool.getNumBadEndPoints()); verify(_mockPool).getNumBadEndPoints(); } private AsyncServicePool<Service> newAsyncPool() { return newAsyncPool(_mockExecutor); } private AsyncServicePool<Service> newAsyncPool(ExecutorService executor) { return newAsyncPool(executor, true); } private AsyncServicePool<Service> newAsyncPool(ExecutorService executor, boolean shutdownExecutorOnClose) { AsyncServicePool<Service> pool = new AsyncServicePool<>(_mockTicker, _mockPool, true, executor, shutdownExecutorOnClose, _metricRegistry); _asyncServicePools.add(pool); return pool; } private AsyncServicePool<Service> newAsyncPool(ServicePool<Service> pool, boolean shutdownPoolOnClose) { AsyncServicePool<Service> asyncPool = new AsyncServicePool<>(_mockTicker, pool, shutdownPoolOnClose, _mockExecutor, true, _metricRegistry); _asyncServicePools.add(asyncPool); return asyncPool; } private static interface Service { } }