/*
Copyright (c) 2016 LinkedIn Corp.
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.
*/
/**
* $Id: $
*/
package test.r2.transport.http.client;
import com.linkedin.common.callback.Callback;
import com.linkedin.common.callback.FutureCallback;
import com.linkedin.common.util.None;
import com.linkedin.r2.transport.http.client.AsyncPool;
import com.linkedin.r2.transport.http.client.AsyncPoolLifecycleStats;
import com.linkedin.r2.transport.http.client.AsyncSharedPoolImpl;
import com.linkedin.r2.transport.http.client.NoopRateLimiter;
import com.linkedin.r2.transport.http.client.PoolStats;
import com.linkedin.r2.transport.http.client.RateLimiter;
import com.linkedin.r2.util.Cancellable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.IntStream;
import org.testng.Assert;
import org.testng.annotations.AfterSuite;
import org.testng.annotations.BeforeSuite;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
/**
* @author Sean Sheng
* @version $Revision: $
*/
public class TestAsyncSharedPoolImpl
{
private static final String POOL_NAME = "testAsyncSharedPoolImpl";
private static final int NUMBER_OF_THREADS = 128;
private static final int SHUTDOWN_TIMEOUT = 5;
private static final int GET_TIMEOUT = 5;
private static final int OPERATION_TIMEOUT = 5;
private static final int GET_COUNT = 100;
private static final TimeUnit TIME_UNIT = TimeUnit.SECONDS;
private static final long SHORT_POOL_TIMEOUT = 500;
private static final long NO_POOL_TIMEOUT = 0;
private static final Object ITEM = new Object();
private static final int MAX_WAITERS = Integer.MAX_VALUE;
private static final int NO_WAITER = 0;
private static final Exception CREATE_ERROR = new Exception("Simulated create failure");
private static final ScheduledThreadPoolExecutor SCHEDULER = new ScheduledThreadPoolExecutor(NUMBER_OF_THREADS);
private static final LifecycleMock LIFECYCLE = new LifecycleMock();
private static final RateLimiter LIMITER = new NoopRateLimiter();
@BeforeSuite
public void doBeforeSuite()
{
}
@AfterSuite
public void doAfterSuite()
{
SCHEDULER.shutdown();
}
@Test
public void testGetName()
{
AsyncSharedPoolImpl<Object> pool = new AsyncSharedPoolImpl<>(
POOL_NAME, LIFECYCLE, SCHEDULER, LIMITER, NO_POOL_TIMEOUT, MAX_WAITERS);
Assert.assertEquals(pool.getName(), POOL_NAME);
}
@Test
public void testGetStats()
{
AsyncSharedPoolImpl<Object> pool = new AsyncSharedPoolImpl<>(
POOL_NAME, LIFECYCLE, SCHEDULER, LIMITER, NO_POOL_TIMEOUT, MAX_WAITERS);
PoolStats stats = pool.getStats();
Assert.assertNotNull(stats);
Assert.assertEquals(stats.getMaxPoolSize(), 1);
Assert.assertEquals(stats.getMinPoolSize(), 0);
Assert.assertEquals(stats.getIdleCount(), 0);
Assert.assertEquals(stats.getTotalDestroyErrors(), 0);
Assert.assertEquals(stats.getTotalDestroyed(), 0);
Assert.assertEquals(stats.getTotalTimedOut(), 0);
Assert.assertEquals(stats.getTotalCreateErrors(), 0);
Assert.assertEquals(stats.getTotalBadDestroyed(), 0);
Assert.assertEquals(stats.getCheckedOut(), 0);
Assert.assertEquals(stats.getTotalCreated(), 0);
Assert.assertEquals(stats.getPoolSize(), 0);
Assert.assertEquals(stats.getSampleMaxCheckedOut(), 0);
Assert.assertEquals(stats.getSampleMaxPoolSize(), 0);
Assert.assertEquals(stats.getWaitTime50Pct(), 0);
Assert.assertEquals(stats.getWaitTime95Pct(), 0);
Assert.assertEquals(stats.getWaitTime99Pct(), 0);
Assert.assertEquals(stats.getWaitTimeAvg(), 0.0);
}
@Test
public void testStartShutdownSucceeds() throws Exception
{
AsyncSharedPoolImpl<Object> pool = new AsyncSharedPoolImpl<>
(POOL_NAME, LIFECYCLE, SCHEDULER, LIMITER, NO_POOL_TIMEOUT, MAX_WAITERS);
pool.start();
verifyStats(pool.getStats(), 0, 0, 0, 0, 0, 0, 0, 0, 0);
FutureCallback<None> callback = new FutureCallback<>();
pool.shutdown(callback);
None none = callback.get(SHUTDOWN_TIMEOUT, TIME_UNIT);
Assert.assertNotNull(none);
Assert.assertSame(none, None.none());
verifyStats(pool.getStats(), 0, 0, 0, 0, 0, 0, 0, 0, 0);
}
@Test
public void testReaperNoPendingPut() throws Exception
{
AsyncSharedPoolImpl<Object> pool = new AsyncSharedPoolImpl<>(
POOL_NAME, LIFECYCLE, SCHEDULER, LIMITER, SHORT_POOL_TIMEOUT, MAX_WAITERS);
pool.start();
FutureCallback<Object> getCallback = new FutureCallback<>();
pool.get(getCallback);
pool.put(getCallback.get(GET_TIMEOUT, TIME_UNIT));
// Waits for twice the timeout amount of time for reaper to kick-in
Thread.sleep(SHORT_POOL_TIMEOUT * 2);
verifyStats(pool.getStats(), 0, 0, 0, 1, 0, 0, 1, 0, 1);
FutureCallback<None> shutdownCallback = new FutureCallback<>();
pool.shutdown(shutdownCallback);
shutdownCallback.get(SHUTDOWN_TIMEOUT, TIME_UNIT);
}
@Test
public void testReaperWithPendingPut() throws Exception
{
AsyncSharedPoolImpl<Object> pool = new AsyncSharedPoolImpl<>(
POOL_NAME, LIFECYCLE, SCHEDULER, LIMITER, SHORT_POOL_TIMEOUT, MAX_WAITERS);
pool.start();
FutureCallback<Object> getCallback = new FutureCallback<>();
pool.get(getCallback);
Object item = getCallback.get(GET_TIMEOUT, TIME_UNIT);
// Waits for twice the timeout amount of time for reaper to kick-in
Thread.sleep(SHORT_POOL_TIMEOUT * 2);
verifyStats(pool.getStats(), 1, 1, 0, 0, 0, 0, 1, 0, 0);
pool.put(item);
FutureCallback<None> shutdownCallback = new FutureCallback<>();
pool.shutdown(shutdownCallback);
shutdownCallback.get(SHUTDOWN_TIMEOUT, TIME_UNIT);
}
@Test(expectedExceptions = ExecutionException.class)
public void testShutdownBeforeStart() throws Exception
{
AsyncSharedPoolImpl<Object> pool = new AsyncSharedPoolImpl<>(
POOL_NAME, LIFECYCLE, SCHEDULER, LIMITER, NO_POOL_TIMEOUT, MAX_WAITERS);
FutureCallback<None> callback = new FutureCallback<>();
pool.shutdown(callback);
callback.get(SHUTDOWN_TIMEOUT, TIME_UNIT);
}
@Test
public void testShutdownWithPendingPut() throws Exception
{
AsyncSharedPoolImpl<Object> pool = new AsyncSharedPoolImpl<>(
POOL_NAME, LIFECYCLE, SCHEDULER, LIMITER, NO_POOL_TIMEOUT, MAX_WAITERS);
pool.start();
// Get a item from the pool
FutureCallback<Object> getCallback = new FutureCallback<>();
pool.get(getCallback);
Object item = getCallback.get(GET_TIMEOUT, TIME_UNIT);
Assert.assertNotNull(item);
// Shutdown while the item is still outstanding
FutureCallback<None> callback = new FutureCallback<>();
pool.shutdown(callback);
verifyStats(pool.getStats(), 1, 1, 0, 0, 0, 0, 1, 0, 0);
// Return the item back the to the pool
pool.put(item);
verifyStats(pool.getStats(), 1, 0, 1, 0, 0, 0, 1, 0, 0);
callback.get(SHUTDOWN_TIMEOUT, TIME_UNIT);
}
@Test
public void testShutdownWithMultiplePendingPut() throws Exception
{
final AsyncSharedPoolImpl<Object> pool = new AsyncSharedPoolImpl<>(
POOL_NAME, LIFECYCLE, SCHEDULER, LIMITER, NO_POOL_TIMEOUT, MAX_WAITERS);
pool.start();
final CountDownLatch latch = new CountDownLatch(GET_COUNT);
final Collection<FutureCallback<Object>> getCallbacks = new ConcurrentLinkedQueue<>();
IntStream.range(0, GET_COUNT).forEach(i -> SCHEDULER.execute(() -> {
FutureCallback<Object> getCallback = new FutureCallback<>();
pool.get(getCallback);
getCallbacks.add(getCallback);
latch.countDown();
}));
if (!latch.await(OPERATION_TIMEOUT, TIME_UNIT))
{
Assert.fail("Timeout waiting for get calls");
}
final Collection<Object> items = new ConcurrentLinkedQueue<>();
getCallbacks.stream().forEach(callback -> {
try
{
items.add(callback.get(GET_TIMEOUT, TIME_UNIT));
} catch (Exception e)
{
}
});
Assert.assertEquals(items.size(), GET_COUNT);
verifyStats(pool.getStats(), 1, GET_COUNT, 0, 0, 0, 0, 1, 0, 0);
FutureCallback<None> shutdownCallback = new FutureCallback<>();
pool.shutdown(shutdownCallback);
// Put items back to the pool
items.stream().forEach(item -> SCHEDULER.execute(() -> pool.put(item)));
shutdownCallback.get(SHUTDOWN_TIMEOUT, TIME_UNIT);
}
@Test
public void testShutdownWithMultiplePendingPutValidationFails() throws Exception
{
final LifecycleMock lifecycleMock = new LifecycleMock();
lifecycleMock.setValidatePutSupplier(() -> false);
final AsyncSharedPoolImpl<Object> pool = new AsyncSharedPoolImpl<>(
POOL_NAME, lifecycleMock, SCHEDULER, LIMITER, NO_POOL_TIMEOUT, MAX_WAITERS);
pool.start();
final CountDownLatch latch = new CountDownLatch(GET_COUNT);
final Collection<FutureCallback<Object>> getCallbacks = new ConcurrentLinkedQueue<>();
IntStream.range(0, GET_COUNT).forEach(i -> SCHEDULER.execute(() -> {
FutureCallback<Object> getCallback = new FutureCallback<>();
pool.get(getCallback);
getCallbacks.add(getCallback);
latch.countDown();
}));
if (!latch.await(OPERATION_TIMEOUT, TIME_UNIT))
{
Assert.fail("Timeout waiting for get calls");
}
Assert.assertEquals(getCallbacks.size(), GET_COUNT);
final Collection<Object> items = new ConcurrentLinkedQueue<>();
getCallbacks.stream().forEach(callback -> {
try
{
items.add(callback.get(GET_TIMEOUT, TIME_UNIT));
} catch (Exception e)
{
e.printStackTrace();
}
});
Assert.assertEquals(items.size(), GET_COUNT);
verifyStats(pool.getStats(), 1, GET_COUNT, 0, 0, 0, 0, 1, 0, 0);
FutureCallback<None> shutdownCallback = new FutureCallback<>();
pool.shutdown(shutdownCallback);
// Put items back to the pool
items.stream().forEach(item -> SCHEDULER.execute(() -> pool.put(item)));
shutdownCallback.get(SHUTDOWN_TIMEOUT, TIME_UNIT);
}
@Test
public void testShutdownWithPendingDispose() throws Exception
{
AsyncSharedPoolImpl<Object> pool = new AsyncSharedPoolImpl<>(
POOL_NAME, LIFECYCLE, SCHEDULER, LIMITER, NO_POOL_TIMEOUT, MAX_WAITERS);
pool.start();
// Get a item from the pool
FutureCallback<Object> getCallback = new FutureCallback<>();
pool.get(getCallback);
Object item = getCallback.get(GET_TIMEOUT, TIME_UNIT);
Assert.assertNotNull(item);
// Shutdown while the item is still outstanding
FutureCallback<None> callback = new FutureCallback<>();
pool.shutdown(callback);
verifyStats(pool.getStats(), 1, 1, 0, 0, 0, 0, 1, 0, 0);
// Return the item back the to the pool
pool.dispose(item);
verifyStats(pool.getStats(), 0, 0, 0, 1, 0, 1, 1, 0, 0);
callback.get(SHUTDOWN_TIMEOUT, TIME_UNIT);
}
@Test
public void testShutdownWithMultiplePendingDispose() throws Exception
{
final AsyncSharedPoolImpl<Object> pool = new AsyncSharedPoolImpl<>(
POOL_NAME, LIFECYCLE, SCHEDULER, LIMITER, NO_POOL_TIMEOUT, MAX_WAITERS);
pool.start();
final CountDownLatch latch = new CountDownLatch(GET_COUNT);
final Collection<FutureCallback<Object>> getCallbacks = new ConcurrentLinkedQueue<>();
IntStream.range(0, GET_COUNT).forEach(i -> SCHEDULER.execute(() -> {
FutureCallback<Object> getCallback = new FutureCallback<>();
pool.get(getCallback);
getCallbacks.add(getCallback);
latch.countDown();
}));
if (!latch.await(OPERATION_TIMEOUT, TIME_UNIT))
{
Assert.fail("Timeout waiting for get calls");
}
final Collection<Object> items = new ConcurrentLinkedQueue<>();
getCallbacks.stream().forEach(callback -> {
try
{
items.add(callback.get(GET_TIMEOUT, TIME_UNIT));
} catch (Exception e)
{
}
});
Assert.assertEquals(items.size(), GET_COUNT);
verifyStats(pool.getStats(), 1, GET_COUNT, 0, 0, 0, 0, 1, 0, 0);
FutureCallback<None> shutdownCallback = new FutureCallback<>();
pool.shutdown(shutdownCallback);
// Put items back to the pool
items.stream().forEach(item -> SCHEDULER.execute(() -> pool.dispose(item)));
shutdownCallback.get(SHUTDOWN_TIMEOUT, TIME_UNIT);
}
@Test
public void testShutdownWithPendingDisposedItems() throws Exception
{
final AsyncSharedPoolImpl<Object> pool = new AsyncSharedPoolImpl<>(
POOL_NAME, LIFECYCLE, SCHEDULER, LIMITER, NO_POOL_TIMEOUT, MAX_WAITERS);
pool.start();
FutureCallback<Object> getCallback1 = new FutureCallback<>();
FutureCallback<Object> getCallback2 = new FutureCallback<>();
pool.get(getCallback1);
pool.get(getCallback2);
Object item1 = getCallback1.get(GET_TIMEOUT, TIME_UNIT);
Object item2 = getCallback2.get(GET_TIMEOUT, TIME_UNIT);
Assert.assertNotNull(item1);
Assert.assertNotNull(item2);
Assert.assertSame(item1, item2);
verifyStats(pool.getStats(), 1, 2, 0, 0, 0, 0, 1, 0, 0);
pool.dispose(item1);
verifyStats(pool.getStats(), 0, 0, 0, 0, 0, 0, 1, 0, 0);
FutureCallback<None> shutdownCallback = new FutureCallback<>();
pool.shutdown(shutdownCallback);
// Put items back to the pool
pool.dispose(item2);
verifyStats(pool.getStats(), 0, 0, 0, 1, 0, 1, 1, 0, 0);
shutdownCallback.get(SHUTDOWN_TIMEOUT, TIME_UNIT);
}
@Test
public void testCancelWaiters() throws Exception
{
final LifecycleMock lifecycle = new LifecycleMock();
final CountDownLatch latch = new CountDownLatch(1);
lifecycle.setCreateConsumer(callback -> {
try
{
latch.await();
callback.onSuccess(ITEM);
} catch (Exception e)
{
callback.onError(e);
}
});
AsyncSharedPoolImpl<Object> pool = new AsyncSharedPoolImpl<>(
POOL_NAME, lifecycle, SCHEDULER, LIMITER, NO_POOL_TIMEOUT, MAX_WAITERS);
pool.start();
final CountDownLatch getLatch = new CountDownLatch(GET_COUNT - 1);
IntStream.range(0, GET_COUNT).forEach(i -> SCHEDULER.execute(() -> {
pool.get(new FutureCallback<>());
getLatch.countDown();
}));
if (!getLatch.await(GET_TIMEOUT, TIME_UNIT))
{
Assert.fail("Timed out awaiting for get");
}
Collection<Callback<Object>> waiters = pool.cancelWaiters();
Assert.assertNotNull(waiters);
Assert.assertEquals(waiters.size(), GET_COUNT);
verifyStats(pool.getStats(), 0, 0, 0, 0, 0, 0, 0, 0, 0);
latch.countDown();
FutureCallback<None> shutdownCallback = new FutureCallback<>();
pool.shutdown(shutdownCallback);
shutdownCallback.get(SHUTDOWN_TIMEOUT, TIME_UNIT);
}
@Test
public void testSingleGetItemSucceeds() throws Exception
{
AsyncSharedPoolImpl<Object> pool = new AsyncSharedPoolImpl<>(
POOL_NAME, LIFECYCLE, SCHEDULER, LIMITER, NO_POOL_TIMEOUT, MAX_WAITERS);
pool.start();
FutureCallback<Object> getCallback = new FutureCallback<>();
Cancellable cancellable = pool.get(getCallback);
Assert.assertNotNull(cancellable);
Object item = getCallback.get(GET_TIMEOUT, TIME_UNIT);
Assert.assertNotNull(item);
verifyStats(pool.getStats(), 1, 1, 0, 0, 0, 0, 1, 0, 0);
pool.put(item);
verifyStats(pool.getStats(), 1, 0, 1, 0, 0, 0, 1, 0, 0);
FutureCallback<None> shutdownCallback = new FutureCallback<>();
pool.shutdown(shutdownCallback);
shutdownCallback.get(SHUTDOWN_TIMEOUT, TIME_UNIT);
}
@Test
public void testMultipleGetItemSucceeds() throws Exception
{
AsyncSharedPoolImpl<Object> pool = new AsyncSharedPoolImpl<>(
POOL_NAME, LIFECYCLE, SCHEDULER, LIMITER, NO_POOL_TIMEOUT, MAX_WAITERS);
pool.start();
final List<Object> items = new ArrayList<>(GET_COUNT);
for (int i = 0; i < GET_COUNT; i++)
{
FutureCallback<Object> getCallback = new FutureCallback<>();
Cancellable cancellable = pool.get(getCallback);
// Operation should not be cancellable
Assert.assertNotNull(cancellable);
Assert.assertEquals(cancellable.cancel(), false);
Object item = getCallback.get(GET_TIMEOUT, TIME_UNIT);
Assert.assertNotNull(item);
items.add(item);
}
// All items should essentially be the same instance
Assert.assertEquals(items.size(), GET_COUNT);
items.stream().forEach(item -> Assert.assertSame(item, items.get(0)));
verifyStats(pool.getStats(), 1, GET_COUNT, 0, 0, 0, 0, 1, 0, 0);
// Put items back to the pool
IntStream.range(0, GET_COUNT).forEach(i -> pool.put(items.get(i)));
FutureCallback<None> shutdownCallback = new FutureCallback<>();
pool.shutdown(shutdownCallback);
shutdownCallback.get(SHUTDOWN_TIMEOUT, TIME_UNIT);
}
@Test
public void testMultipleDisposeItemSucceeds() throws Exception
{
AsyncSharedPoolImpl<Object> pool = new AsyncSharedPoolImpl<>(
POOL_NAME, LIFECYCLE, SCHEDULER, LIMITER, NO_POOL_TIMEOUT, MAX_WAITERS);
pool.start();
final List<Object> items = new ArrayList<>(GET_COUNT);
for (int i = 0; i < GET_COUNT; i++)
{
FutureCallback<Object> getCallback = new FutureCallback<>();
Cancellable cancellable = pool.get(getCallback);
// Operation should not be cancellable
Assert.assertNotNull(cancellable);
Assert.assertEquals(cancellable.cancel(), false);
Object item = getCallback.get(GET_TIMEOUT, TIME_UNIT);
Assert.assertNotNull(item);
items.add(item);
}
// All items should essentially be the same instance
Assert.assertEquals(items.size(), GET_COUNT);
items.stream().forEach(item -> Assert.assertSame(item, items.get(0)));
verifyStats(pool.getStats(), 1, GET_COUNT, 0, 0, 0, 0, 1, 0, 0);
// Put items back to the pool
IntStream.range(0, GET_COUNT).forEach(i -> pool.dispose(items.get(i)));
FutureCallback<None> shutdownCallback = new FutureCallback<>();
pool.shutdown(shutdownCallback);
shutdownCallback.get(SHUTDOWN_TIMEOUT, TIME_UNIT);
}
@Test
public void testMixedPutAndDisposeItemSucceeds() throws Exception
{
AsyncSharedPoolImpl<Object> pool = new AsyncSharedPoolImpl<>(
POOL_NAME, LIFECYCLE, SCHEDULER, LIMITER, NO_POOL_TIMEOUT, MAX_WAITERS);
pool.start();
final List<Object> items = new ArrayList<>(GET_COUNT);
for (int i = 0; i < GET_COUNT; i++)
{
FutureCallback<Object> getCallback = new FutureCallback<>();
Cancellable cancellable = pool.get(getCallback);
// Operation should not be cancellable
Assert.assertNotNull(cancellable);
Assert.assertEquals(cancellable.cancel(), false);
Object item = getCallback.get(GET_TIMEOUT, TIME_UNIT);
Assert.assertNotNull(item);
items.add(item);
}
// All items should essentially be the same instance
Assert.assertEquals(items.size(), GET_COUNT);
items.stream().forEach(item -> Assert.assertSame(item, items.get(0)));
verifyStats(pool.getStats(), 1, GET_COUNT, 0, 0, 0, 0, 1, 0, 0);
FutureCallback<None> shutdownCallback = new FutureCallback<>();
pool.shutdown(shutdownCallback);
// Put items back to the pool
IntStream.range(0, GET_COUNT).forEach(i -> {
if (i % 2 == 0)
{
pool.put(items.get(i));
} else
{
pool.dispose(items.get(i));
}
});
shutdownCallback.get(SHUTDOWN_TIMEOUT, TIME_UNIT);
}
@Test
public void testGetOnSuccessCallbackThrows() throws Exception
{
AsyncSharedPoolImpl<Object> pool = new AsyncSharedPoolImpl<>(
POOL_NAME, LIFECYCLE, SCHEDULER, LIMITER, NO_POOL_TIMEOUT, MAX_WAITERS);
pool.start();
CountDownLatch onSuccessLatch = new CountDownLatch(1);
pool.get(new Callback<Object>()
{
@Override
public void onSuccess(Object result)
{
onSuccessLatch.countDown();
throw new RuntimeException();
}
@Override
public void onError(Throwable e)
{
}
});
if (!onSuccessLatch.await(GET_TIMEOUT, TIME_UNIT))
{
Assert.fail("Callback onSuccess was not invoked");
}
}
@Test
public void testGetOnErrorCallbackThrows() throws Exception
{
final LifecycleMock lifecycle = new LifecycleMock();
lifecycle.setCreateConsumer(callback -> callback.onError(new Throwable()));
AsyncSharedPoolImpl<Object> pool = new AsyncSharedPoolImpl<>(
POOL_NAME, lifecycle, SCHEDULER, LIMITER, NO_POOL_TIMEOUT, MAX_WAITERS);
pool.start();
CountDownLatch onSuccessLatch = new CountDownLatch(1);
pool.get(new Callback<Object>()
{
@Override
public void onSuccess(Object result)
{
}
@Override
public void onError(Throwable e)
{
onSuccessLatch.countDown();
throw new RuntimeException();
}
});
if (!onSuccessLatch.await(GET_TIMEOUT, TIME_UNIT))
{
Assert.fail("Callback Error was not invoked");
}
}
@Test
public void testGetItemCancelled() throws Exception
{
final LifecycleMock lifecycle = new LifecycleMock();
final CountDownLatch createLatch = new CountDownLatch(1);
lifecycle.setCreateConsumer(callback -> {
try
{
createLatch.await();
callback.onSuccess(ITEM);
} catch (Exception e)
{
callback.onError(e);
}
});
AsyncSharedPoolImpl<Object> pool = new AsyncSharedPoolImpl<>(
POOL_NAME, lifecycle, SCHEDULER, LIMITER, NO_POOL_TIMEOUT, MAX_WAITERS);
pool.start();
// Only one thread will perform the actual item creation task and the rest
// will return immediately. Therefore we wait for GET_COUNT - 1 threads to complete.
final CountDownLatch getLatch = new CountDownLatch(GET_COUNT - 1);
final ConcurrentLinkedQueue<Cancellable> cancellables = new ConcurrentLinkedQueue<>();
for (int i = 0; i < GET_COUNT; i++)
{
SCHEDULER.execute(() -> {
cancellables.add(pool.get(new FutureCallback<>()));
getLatch.countDown();
});
}
if (!getLatch.await(GET_TIMEOUT, TIME_UNIT))
{
Assert.fail("Timed out awaiting for get");
}
Assert.assertEquals(cancellables.size(), GET_COUNT - 1);
// Cancelling waiters should all succeed
cancellables.stream().forEach(cancellable -> Assert.assertTrue(cancellable.cancel()));
// Cancel the last waiter blocking item creation
Assert.assertEquals(pool.cancelWaiters().size(), 1);
createLatch.countDown();
FutureCallback<None> shutdownCallback = new FutureCallback<>();
pool.shutdown(shutdownCallback);
shutdownCallback.get(SHUTDOWN_TIMEOUT, TIME_UNIT);
}
@Test(expectedExceptions = ExecutionException.class)
public void testGetItemCreateFails() throws Exception
{
final LifecycleMock lifecycle = new LifecycleMock();
lifecycle.setCreateConsumer(callback -> callback.onError(new Exception("Simulated create failure")));
AsyncSharedPoolImpl<Object> pool = new AsyncSharedPoolImpl<>(
POOL_NAME, lifecycle, SCHEDULER, LIMITER, NO_POOL_TIMEOUT, MAX_WAITERS);
pool.start();
FutureCallback<Object> getCallback = new FutureCallback<>();
Cancellable cancellable = pool.get(getCallback);
Assert.assertNotNull(cancellable);
verifyStats(pool.getStats(), 0, 0, 0, 0, 0, 0, 0, 1, 0);
getCallback.get(GET_TIMEOUT, TIME_UNIT);
}
@Test(expectedExceptions = ExecutionException.class)
public void testGetWithNoWaiter() throws Exception
{
AsyncSharedPoolImpl<Object> pool = new AsyncSharedPoolImpl<>(
POOL_NAME, LIFECYCLE, SCHEDULER, LIMITER, NO_POOL_TIMEOUT, NO_WAITER);
pool.start();
FutureCallback<Object> callback = new FutureCallback<>();
Cancellable cancellable = pool.get(callback);
Assert.assertNotNull(cancellable);
Assert.assertFalse(cancellable.cancel());
callback.get(GET_TIMEOUT, TIME_UNIT);
}
@Test
public void testGetExceedMaxWaiters() throws Exception
{
final int maxWaiters = 5;
final CountDownLatch latch = new CountDownLatch(1);
final LifecycleMock lifecycle = new LifecycleMock();
lifecycle.setCreateConsumer(callback -> {
try
{
latch.await();
callback.onSuccess(new Object());
}
catch (Exception e)
{
callback.onError(e);
}
});
AsyncSharedPoolImpl<Object> pool = new AsyncSharedPoolImpl<>(
POOL_NAME, lifecycle, SCHEDULER, LIMITER, NO_POOL_TIMEOUT, maxWaiters);
pool.start();
CountDownLatch getLatch = new CountDownLatch(maxWaiters - 1);
ConcurrentLinkedQueue<FutureCallback<Object>> callbacks = new ConcurrentLinkedQueue<>();
for (int i = 0; i < maxWaiters; i++)
{
SCHEDULER.execute(() -> {
FutureCallback<Object> callback = new FutureCallback<>();
Cancellable cancellable = pool.get(callback);
Assert.assertNotNull(cancellable);
callbacks.add(callback);
getLatch.countDown();
});
}
getLatch.await(GET_TIMEOUT, TIME_UNIT);
FutureCallback<Object> waiterCallback = new FutureCallback<>();
Cancellable cancellable = pool.get(waiterCallback);
Assert.assertNotNull(cancellable);
Assert.assertFalse(cancellable.cancel());
try
{
waiterCallback.get(GET_TIMEOUT, TIME_UNIT);
Assert.fail("Callback should fail but did not");
}
catch (ExecutionException e)
{
// Exception is recoverable and expected
}
latch.countDown();
callbacks.forEach(callback -> {
try
{
Object item = callback.get();
Assert.assertNotNull(item);
pool.put(item);
}
catch (Exception e)
{
Assert.fail("Unexpected exception during #get()");
}
});
FutureCallback<None> shutdownCallback = new FutureCallback<>();
pool.shutdown(shutdownCallback);
shutdownCallback.get(SHUTDOWN_TIMEOUT, TIME_UNIT);
}
@Test(expectedExceptions = ExecutionException.class)
public void testGetItemBeforeStart() throws Exception
{
AsyncSharedPoolImpl<Object> pool = new AsyncSharedPoolImpl<>(
POOL_NAME, LIFECYCLE, SCHEDULER, LIMITER, NO_POOL_TIMEOUT, MAX_WAITERS);
FutureCallback<Object> callback = new FutureCallback<>();
pool.get(callback);
callback.get(GET_TIMEOUT, TIME_UNIT);
}
@Test
public void testValidateGetFails() throws Exception
{
final LifecycleMock lifecycle = new LifecycleMock();
lifecycle.setValidateGetSupplier(() -> false);
AsyncSharedPoolImpl<Object> pool = new AsyncSharedPoolImpl<>(
POOL_NAME, lifecycle, SCHEDULER, LIMITER, NO_POOL_TIMEOUT, MAX_WAITERS);
pool.start();
FutureCallback<Object> getCallback1 = new FutureCallback<>();
pool.get(getCallback1);
Object item1 = getCallback1.get(GET_TIMEOUT, TIME_UNIT);
Assert.assertNotNull(item1);
verifyStats(pool.getStats(), 1, 1, 0, 0, 0, 0, 1, 0, 0);
FutureCallback<Object> getCallback2 = new FutureCallback<>();
pool.get(getCallback2);
Object item2 = getCallback2.get(GET_TIMEOUT, TIME_UNIT);
Assert.assertNotNull(item2);
verifyStats(pool.getStats(), 1, 1, 0, 0, 0, 0, 2, 0, 0);
FutureCallback<None> shutdownCallback = new FutureCallback<>();
pool.shutdown(shutdownCallback);
pool.put(item1);
verifyStats(pool.getStats(), 1, 1, 0, 1, 0, 1, 2, 0, 0);
pool.put(item2);
verifyStats(pool.getStats(), 1, 0, 1, 1, 0, 1, 2, 0, 0);
shutdownCallback.get(SHUTDOWN_TIMEOUT, TIME_UNIT);
}
@Test
public void testValidatePutFails() throws Exception
{
final LifecycleMock lifecycle = new LifecycleMock();
lifecycle.setValidatePutSupplier(() -> false);
AsyncSharedPoolImpl<Object> pool = new AsyncSharedPoolImpl<>(
POOL_NAME, lifecycle, SCHEDULER, LIMITER, NO_POOL_TIMEOUT, MAX_WAITERS);
pool.start();
FutureCallback<Object> getCallback = new FutureCallback<>();
pool.get(getCallback);
Object item = getCallback.get(GET_TIMEOUT, TIME_UNIT);
Assert.assertNotNull(item);
pool.put(item);
verifyStats(pool.getStats(), 0, 0, 0, 1, 0, 1, 1, 0, 0);
FutureCallback<None> shutdownCallback = new FutureCallback<>();
pool.shutdown(shutdownCallback);
shutdownCallback.get(SHUTDOWN_TIMEOUT, TIME_UNIT);
}
@Test
public void testDisposeSucceeds() throws Exception
{
AsyncSharedPoolImpl<Object> pool = new AsyncSharedPoolImpl<>(
POOL_NAME, LIFECYCLE, SCHEDULER, LIMITER, NO_POOL_TIMEOUT, MAX_WAITERS);
pool.start();
FutureCallback<Object> getCallback = new FutureCallback<>();
pool.get(getCallback);
Object item = getCallback.get(GET_TIMEOUT, TIME_UNIT);
Assert.assertNotNull(item);
pool.dispose(item);
verifyStats(pool.getStats(), 0, 0, 0, 1, 0, 1, 1, 0, 0);
FutureCallback<None> shutdownCallback = new FutureCallback<>();
pool.shutdown(shutdownCallback);
shutdownCallback.get(SHUTDOWN_TIMEOUT, TIME_UNIT);
}
@Test
public void testDisposeWithPendingCheckouts() throws Exception
{
AsyncSharedPoolImpl<Object> pool = new AsyncSharedPoolImpl<>(
POOL_NAME, LIFECYCLE, SCHEDULER, LIMITER, NO_POOL_TIMEOUT, MAX_WAITERS);
pool.start();
FutureCallback<Object> getCallback1 = new FutureCallback<>();
FutureCallback<Object> getCallback2 = new FutureCallback<>();
pool.get(getCallback1);
pool.get(getCallback2);
Object item1 = getCallback1.get(GET_TIMEOUT, TIME_UNIT);
Object item2 = getCallback2.get(GET_TIMEOUT, TIME_UNIT);
Assert.assertNotNull(item1);
Assert.assertNotNull(item2);
Assert.assertSame(item1, item2);
verifyStats(pool.getStats(), 1, 2, 0, 0, 0, 0, 1, 0, 0);
pool.dispose(item1);
verifyStats(pool.getStats(), 0, 0, 0, 0, 0, 0, 1, 0, 0);
pool.dispose(item2);
verifyStats(pool.getStats(), 0, 0, 0, 1, 0, 1, 1, 0, 0);
FutureCallback<None> shutdownCallback = new FutureCallback<>();
pool.shutdown(shutdownCallback);
shutdownCallback.get(SHUTDOWN_TIMEOUT, TIME_UNIT);
}
@Test
public void testDisposeDestroyFails() throws Exception
{
final LifecycleMock lifecycle = new LifecycleMock();
lifecycle.setDestroyConsumer(callback -> callback.onError(new Exception("Simulated destroy failure")));
AsyncSharedPoolImpl<Object> pool = new AsyncSharedPoolImpl<>(
POOL_NAME, lifecycle, SCHEDULER, LIMITER, NO_POOL_TIMEOUT, MAX_WAITERS);
pool.start();
FutureCallback<Object> getCallback = new FutureCallback<>();
pool.get(getCallback);
Object item = getCallback.get(GET_TIMEOUT, TIME_UNIT);
Assert.assertNotNull(item);
pool.dispose(item);
verifyStats(pool.getStats(), 0, 0, 0, 0, 1, 1, 1, 0, 0);
FutureCallback<None> shutdownCallback = new FutureCallback<>();
pool.shutdown(shutdownCallback);
shutdownCallback.get(SHUTDOWN_TIMEOUT, TIME_UNIT);
}
@Test
public void testPutDestroyedItem() throws Exception
{
AsyncSharedPoolImpl<Object> pool = new AsyncSharedPoolImpl<>(
POOL_NAME, LIFECYCLE, SCHEDULER, LIMITER, NO_POOL_TIMEOUT, MAX_WAITERS);
pool.start();
FutureCallback<Object> getCallback1 = new FutureCallback<>();
pool.get(getCallback1);
Object item1 = getCallback1.get(GET_TIMEOUT, TIME_UNIT);
FutureCallback<Object> getCallback2 = new FutureCallback<>();
pool.get(getCallback2);
Object item2 = getCallback2.get(GET_TIMEOUT, TIME_UNIT);
Assert.assertSame(item1, item2);
verifyStats(pool.getStats(), 1, 2, 0, 0, 0, 0, 1, 0, 0);
pool.dispose(item1);
verifyStats(pool.getStats(), 0, 0, 0, 0, 0, 0, 1, 0, 0);
pool.put(item2);
verifyStats(pool.getStats(), 0, 0, 0, 1, 0, 1, 1, 0, 0);
FutureCallback<None> shutdownCallback = new FutureCallback<>();
pool.shutdown(shutdownCallback);
shutdownCallback.get(SHUTDOWN_TIMEOUT, TIME_UNIT);
}
@Test(expectedExceptions = IllegalArgumentException.class)
public void testPutItemMismatch() throws Exception
{
AsyncSharedPoolImpl<Object> pool = new AsyncSharedPoolImpl<>(
POOL_NAME, LIFECYCLE, SCHEDULER, LIMITER, NO_POOL_TIMEOUT, MAX_WAITERS);
pool.start();
FutureCallback<Object> getCallback = new FutureCallback<>();
pool.get(getCallback);
getCallback.get(GET_TIMEOUT, TIME_UNIT);
// Returns another item reference
pool.put(new Object());
}
@Test
public void testDisposeDestroyedItem() throws Exception
{
AsyncSharedPoolImpl<Object> pool = new AsyncSharedPoolImpl<>(
POOL_NAME, LIFECYCLE, SCHEDULER, LIMITER, NO_POOL_TIMEOUT, MAX_WAITERS);
pool.start();
FutureCallback<Object> getCallback1 = new FutureCallback<>();
pool.get(getCallback1);
Object item1 = getCallback1.get(GET_TIMEOUT, TIME_UNIT);
FutureCallback<Object> getCallback2 = new FutureCallback<>();
pool.get(getCallback2);
Object item2 = getCallback2.get(GET_TIMEOUT, TIME_UNIT);
Assert.assertSame(item1, item2);
verifyStats(pool.getStats(), 1, 2, 0, 0, 0, 0, 1, 0, 0);
pool.dispose(item1);
verifyStats(pool.getStats(), 0, 0, 0, 0, 0, 0, 1, 0, 0);
pool.dispose(item2);
verifyStats(pool.getStats(), 0, 0, 0, 1, 0, 1, 1, 0, 0);
FutureCallback<None> shutdownCallback = new FutureCallback<>();
pool.shutdown(shutdownCallback);
shutdownCallback.get(SHUTDOWN_TIMEOUT, TIME_UNIT);
}
@Test(expectedExceptions = IllegalArgumentException.class)
public void testDestroyItemMismatch() throws Exception
{
AsyncSharedPoolImpl<Object> pool = new AsyncSharedPoolImpl<>(
POOL_NAME, LIFECYCLE, SCHEDULER, LIMITER, NO_POOL_TIMEOUT, MAX_WAITERS);
pool.start();
FutureCallback<Object> getCallback = new FutureCallback<>();
pool.get(getCallback);
Object item = getCallback.get(GET_TIMEOUT, TIME_UNIT);
// Disposes another item reference
pool.dispose(new Object());
}
@Test(expectedExceptions = IllegalArgumentException.class)
public void testExcessivePut() throws Exception
{
AsyncSharedPoolImpl<Object> pool = new AsyncSharedPoolImpl<>(
POOL_NAME, LIFECYCLE, SCHEDULER, LIMITER, NO_POOL_TIMEOUT, MAX_WAITERS);
pool.start();
FutureCallback<Object> getCallback = new FutureCallback<>();
pool.get(getCallback);
Object item = getCallback.get(GET_TIMEOUT, TIME_UNIT);
pool.put(item);
// Excessive put
pool.put(item);
}
@Test(expectedExceptions = IllegalArgumentException.class)
public void testExcessiveDestroy() throws Exception
{
AsyncSharedPoolImpl<Object> pool = new AsyncSharedPoolImpl<>(
POOL_NAME, LIFECYCLE, SCHEDULER, LIMITER, NO_POOL_TIMEOUT, MAX_WAITERS);
pool.start();
FutureCallback<Object> getCallback = new FutureCallback<>();
pool.get(getCallback);
Object item = getCallback.get(GET_TIMEOUT, TIME_UNIT);
pool.dispose(item);
// Excessive destroy of item
pool.dispose(item);
}
@DataProvider(name = "lifecycles")
public Object[][] lifecycleProvider()
{
Random random = new Random(System.currentTimeMillis());
return new Object[][]
{
{ new LifecycleMock() },
{ new LifecycleMock().setCreateConsumer(callback -> callback.onError(CREATE_ERROR)) },
{ new LifecycleMock().setValidateGetSupplier(() -> false) },
{ new LifecycleMock().setValidatePutSupplier(() -> false) },
{ new LifecycleMock()
.setValidateGetSupplier(() -> false)
.setValidatePutSupplier(() -> false) },
{ new LifecycleMock()
.setCreateConsumer(callback -> callback.onError(CREATE_ERROR))
.setValidateGetSupplier(() -> false)
.setValidatePutSupplier(() -> false) },
{ new LifecycleMock()
.setCreateConsumer(callback -> callback.onError(CREATE_ERROR))
.setValidatePutSupplier(() -> false) },
{ new LifecycleMock()
.setCreateConsumer(callback -> callback.onError(CREATE_ERROR))
.setValidateGetSupplier(() -> false) },
{ new LifecycleMock()
.setCreateConsumer(callback -> {
if (random.nextBoolean())
{
callback.onSuccess(new Object());
}
else
{
callback.onError(new Exception("Simulated create failure"));
}
})
.setValidateGetSupplier(() -> random.nextBoolean())
.setValidatePutSupplier(() -> random.nextBoolean()) },
};
}
@Test(dataProvider = "lifecycles")
public void testMaximumConcurrency(AsyncPool.Lifecycle<Object> lifecycle) throws Exception
{
final AsyncSharedPoolImpl<Object> pool = new AsyncSharedPoolImpl<>(
POOL_NAME, lifecycle, SCHEDULER, LIMITER, NO_POOL_TIMEOUT, MAX_WAITERS);
pool.start();
CountDownLatch latch = new CountDownLatch(NUMBER_OF_THREADS);
IntStream.range(0, NUMBER_OF_THREADS).forEach(i -> SCHEDULER.execute(() -> {
try
{
FutureCallback<Object> callback = new FutureCallback<>();
Assert.assertNotNull(pool.get(callback));
Object item = callback.get(GET_TIMEOUT, TIME_UNIT);
Assert.assertNotNull(item);
pool.put(item);
}
catch (Exception e)
{
}
finally
{
latch.countDown();
}
}));
if (!latch.await(OPERATION_TIMEOUT, TIME_UNIT))
{
Assert.fail("Timed out before tasks finish");
}
PoolStats stats = pool.getStats();
System.err.println("Total Created: " + stats.getTotalCreated());
FutureCallback<None> shutdownCallback = new FutureCallback<>();
pool.shutdown(shutdownCallback);
shutdownCallback.get(SHUTDOWN_TIMEOUT, TIME_UNIT);
}
private static void verifyStats(PoolStats stats, int poolSize, int checkedOut, int idles, int destroyed,
int destroyErrors, int badDestroyed, int created, int createErrors, int timeout)
{
Assert.assertNotNull(stats);
Assert.assertEquals(stats.getPoolSize(), poolSize);
Assert.assertEquals(stats.getCheckedOut(), checkedOut);
Assert.assertEquals(stats.getIdleCount(), idles);
Assert.assertEquals(stats.getTotalDestroyed(), destroyed);
Assert.assertEquals(stats.getTotalDestroyErrors(), destroyErrors);
Assert.assertEquals(stats.getTotalBadDestroyed(), badDestroyed);
Assert.assertEquals(stats.getTotalCreated(), created);
Assert.assertEquals(stats.getTotalCreateErrors(), createErrors);
Assert.assertEquals(stats.getTotalTimedOut(), timeout);
}
public static class LifecycleMock implements AsyncPool.Lifecycle<Object>
{
private final AsyncPoolLifecycleStats LIFECYCLE_STATS = new AsyncPoolLifecycleStats(0, 0, 0, 0);
private Consumer<Callback<Object>> _createConsumer;
private Consumer<Callback<Object>> _destroyConsumer;
private Supplier<Boolean> _validateGetSupplier;
private Supplier<Boolean> _validatePutSupplier;
private Supplier<AsyncPoolLifecycleStats> _statsSupplier;
public LifecycleMock()
{
_createConsumer = null;
_destroyConsumer = null;
_validateGetSupplier = () -> true;
_validatePutSupplier = () -> true;
_statsSupplier = () -> LIFECYCLE_STATS;
}
@Override
public void create(Callback<Object> callback)
{
if (_createConsumer == null)
{
callback.onSuccess(new Object());
return;
}
_createConsumer.accept(callback);
}
@Override
public boolean validateGet(Object item)
{
return _validateGetSupplier.get();
}
@Override
public boolean validatePut(Object item)
{
return _validatePutSupplier.get();
}
@Override
public void destroy(Object item, boolean error, Callback<Object> callback)
{
if (_destroyConsumer == null)
{
callback.onSuccess(item);
return;
}
_destroyConsumer.accept(callback);
}
@Override
public PoolStats.LifecycleStats getStats()
{
return _statsSupplier.get();
}
public LifecycleMock setCreateConsumer(Consumer<Callback<Object>> createConsumer)
{
_createConsumer = createConsumer;
return this;
}
public LifecycleMock setDestroyConsumer(Consumer<Callback<Object>> destroyConsumer)
{
_destroyConsumer = destroyConsumer;
return this;
}
public LifecycleMock setValidateGetSupplier(Supplier<Boolean> validateGetSupplier)
{
_validateGetSupplier = validateGetSupplier;
return this;
}
public LifecycleMock setValidatePutSupplier(Supplier<Boolean> validatePutSupplier)
{
_validatePutSupplier = validatePutSupplier;
return this;
}
public LifecycleMock setStatsSupplier(Supplier<AsyncPoolLifecycleStats> statsSupplier)
{
_statsSupplier = statsSupplier;
return this;
}
}
}