package com.bazaarvoice.ostrich.pool;
import com.bazaarvoice.ostrich.HostDiscovery;
import com.bazaarvoice.ostrich.LoadBalanceAlgorithm;
import com.bazaarvoice.ostrich.PartitionContext;
import com.bazaarvoice.ostrich.RetryPolicy;
import com.bazaarvoice.ostrich.ServiceCallback;
import com.bazaarvoice.ostrich.ServiceEndPoint;
import com.bazaarvoice.ostrich.ServiceFactory;
import com.bazaarvoice.ostrich.ServicePoolStatistics;
import com.bazaarvoice.ostrich.exceptions.MaxRetriesException;
import com.bazaarvoice.ostrich.exceptions.ServiceException;
import com.bazaarvoice.ostrich.healthcheck.FixedHealthCheckRetryDelay;
import com.bazaarvoice.ostrich.partition.PartitionFilter;
import com.codahale.metrics.MetricRegistry;
import com.google.common.base.Throwables;
import com.google.common.base.Ticker;
import com.google.common.collect.ImmutableList;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import static com.google.common.collect.Lists.newArrayList;
import static org.junit.Assert.assertNotSame;
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.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class ServicePoolCachingTest {
private static final ServiceEndPoint FOO_ENDPOINT = mock(ServiceEndPoint.class);
private static final RetryPolicy NEVER_RETRY = mock(RetryPolicy.class);
private static final ServiceCachingPolicy CACHE_ONE_INSTANCE_PER_ENDPOINT = new ServiceCachingPolicyBuilder()
.withMaxNumServiceInstancesPerEndPoint(1)
.build();
private static final ServiceCallback<Service, Service> IDENTITY_CALLBACK = new ServiceCallback<Service, Service>() {
@Override
public Service call(Service service) throws ServiceException {
return service;
}
};
private Ticker _ticker;
private HostDiscovery _hostDiscovery;
private ServiceFactory<Service> _serviceFactory;
private LoadBalanceAlgorithm _loadBalanceAlgorithm;
private ScheduledExecutorService _healthCheckExecutor;
private PartitionFilter _partitionFilter;
private List<ServicePool<Service>> _pools = newArrayList();
private MetricRegistry _registry = new MetricRegistry();
@SuppressWarnings("unchecked")
@Before
public void setup() {
//
// This setup method takes the approach of building a reasonably useful ServicePool using mocks that can then be
// customized by individual test methods to add whatever functionality they need to (or ignored completely).
//
_ticker = mock(Ticker.class);
_hostDiscovery = mock(HostDiscovery.class);
when(_hostDiscovery.getHosts()).thenReturn(ImmutableList.of(FOO_ENDPOINT));
_loadBalanceAlgorithm = mock(LoadBalanceAlgorithm.class);
when(_loadBalanceAlgorithm.choose(any(Iterable.class), any(ServicePoolStatistics.class)))
.thenReturn(FOO_ENDPOINT);
_serviceFactory = (ServiceFactory<Service>) mock(ServiceFactory.class);
when(_serviceFactory.getServiceName()).thenReturn(Service.class.getSimpleName());
when(_serviceFactory.create(any(ServiceEndPoint.class))).then(new Answer<Service>() {
@Override
public Service answer(InvocationOnMock invocation) throws Throwable {
return mock(Service.class);
}
});
when(_serviceFactory.isRetriableException(any(Exception.class))).thenReturn(true);
_healthCheckExecutor = mock(ScheduledExecutorService.class);
_partitionFilter = mock(PartitionFilter.class);
when(_partitionFilter.filter(any(Iterable.class), any(PartitionContext.class)))
.thenAnswer(new Answer<Iterable<ServiceEndPoint>>() {
@Override
public Iterable<ServiceEndPoint> answer(InvocationOnMock invocation) throws Throwable {
return (Iterable<ServiceEndPoint>) invocation.getArguments()[0];
}
});
}
@After
public void teardown() throws IOException {
for (ServicePool<Service> pool : _pools) {
pool.close();
}
_hostDiscovery.close();
}
@Test
public void testServiceInstanceIsCached() {
ServicePool<Service> pool = newPool(CACHE_ONE_INSTANCE_PER_ENDPOINT);
Service service = pool.execute(NEVER_RETRY, IDENTITY_CALLBACK);
assertSame(service, pool.execute(NEVER_RETRY, IDENTITY_CALLBACK));
}
@Test
public void testEvictsAllCachedInstancesWhenHostDiscoveryRemovesEndPoint() {
ServicePool<Service> pool = newPool(CACHE_ONE_INSTANCE_PER_ENDPOINT);
Service service = pool.execute(NEVER_RETRY, IDENTITY_CALLBACK);
// Set it up so that when we health check FOO, that it becomes healthy.
when(_serviceFactory.isHealthy(FOO_ENDPOINT)).thenReturn(true);
// Capture the end point listener that was registered with HostDiscovery
ArgumentCaptor<HostDiscovery.EndPointListener> listener = ArgumentCaptor.forClass(
HostDiscovery.EndPointListener.class);
verify(_hostDiscovery).addListener(listener.capture());
// Remove the end point from host discovery then add it back
listener.getValue().onEndPointRemoved(FOO_ENDPOINT);
listener.getValue().onEndPointAdded(FOO_ENDPOINT);
pool.forceHealthChecks();
assertNotSame(service, pool.execute(NEVER_RETRY, IDENTITY_CALLBACK));
}
@Test
public void testEvictsCachedInstancesOnServiceException() {
ServicePool<Service> pool = newPool(CACHE_ONE_INSTANCE_PER_ENDPOINT);
Service service = pool.execute(NEVER_RETRY, IDENTITY_CALLBACK);
// Set it up so that when we health check FOO, that it becomes healthy.
when(_serviceFactory.isHealthy(FOO_ENDPOINT)).thenReturn(true);
// Cause a service exception, the health check will happen inline and will mark the end point as valid again
try {
pool.execute(NEVER_RETRY, new ServiceCallback<Service, Void>() {
@Override
public Void call(Service service) throws ServiceException {
throw new ServiceException();
}
});
fail();
} catch (MaxRetriesException expected) {
// Expected
}
pool.forceHealthChecks();
assertNotSame(service, pool.execute(NEVER_RETRY, IDENTITY_CALLBACK));
}
@Test
public void testDoesNotEvictCachedInstancesOnNonRetriableException() {
when(_serviceFactory.isRetriableException(any(Exception.class))).thenReturn(false);
ServicePool<Service> pool = newPool(CACHE_ONE_INSTANCE_PER_ENDPOINT);
Service service = pool.execute(NEVER_RETRY, IDENTITY_CALLBACK);
// Cause an exception, this won't trigger a health check since it's not a ServiceException.
try {
pool.execute(NEVER_RETRY, new ServiceCallback<Service, Void>() {
@Override
public Void call(Service service) throws ServiceException {
throw new NullPointerException();
}
});
fail();
} catch (NullPointerException expected) {
// Expected
}
assertSame(service, pool.execute(NEVER_RETRY, IDENTITY_CALLBACK));
}
@Test
public void testWithServiceExceptionRemoving() throws ExecutionException, InterruptedException {
final ServicePool<Service> pool = newPool(CACHE_ONE_INSTANCE_PER_ENDPOINT);
final CountDownLatch canReturn = new CountDownLatch(1);
// Set it up so that when we health check FOO, that it becomes healthy.
when(_serviceFactory.isHealthy(FOO_ENDPOINT)).thenReturn(true);
final CountDownLatch callableStarted = new CountDownLatch(1);
ExecutorService executor = Executors.newSingleThreadExecutor();
try {
Future<Service> serviceFuture = executor.submit(new Callable<Service>() {
@Override
public Service call() throws Exception {
return pool.execute(NEVER_RETRY, new ServiceCallback<Service, Service>() {
@Override
public Service call(Service service) throws ServiceException {
callableStarted.countDown();
// Block until we're allowed to return
try {
canReturn.await(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
throw Throwables.propagate(e);
}
return service;
}
});
}
});
// Wait until the callable has definitely started and allocated a service instance...
assertTrue(callableStarted.await(10, TimeUnit.SECONDS));
// Throw an exception so that the end point is marked as bad and removed from the cache
try {
pool.execute(NEVER_RETRY, new ServiceCallback<Service, Void>() {
@Override
public Void call(Service service) throws ServiceException {
throw new ServiceException();
}
});
fail();
} catch (MaxRetriesException expected) {
// expected exception
}
// Let the initial callback terminate...
canReturn.countDown();
pool.forceHealthChecks();
assertNotSame(serviceFuture.get(), pool.execute(NEVER_RETRY, IDENTITY_CALLBACK));
} finally {
executor.shutdown();
}
}
@Test
public void testWithHostDiscoveryRemoving() throws ExecutionException, InterruptedException {
final ServicePool<Service> pool = newPool(CACHE_ONE_INSTANCE_PER_ENDPOINT);
final CountDownLatch canReturn = new CountDownLatch(1);
// Set it up so that when we health check FOO, that it becomes healthy.
when(_serviceFactory.isHealthy(FOO_ENDPOINT)).thenReturn(true);
final CountDownLatch callableStarted = new CountDownLatch(1);
ExecutorService executor = Executors.newSingleThreadExecutor();
try {
Future<Service> serviceFuture = executor.submit(new Callable<Service>() {
@Override
public Service call() throws Exception {
return pool.execute(NEVER_RETRY, new ServiceCallback<Service, Service>() {
@Override
public Service call(Service service) throws ServiceException {
callableStarted.countDown();
// Block until we're allowed to return
try {
canReturn.await(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
throw Throwables.propagate(e);
}
return service;
}
});
}
});
// Wait until the callable has definitely started and allocated a service instance...
assertTrue(callableStarted.await(10, TimeUnit.SECONDS));
// Capture the end point listener that was registered with HostDiscovery
ArgumentCaptor<HostDiscovery.EndPointListener> listener = ArgumentCaptor.forClass(
HostDiscovery.EndPointListener.class);
verify(_hostDiscovery).addListener(listener.capture());
// Remove the end point from host discovery then add it back
listener.getValue().onEndPointRemoved(FOO_ENDPOINT);
listener.getValue().onEndPointAdded(FOO_ENDPOINT);
// Let the initial callback terminate...
canReturn.countDown();
pool.forceHealthChecks();
assertNotSame(serviceFuture.get(), pool.execute(NEVER_RETRY, IDENTITY_CALLBACK));
} finally {
executor.shutdown();
}
}
private ServicePool<Service> newPool(ServiceCachingPolicy cachingPolicy) {
ServicePool<Service> pool = new ServicePool<>(_ticker, _hostDiscovery, false, _serviceFactory, cachingPolicy,
_partitionFilter, _loadBalanceAlgorithm, _healthCheckExecutor, true, FixedHealthCheckRetryDelay.ZERO, _registry);
_pools.add(pool);
return pool;
}
private interface Service {
}
}