package com.bazaarvoice.ostrich.pool;
import com.bazaarvoice.ostrich.ServiceEndPoint;
import com.bazaarvoice.ostrich.ServiceFactory;
import com.bazaarvoice.ostrich.exceptions.NoCachedInstancesAvailableException;
import com.codahale.metrics.MetricRegistry;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import org.apache.commons.pool.impl.GenericKeyedObjectPool;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
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.Matchers.anyBoolean;
import static org.mockito.Matchers.anyLong;
import static org.mockito.Matchers.eq;
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 SingleThreadedClientServiceCacheTest {
private static final ServiceEndPoint END_POINT = mock(ServiceEndPoint.class);
private ServiceFactory<Service> _factory;
private ServiceCachingPolicy _cachingPolicy;
private MetricRegistry _registry = new MetricRegistry();
private List<SingleThreadedClientServiceCache<?>> _caches = Lists.newArrayList();
@SuppressWarnings("unchecked")
@Before
public void setup() {
_factory = mock(ServiceFactory.class);
when(_factory.getServiceName()).thenReturn(Service.class.getSimpleName());
when(_factory.create(any(ServiceEndPoint.class))).thenAnswer(new Answer<Service>() {
@Override
public Service answer(InvocationOnMock invocation) throws Throwable {
return mock(Service.class);
}
});
// By default the caching policy will grow infinitely
_cachingPolicy = mock(ServiceCachingPolicy.class);
when(_cachingPolicy.getMaxNumServiceInstances()).thenReturn(-1);
when(_cachingPolicy.getMaxNumServiceInstancesPerEndPoint()).thenReturn(1);
when(_cachingPolicy.getCacheExhaustionAction()).thenReturn(ServiceCachingPolicy.ExhaustionAction.FAIL);
}
@After
public void teardown() {
for (SingleThreadedClientServiceCache<?> cache : _caches) {
cache.close();
}
}
@Test
public void testKeyedObjectPoolIsCorrectlyConfigured() {
// Set values to be different from corresponding GenericKeyedObjectPool defaults.
when(_cachingPolicy.getCacheExhaustionAction()).thenReturn(ServiceCachingPolicy.ExhaustionAction.GROW);
when(_cachingPolicy.getMaxNumServiceInstances()).thenReturn(20);
when(_cachingPolicy.getMaxNumServiceInstancesPerEndPoint()).thenReturn(5);
when(_cachingPolicy.getMaxServiceInstanceIdleTime(TimeUnit.MILLISECONDS)).thenReturn(10L);
GenericKeyedObjectPool<ServiceEndPoint, Service> pool = newCache().getPool();
assertEquals(GenericKeyedObjectPool.WHEN_EXHAUSTED_GROW, pool.getWhenExhaustedAction());
assertEquals(20, pool.getMaxTotal());
assertEquals(5, pool.getMaxActive());
assertEquals(5, pool.getMaxIdle());
assertEquals(10L, pool.getMinEvictableIdleTimeMillis());
assertEquals(20, pool.getNumTestsPerEvictionRun());
}
@Test(expected = NullPointerException.class)
public void testCheckOutFromNullEndPoint() throws Exception {
newCache().checkOut(null);
}
@Test(expected = NullPointerException.class)
public void testCheckInNullHandle() throws Exception {
newCache().checkIn(null);
}
@Test(expected = NullPointerException.class)
public void testCheckInToNullEndPoint() throws Exception {
Service service = mock(Service.class);
ServiceHandle<Service> handle = new ServiceHandle<>(service, null);
newCache().checkIn(handle);
}
@Test(expected = NullPointerException.class)
public void testCheckInNullServiceInstance() throws Exception {
ServiceHandle<Service> handle = new ServiceHandle<>(null, END_POINT);
newCache().checkIn(handle);
}
@Test(expected = NullPointerException.class)
public void testEvictNullEndPoint() {
newCache().evict(null);
}
@Test
public void testFactoryExceptionIsPropagated() {
NullPointerException exception = mock(NullPointerException.class);
when(_factory.create(any(ServiceEndPoint.class))).thenThrow(exception);
SingleThreadedClientServiceCache<Service> cache = newCache();
try {
cache.checkOut(END_POINT);
fail();
} catch (Exception caught) {
assertSame(exception, caught);
}
}
@Test
public void testServiceInstanceIsReused() throws Exception {
SingleThreadedClientServiceCache<Service> cache = newCache();
ServiceHandle<Service> handle = cache.checkOut(END_POINT);
cache.checkIn(handle);
assertSame(handle.getService(), cache.checkOut(END_POINT).getService());
}
@Test
public void testInUseServiceInstanceNotReused() throws Exception {
// Allow 2 instances per end point
when(_cachingPolicy.getMaxNumServiceInstancesPerEndPoint()).thenReturn(2);
SingleThreadedClientServiceCache<Service> cache = newCache();
ServiceHandle<Service> handle = cache.checkOut(END_POINT);
assertNotSame(handle.getService(), cache.checkOut(END_POINT).getService());
}
@Test
public void testDuplicateServiceInstancesAllowed() throws Exception {
Service service = mock(Service.class);
when(_factory.create(any(ServiceEndPoint.class))).thenReturn(service);
when(_cachingPolicy.getMaxNumServiceInstancesPerEndPoint()).thenReturn(-1);
SingleThreadedClientServiceCache<Service> cache = newCache();
ServiceHandle<Service> handle1 = cache.checkOut(END_POINT);
ServiceHandle<Service> handle2 = cache.checkOut(END_POINT);
assertSame(service, handle1.getService());
assertSame(service, handle2.getService());
assertEquals(2, cache.getNumActiveInstances(END_POINT));
cache.checkIn(handle1);
cache.checkIn(handle2);
assertEquals(2, cache.getNumIdleInstances(END_POINT));
}
@Test
public void testSameServiceInstanceAllowedForMultipleEndPoints() throws Exception {
Service service = mock(Service.class);
when(_factory.create(any(ServiceEndPoint.class))).thenReturn(service);
SingleThreadedClientServiceCache<Service> cache = newCache();
ServiceEndPoint endPoint1 = mock(ServiceEndPoint.class);
ServiceEndPoint endPoint2 = mock(ServiceEndPoint.class);
ServiceHandle<Service> handle1 = cache.checkOut(endPoint1);
assertSame(service, handle1.getService());
assertEquals(1, cache.getNumActiveInstances(endPoint1));
assertEquals(0, cache.getNumActiveInstances(endPoint2));
ServiceHandle<Service> handle2 = cache.checkOut(endPoint2);
assertSame(service, handle2.getService());
assertEquals(1, cache.getNumActiveInstances(endPoint1));
assertEquals(1, cache.getNumActiveInstances(endPoint2));
cache.checkIn(handle1);
assertEquals(1, cache.getNumIdleInstances(endPoint1));
assertEquals(0, cache.getNumIdleInstances(endPoint2));
cache.checkIn(handle2);
assertEquals(1, cache.getNumIdleInstances(endPoint1));
assertEquals(1, cache.getNumIdleInstances(endPoint2));
}
@Test
public void testEvictedEndPointDestroyedAutomaticEviction() throws Exception {
// Make the cache only hold one instance total.
when(_cachingPolicy.getMaxNumServiceInstances()).thenReturn(1);
SingleThreadedClientServiceCache<Service> cache = newCache();
ServiceHandle<Service> handle = cache.checkOut(END_POINT);
cache.checkIn(handle);
// Check out a different end point to force the currently cached instance out.
cache.checkOut(mock(ServiceEndPoint.class));
verify(_factory).destroy(END_POINT, handle.getService());
}
@Test
public void testEvictedEndPointDestroyedManualEviction() throws Exception {
SingleThreadedClientServiceCache<Service> cache = newCache();
ServiceHandle<Service> handle = cache.checkOut(END_POINT);
cache.checkIn(handle);
cache.evict(END_POINT);
verify(_factory).destroy(END_POINT, handle.getService());
}
@Test
public void testEvictedEndPointHasServiceInstancesRemovedFromCache() throws Exception {
SingleThreadedClientServiceCache<Service> cache = newCache();
ServiceHandle<Service> handle = cache.checkOut(END_POINT);
cache.checkIn(handle);
cache.evict(END_POINT);
assertNotSame(handle.getService(), cache.checkOut(END_POINT).getService());
}
@Test
public void testEvictedEndPointWhileServiceInstanceCheckedOut() throws Exception {
SingleThreadedClientServiceCache<Service> cache = newCache();
ServiceHandle<Service> handle = cache.checkOut(END_POINT);
cache.evict(END_POINT);
cache.checkIn(handle);
assertNotSame(handle.getService(), cache.checkOut(END_POINT).getService());
}
@Test
public void testEvictedEndPointWhileDuplicateServiceInstancesCheckedOut() throws Exception {
Service service = mock(Service.class);
when(_factory.create(any(ServiceEndPoint.class))).thenReturn(service);
when(_cachingPolicy.getMaxNumServiceInstancesPerEndPoint()).thenReturn(-1);
SingleThreadedClientServiceCache<Service> cache = newCache();
ServiceHandle<Service> handle1 = cache.checkOut(END_POINT);
ServiceHandle<Service> handle2 = cache.checkOut(END_POINT);
cache.evict(END_POINT);
cache.checkIn(handle1);
cache.checkIn(handle2);
verify(_factory, times(2)).destroy(END_POINT, service);
}
@Test
public void testEvictedEndPointWhileServiceInstanceCheckedOutMoreDuplicatesCheckedOut() throws Exception {
Service service = mock(Service.class);
when(_factory.create(any(ServiceEndPoint.class))).thenReturn(service);
when(_cachingPolicy.getMaxNumServiceInstancesPerEndPoint()).thenReturn(-1);
SingleThreadedClientServiceCache<Service> cache = newCache();
ServiceHandle<Service> handle1 = cache.checkOut(END_POINT);
cache.evict(END_POINT);
// Check out a new one after eviction, while a copy is still checked out.
ServiceHandle<Service> handle2 = cache.checkOut(END_POINT);
cache.checkIn(handle1);
verify(_factory, times(1)).destroy(END_POINT, service);
cache.checkIn(handle2);
verify(_factory, times(1)).destroy(END_POINT, service);
}
@Test
public void testEvictedEndPointWhileServiceInstanceCheckedOutAllowsSubsequentCopies() throws Exception {
Service service = mock(Service.class);
when(_factory.create(any(ServiceEndPoint.class))).thenReturn(service);
SingleThreadedClientServiceCache<Service> cache = newCache();
ServiceHandle<Service> handle = cache.checkOut(END_POINT);
cache.evict(END_POINT);
cache.checkIn(handle);
handle = cache.checkOut(END_POINT);
cache.checkIn(handle);
verify(_factory, times(1)).destroy(END_POINT, service);
}
@Test
public void testEvictedEndPointWhileServiceInstanceCheckedOutAllowsSameInstanceOtherEndPoints() throws Exception {
Service service = mock(Service.class);
when(_factory.create(any(ServiceEndPoint.class))).thenReturn(service);
SingleThreadedClientServiceCache<Service> cache = newCache();
ServiceEndPoint invalidEndPoint = mock(ServiceEndPoint.class);
ServiceEndPoint validEndPoint = mock(ServiceEndPoint.class);
ServiceHandle<Service> handle1 = cache.checkOut(invalidEndPoint);
ServiceHandle<Service> handle2 = cache.checkOut(validEndPoint);
cache.evict(invalidEndPoint);
cache.checkIn(handle1);
cache.checkIn(handle2);
verify(_factory, never()).destroy(validEndPoint, service);
verify(_factory, times(1)).destroy(invalidEndPoint, service);
}
@Test(expected = NoCachedInstancesAvailableException.class)
public void testFailCacheExhaustionAction() throws Exception {
when(_cachingPolicy.getCacheExhaustionAction()).thenReturn(ServiceCachingPolicy.ExhaustionAction.FAIL);
SingleThreadedClientServiceCache<Service> cache = newCache();
cache.checkOut(END_POINT);
cache.checkOut(END_POINT);
}
@Test
public void testGrowCacheExhaustionAction() throws Exception {
when(_cachingPolicy.getCacheExhaustionAction()).thenReturn(ServiceCachingPolicy.ExhaustionAction.GROW);
SingleThreadedClientServiceCache<Service> cache = newCache();
cache.checkOut(END_POINT);
cache.checkOut(END_POINT);
}
@Test
public void testInstancesCreatedWhileGrowingAreNotReused() throws Exception {
when(_cachingPolicy.getMaxNumServiceInstancesPerEndPoint()).thenReturn(1);
when(_cachingPolicy.getCacheExhaustionAction()).thenReturn(ServiceCachingPolicy.ExhaustionAction.GROW);
SingleThreadedClientServiceCache<Service> cache = newCache();
// Grow the cache a bunch, remembering each service that was created...
Set<ServiceHandle<Service>> seenHandles = Sets.newHashSet();
Set<Service> seenServices = Sets.newHashSet();
for (int i = 0; i < 10; i++) {
ServiceHandle<Service> handle = cache.checkOut(END_POINT);
seenHandles.add(handle);
seenServices.add(handle.getService());
}
// Now return each of the services. Since the cache has a size of 1, only one of them should be retained...
for (ServiceHandle<Service> handle : seenHandles) {
cache.checkIn(handle);
}
// Figure out which one is retained...
ServiceHandle<Service> retainedHandle = cache.checkOut(END_POINT);
assertTrue(seenServices.contains(retainedHandle.getService()));
// Force the cache to grow again, this new service should have never been seen before...
ServiceHandle<Service> newHandle = cache.checkOut(END_POINT);
assertFalse(seenServices.contains(newHandle.getService()));
}
@Test
public void testWaitCacheExhaustionAction() throws Exception {
when(_cachingPolicy.getMaxNumServiceInstancesPerEndPoint()).thenReturn(1);
when(_cachingPolicy.getCacheExhaustionAction()).thenReturn(ServiceCachingPolicy.ExhaustionAction.WAIT);
final SingleThreadedClientServiceCache<Service> cache = newCache();
ServiceHandle<Service> handle = cache.checkOut(END_POINT);
// Run a 2nd check out operation in a background thread. It should block because there is only one service
// instance available, and the above check out operation is holding onto it. Eventually we're going to call
// check in to return the instance, at which point the background thread should be able to terminate.
final CountDownLatch inCallable = new CountDownLatch(1);
ExecutorService executor = Executors.newSingleThreadExecutor();
try {
Future<ServiceHandle<Service>> serviceFuture = executor.submit(new Callable<ServiceHandle<Service>>() {
@Override
public ServiceHandle<Service> call() throws Exception {
inCallable.countDown();
return cache.checkOut(END_POINT);
}
});
// Block until we know for sure the callable has had a chance to start executing and it is highly likely
// that is is blocked in the checkOut call.
assertTrue(inCallable.await(10, TimeUnit.SECONDS));
try {
// This should fail because the service instance hasn't yet been returned. There's a small chance that
// this could fail while there is a bug in the code if it takes the bug more time to manifest itself
// than the allotted time to wait.
serviceFuture.get(100, TimeUnit.MILLISECONDS);
fail();
} catch (TimeoutException e) {
// Expected to fail because the instance hasn't been checked in yet.
}
cache.checkIn(handle);
assertSame(handle.getService(), serviceFuture.get(10, TimeUnit.SECONDS).getService());
} finally {
executor.shutdown();
}
}
@Test
public void testSchedulesPeriodicEvictionCheckUponCreation() {
when(_cachingPolicy.getMaxServiceInstanceIdleTime(any(TimeUnit.class))).thenReturn(10L);
ScheduledExecutorService executor = mock(ScheduledExecutorService.class);
when(executor.scheduleAtFixedRate(any(Runnable.class), anyLong(), anyLong(), any(TimeUnit.class))).thenAnswer(
new Answer<ScheduledFuture<?>>() {
@Override
public ScheduledFuture<?> answer(InvocationOnMock invocation) throws Throwable {
return mock(ScheduledFuture.class);
}
}
);
newCache(executor);
verify(executor).scheduleAtFixedRate(
any(Runnable.class),
eq(SingleThreadedClientServiceCache.EVICTION_DURATION_IN_SECONDS),
eq(SingleThreadedClientServiceCache.EVICTION_DURATION_IN_SECONDS),
eq(TimeUnit.SECONDS));
}
@Test(expected = NullPointerException.class)
public void testNumIdleNullEndPoint() {
SingleThreadedClientServiceCache<Service> cache = newCache();
cache.getNumIdleInstances(null);
}
@Test(expected = NullPointerException.class)
public void testNumActiveNullEndPoint() {
SingleThreadedClientServiceCache<Service> cache = newCache();
cache.getNumActiveInstances(null);
}
@Test
public void testNumIdleStartsAtZero() {
SingleThreadedClientServiceCache<Service> cache = newCache();
assertEquals(0, cache.getNumIdleInstances(END_POINT));
}
@Test
public void testNumActiveStartsAtZero() {
SingleThreadedClientServiceCache<Service> cache = newCache();
assertEquals(0, cache.getNumActiveInstances(END_POINT));
}
@Test
public void testNumActiveUpdatedOnCheckOut() throws Exception {
SingleThreadedClientServiceCache<Service> cache = newCache();
cache.checkOut(END_POINT);
assertEquals(1, cache.getNumActiveInstances(END_POINT));
}
@Test
public void testNumIdleUpdatedOnCheckIn() throws Exception {
SingleThreadedClientServiceCache<Service> cache = newCache();
cache.checkIn(cache.checkOut(END_POINT));
assertEquals(1, cache.getNumIdleInstances(END_POINT));
}
@Test
public void testActiveServiceNotCountedIdle() throws Exception {
SingleThreadedClientServiceCache<Service> cache = newCache();
cache.checkOut(END_POINT);
assertEquals(0, cache.getNumIdleInstances(END_POINT));
}
@Test
public void testIdleServiceNotCountedActive() throws Exception {
SingleThreadedClientServiceCache<Service> cache = newCache();
cache.checkIn(cache.checkOut(END_POINT));
assertEquals(0, cache.getNumActiveInstances(END_POINT));
}
@Test
public void testActiveCountAccurateWhenGrowing() throws Exception {
when(_cachingPolicy.getMaxNumServiceInstancesPerEndPoint()).thenReturn(1);
when(_cachingPolicy.getCacheExhaustionAction()).thenReturn(ServiceCachingPolicy.ExhaustionAction.GROW);
SingleThreadedClientServiceCache<Service> cache = newCache();
cache.checkOut(END_POINT);
cache.checkOut(END_POINT);
assertEquals(2, cache.getNumActiveInstances(END_POINT));
}
@Test
public void testCloseDestroysCachedInstances() throws Exception {
SingleThreadedClientServiceCache<Service> cache = newCache();
ServiceHandle<Service> handle = cache.checkOut(END_POINT);
cache.checkIn(handle);
cache.close();
verify(_factory).destroy(END_POINT, handle.getService());
}
@SuppressWarnings ({"unchecked", "rawtypes"})
@Test
public void testCloseCancelsEvictionFuture() {
when(_cachingPolicy.getMaxServiceInstanceIdleTime(any(TimeUnit.class))).thenReturn(10L);
ScheduledExecutorService executor = mock(ScheduledExecutorService.class);
ScheduledFuture future = mock(ScheduledFuture.class);
when(executor.scheduleAtFixedRate(
any(Runnable.class),
anyLong(),
anyLong(),
any(TimeUnit.class))).thenReturn(future);
SingleThreadedClientServiceCache<Service> cache = newCache(executor);
cache.close();
verify(future).cancel(anyBoolean());
}
@Test
public void testMultipleClose() {
SingleThreadedClientServiceCache<Service> cache = newCache();
cache.close();
cache.close();
}
private SingleThreadedClientServiceCache<Service> newCache() {
SingleThreadedClientServiceCache<Service> cache = new SingleThreadedClientServiceCache<>(_cachingPolicy, _factory, _registry);
_caches.add(cache);
return cache;
}
private SingleThreadedClientServiceCache<Service> newCache(ScheduledExecutorService executor) {
SingleThreadedClientServiceCache<Service> cache = new SingleThreadedClientServiceCache<>(_cachingPolicy, _factory, executor, _registry);
_caches.add(cache);
return cache;
}
public static interface Service {}
}