/*
Copyright (c) 2012 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.r2.transport.http.client.AsyncPool;
import com.linkedin.r2.transport.http.client.AsyncPoolImpl;
import com.linkedin.common.util.None;
import com.linkedin.r2.transport.http.client.PoolStats;
import org.testng.Assert;
import org.testng.annotations.AfterClass;
import org.testng.annotations.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* @author Steven Ihde
* @version $Revision: $
*/
public class TestAsyncPool
{
private ScheduledExecutorService _executor = Executors.newSingleThreadScheduledExecutor();
@AfterClass
public void stopExecutor()
{
_executor.shutdown();
}
@Test
public void testMustStart() throws TimeoutException, InterruptedException
{
AsyncPool<Object> pool = new AsyncPoolImpl<Object>("object pool",
new SynchronousLifecycle(),
1,
100,
_executor
);
FutureCallback<Object> cb = new FutureCallback<Object>();
pool.get(cb);
try
{
cb.get(30, TimeUnit.SECONDS);
Assert.fail("Get succeeded on pool not yet started");
}
catch (ExecutionException e)
{
// This is what we expect
}
}
@Test
public void testCreate()
{
AsyncPool<Object> pool = new AsyncPoolImpl<Object>("object pool",
new SynchronousLifecycle(),
1,
100,
_executor
);
pool.start();
FutureCallback<Object> cb = new FutureCallback<Object>();
pool.get(cb);
try
{
Object o = cb.get();
Assert.assertNotNull(o);
}
catch (Exception e)
{
Assert.fail("Could not get object from pool", e);
}
}
@Test
public void testMaxSize()
{
final int ITERATIONS = 1000;
final int THREADS = 100;
final int POOL_SIZE = 25;
final int DELAY = 1;
SynchronousLifecycle lifecycle = new SynchronousLifecycle();
final AsyncPool<Object> pool = new AsyncPoolImpl<Object>("object pool",
lifecycle,
POOL_SIZE,
100,
_executor
);
pool.start();
Runnable r = new Runnable()
{
@Override
public void run()
{
for (int i = 0; i < ITERATIONS; i++)
{
FutureCallback<Object> cb = new FutureCallback<Object>();
pool.get(cb);
try
{
Object o = cb.get();
if (DELAY > 0)
{
Thread.sleep(DELAY);
}
pool.put(o);
}
catch (Exception e)
{
Assert.fail("Unexpected failure", e);
}
}
}
};
List<Thread> threads = new ArrayList<Thread>(THREADS);
for (int i = 0; i < THREADS; i++)
{
Thread t = new Thread(r);
t.start();
threads.add(t);
}
for (Thread t : threads)
{
try
{
t.join();
}
catch (InterruptedException e)
{
Assert.fail("Unexpected interruption", e);
}
}
Assert.assertTrue(lifecycle.getHighWaterMark() <= POOL_SIZE, "High water mark exceeded " + POOL_SIZE);
}
@Test
public void testShutdown()
{
final int POOL_SIZE = 25;
final int CHECKOUT = POOL_SIZE;
SynchronousLifecycle lifecycle = new SynchronousLifecycle();
final AsyncPool<Object> pool = new AsyncPoolImpl<Object>("object pool",
lifecycle,
POOL_SIZE,
100,
_executor
);
pool.start();
List<Object> objects = new ArrayList<Object>(CHECKOUT);
for (int i = 0; i < CHECKOUT; i++)
{
FutureCallback<Object> cb = new FutureCallback<Object>();
pool.get(cb);
try
{
Object o = cb.get();
Assert.assertNotNull(o);
objects.add(o);
}
catch (Exception e)
{
Assert.fail("unexpected error", e);
}
}
FutureCallback<None> shutdown = new FutureCallback<None>();
pool.shutdown(shutdown);
for (Object o : objects)
{
Assert.assertFalse(shutdown.isDone(), "Pool shutdown with objects checked out");
pool.put(o);
}
try
{
shutdown.get();
}
catch (Exception e)
{
Assert.fail("unexpected error", e);
}
}
@Test
public void testLRU() throws Exception
{
final int POOL_SIZE = 25;
final int GET = 15;
SynchronousLifecycle lifecycle = new SynchronousLifecycle();
final AsyncPool<Object> pool = new AsyncPoolImpl<Object>("object pool",
lifecycle, POOL_SIZE, 1000, _executor, _executor, Integer.MAX_VALUE, AsyncPoolImpl.Strategy.LRU, 0);
pool.start();
ArrayList<Object> objects = new ArrayList<Object>();
for(int i = 0; i < GET; i++)
{
FutureCallback<Object>cb = new FutureCallback<Object>();
pool.get(cb);
objects.add(cb.get());
}
// put the objects back
for(int i = 0; i < GET; i++)
{
pool.put(objects.get(i));
}
// we should get the same objects back in FIFO order
for(int i = 0; i < GET; i++)
{
FutureCallback<Object> cb = new FutureCallback<Object>();
pool.get(cb);
Assert.assertEquals(cb.get(), objects.get(i));
}
}
@Test
public void testMinSize() throws Exception
{
final int POOL_SIZE = 25;
final int MIN_SIZE = 15;
final int GET = 20;
final int DELAY = 1200;
// Test every strategy
for(AsyncPoolImpl.Strategy strategy : AsyncPoolImpl.Strategy.values()) {
SynchronousLifecycle lifecycle = new SynchronousLifecycle();
final AsyncPool<Object> pool = new AsyncPoolImpl<Object>("object pool",
lifecycle, POOL_SIZE, 100, _executor, _executor, Integer.MAX_VALUE, strategy, MIN_SIZE);
pool.start();
Assert.assertEquals(lifecycle.getLive(), MIN_SIZE);
ArrayList<Object> objects = new ArrayList<Object>();
for(int i = 0; i < GET; i++)
{
FutureCallback<Object>cb = new FutureCallback<Object>();
pool.get(cb);
objects.add(cb.get());
}
Assert.assertEquals(lifecycle.getLive(), GET);
for(int i = 0; i < GET; i++)
{
pool.put(objects.remove(objects.size()-1));
}
Thread.sleep(DELAY);
Assert.assertEquals(lifecycle.getLive(), MIN_SIZE);
}
}
@Test
public void testGetStats() throws Exception
{
final int POOL_SIZE = 25;
final int GET = 20;
final int PUT_GOOD = 2;
final int PUT_BAD = 3;
final int DISPOSE = 4;
final int TIMEOUT = 100;
final int DELAY = 1200;
final UnreliableLifecycle lifecycle = new UnreliableLifecycle();
final AsyncPool<AtomicBoolean> pool = new AsyncPoolImpl<AtomicBoolean>(
"object pool", lifecycle, POOL_SIZE, TIMEOUT, _executor
);
PoolStats stats;
final List<AtomicBoolean> objects = new ArrayList<AtomicBoolean>();
pool.start();
// test values at initialization
stats = pool.getStats();
Assert.assertEquals(stats.getTotalCreated(), 0);
Assert.assertEquals(stats.getTotalDestroyed(), 0);
Assert.assertEquals(stats.getTotalCreateErrors(), 0);
Assert.assertEquals(stats.getTotalDestroyErrors(), 0);
Assert.assertEquals(stats.getCheckedOut(), 0);
Assert.assertEquals(stats.getTotalTimedOut(), 0);
Assert.assertEquals(stats.getTotalBadDestroyed(), 0);
Assert.assertEquals(stats.getMaxPoolSize(), POOL_SIZE);
Assert.assertEquals(stats.getMinPoolSize(), 0);
Assert.assertEquals(stats.getPoolSize(), 0);
Assert.assertEquals(stats.getSampleMaxCheckedOut(), 0);
Assert.assertEquals(stats.getSampleMaxPoolSize(), 0);
// do a few gets
for(int i = 0; i < GET; i++)
{
FutureCallback<AtomicBoolean> cb = new FutureCallback<AtomicBoolean>();
pool.get(cb);
AtomicBoolean obj = cb.get();
objects.add(obj);
}
stats = pool.getStats();
Assert.assertEquals(stats.getTotalCreated(), GET);
Assert.assertEquals(stats.getTotalDestroyed(), 0);
Assert.assertEquals(stats.getTotalCreateErrors(), 0);
Assert.assertEquals(stats.getTotalDestroyErrors(), 0);
Assert.assertEquals(stats.getCheckedOut(), GET);
Assert.assertEquals(stats.getTotalTimedOut(), 0);
Assert.assertEquals(stats.getTotalBadDestroyed(), 0);
Assert.assertEquals(stats.getMaxPoolSize(), POOL_SIZE);
Assert.assertEquals(stats.getMinPoolSize(), 0);
Assert.assertEquals(stats.getPoolSize(), GET);
Assert.assertEquals(stats.getSampleMaxCheckedOut(), GET);
Assert.assertEquals(stats.getSampleMaxPoolSize(), GET);
// do some puts with good objects
for(int i = 0; i < PUT_GOOD; i++)
{
AtomicBoolean obj = objects.remove(objects.size()-1);
pool.put(obj);
}
stats = pool.getStats();
Assert.assertEquals(stats.getTotalCreated(), GET);
Assert.assertEquals(stats.getTotalDestroyed(), 0);
Assert.assertEquals(stats.getTotalCreateErrors(), 0);
Assert.assertEquals(stats.getTotalDestroyErrors(), 0);
Assert.assertEquals(stats.getCheckedOut(), GET - PUT_GOOD);
Assert.assertEquals(stats.getTotalTimedOut(), 0);
Assert.assertEquals(stats.getTotalBadDestroyed(), 0);
Assert.assertEquals(stats.getMaxPoolSize(), POOL_SIZE);
Assert.assertEquals(stats.getMinPoolSize(), 0);
Assert.assertEquals(stats.getPoolSize(), GET);
Assert.assertEquals(stats.getSampleMaxCheckedOut(), GET);
Assert.assertEquals(stats.getSampleMaxPoolSize(), GET);
// do some puts with bad objects
for(int i = 0; i < PUT_BAD; i++)
{
AtomicBoolean obj = objects.remove(objects.size()-1);
obj.set(false); // invalidate the object
pool.put(obj);
}
stats = pool.getStats();
Assert.assertEquals(stats.getTotalCreated(), GET);
Assert.assertEquals(stats.getTotalDestroyed(), PUT_BAD);
Assert.assertEquals(stats.getTotalCreateErrors(), 0);
Assert.assertEquals(stats.getTotalDestroyErrors(), 0);
Assert.assertEquals(stats.getCheckedOut(), GET - PUT_GOOD - PUT_BAD);
Assert.assertEquals(stats.getTotalTimedOut(), 0);
Assert.assertEquals(stats.getTotalBadDestroyed(), PUT_BAD);
Assert.assertEquals(stats.getMaxPoolSize(), POOL_SIZE);
Assert.assertEquals(stats.getMinPoolSize(), 0);
Assert.assertEquals(stats.getPoolSize(), GET - PUT_BAD);
Assert.assertEquals(stats.getSampleMaxCheckedOut(), GET - PUT_GOOD);
Assert.assertEquals(stats.getSampleMaxPoolSize(), GET);
// do some disposes
for(int i = 0; i < DISPOSE; i++)
{
AtomicBoolean obj = objects.remove(objects.size() - 1);
pool.dispose(obj);
}
stats = pool.getStats();
Assert.assertEquals(stats.getTotalCreated(), GET);
Assert.assertEquals(stats.getTotalDestroyed(), PUT_BAD + DISPOSE);
Assert.assertEquals(stats.getTotalCreateErrors(), 0);
Assert.assertEquals(stats.getTotalDestroyErrors(), 0);
Assert.assertEquals(stats.getCheckedOut(), GET - PUT_GOOD - PUT_BAD - DISPOSE);
Assert.assertEquals(stats.getTotalTimedOut(), 0);
Assert.assertEquals(stats.getTotalBadDestroyed(), PUT_BAD + DISPOSE);
Assert.assertEquals(stats.getMaxPoolSize(), POOL_SIZE);
Assert.assertEquals(stats.getMinPoolSize(), 0);
Assert.assertEquals(stats.getPoolSize(), GET - PUT_BAD - DISPOSE);
Assert.assertEquals(stats.getSampleMaxCheckedOut(), GET - PUT_GOOD - PUT_BAD);
Assert.assertEquals(stats.getSampleMaxPoolSize(), GET - PUT_BAD);
// wait for a reap -- should destroy the PUT_GOOD objects
Thread.sleep(DELAY);
stats = pool.getStats();
Assert.assertEquals(stats.getTotalCreated(), GET);
Assert.assertEquals(stats.getTotalDestroyed(), PUT_GOOD + PUT_BAD + DISPOSE);
Assert.assertEquals(stats.getTotalCreateErrors(), 0);
Assert.assertEquals(stats.getTotalDestroyErrors(), 0);
Assert.assertEquals(stats.getCheckedOut(), GET - PUT_GOOD - PUT_BAD - DISPOSE);
Assert.assertEquals(stats.getTotalTimedOut(), PUT_GOOD);
Assert.assertEquals(stats.getTotalBadDestroyed(), PUT_BAD + DISPOSE);
Assert.assertEquals(stats.getMaxPoolSize(), POOL_SIZE);
Assert.assertEquals(stats.getMinPoolSize(), 0);
Assert.assertEquals(stats.getPoolSize(), GET - PUT_GOOD - PUT_BAD - DISPOSE);
Assert.assertEquals(stats.getSampleMaxCheckedOut(), GET - PUT_GOOD - PUT_BAD - DISPOSE);
Assert.assertEquals(stats.getSampleMaxPoolSize(), GET - PUT_BAD - DISPOSE);
}
@Test
public void testGetStatsWithErrors() throws Exception
{
final int POOL_SIZE = 25;
final int GET = 20;
final int PUT_BAD = 5;
final int DISPOSE = 7;
final int CREATE_BAD = 9;
final int TIMEOUT = 100;
final UnreliableLifecycle lifecycle = new UnreliableLifecycle();
final AsyncPool<AtomicBoolean> pool = new AsyncPoolImpl<AtomicBoolean>(
"object pool", lifecycle, POOL_SIZE, TIMEOUT, _executor
);
PoolStats stats;
final List<AtomicBoolean> objects = new ArrayList<AtomicBoolean>();
pool.start();
// do a few gets
for(int i = 0; i < GET; i++)
{
FutureCallback<AtomicBoolean> cb = new FutureCallback<AtomicBoolean>();
pool.get(cb);
AtomicBoolean obj = cb.get();
objects.add(obj);
}
// put and destroy some, with errors
lifecycle.setFail(true);
for(int i = 0; i < PUT_BAD; i++)
{
AtomicBoolean obj = objects.remove(objects.size() - 1);
obj.set(false);
pool.put(obj);
}
for(int i = 0; i < DISPOSE; i++)
{
AtomicBoolean obj = objects.remove(objects.size() - 1);
pool.dispose(obj);
}
stats = pool.getStats();
Assert.assertEquals(stats.getTotalDestroyed(), 0);
Assert.assertEquals(stats.getTotalCreateErrors(), 0);
Assert.assertEquals(stats.getTotalDestroyErrors(), PUT_BAD + DISPOSE);
Assert.assertEquals(stats.getTotalBadDestroyed(), PUT_BAD + DISPOSE);
// create some with errors
for(int i = 0; i < CREATE_BAD; i++)
{
FutureCallback<AtomicBoolean> cb = new FutureCallback<AtomicBoolean>();
try
{
pool.get(cb);
}
catch (Exception e)
{
// this error is expected
}
}
stats = pool.getStats();
Assert.assertEquals(stats.getCheckedOut(), GET - PUT_BAD - DISPOSE);
// When the each create fails, it will retry and cancel the waiter,
// resulting in a second create error.
Assert.assertEquals(stats.getTotalCreateErrors(), 2*CREATE_BAD);
}
@Test
public void testWaitTimeStats() throws Exception
{
final int POOL_SIZE = 25;
final int CHECKOUT = POOL_SIZE;
final long DELAY = 100;
final double DELTA = 0.1;
DelayedLifecycle lifecycle = new DelayedLifecycle(DELAY);
final AsyncPool<Object> pool = new AsyncPoolImpl<Object>("object pool",
lifecycle,
POOL_SIZE,
100,
_executor
);
pool.start();
PoolStats stats;
List<Object> objects = new ArrayList<Object>(CHECKOUT);
for (int i = 0; i < CHECKOUT; i++)
{
FutureCallback<Object> cb = new FutureCallback<Object>();
pool.get(cb);
Object o = cb.get();
objects.add(o);
}
stats = pool.getStats();
Assert.assertEquals(stats.getWaitTimeAvg(), DELAY, DELTA * DELAY);
}
public static class SynchronousLifecycle implements AsyncPool.Lifecycle<Object>
{
private int _live = 0;
private int _highWaterMark = 0;
@Override
public synchronized void create(Callback<Object> callback)
{
_live++;
if (_highWaterMark < _live)
{
_highWaterMark = _live;
}
callback.onSuccess(new Object());
}
@Override
public boolean validateGet(Object obj)
{
return true;
}
@Override
public boolean validatePut(Object obj)
{
return true;
}
@Override
public synchronized void destroy(Object obj, boolean error, Callback<Object> callback)
{
_live--;
callback.onSuccess(obj);
}
@Override
public PoolStats.LifecycleStats getStats()
{
return null;
}
public int getHighWaterMark()
{
return _highWaterMark;
}
public int getLive()
{
return _live;
}
}
/*
* Allows testing of "bad" objects and create/destroy errors.
*
* A "bad" object is represented by an AtomicBoolean with a false value.
* A "good" object has a true value. setFail controls whether create/destroy
* succeed or fail.
*/
public static class UnreliableLifecycle implements AsyncPool.Lifecycle<AtomicBoolean>
{
private boolean _fail = false;
@Override
public synchronized void create(Callback<AtomicBoolean> callback)
{
if(_fail)
{
callback.onError(new Exception());
}
else
{
callback.onSuccess(new AtomicBoolean(true));
}
}
@Override
public boolean validateGet(AtomicBoolean obj)
{
return obj.get();
}
@Override
public boolean validatePut(AtomicBoolean obj)
{
return obj.get();
}
@Override
public synchronized void destroy(AtomicBoolean obj, boolean error, Callback<AtomicBoolean> callback)
{
if(_fail)
{
callback.onError(new Exception());
}
else
{
callback.onSuccess(obj);
}
}
@Override
public PoolStats.LifecycleStats getStats()
{
return null;
}
public synchronized void setFail(boolean fail)
{
_fail = fail;
}
}
public static class DelayedLifecycle implements AsyncPool.Lifecycle<Object>
{
private final long _delay;
public DelayedLifecycle(long delay)
{
_delay = delay;
}
@Override
public synchronized void create(Callback<Object> callback)
{
try
{
Thread.sleep(_delay);
callback.onSuccess(new Object());
}
catch (InterruptedException e)
{
callback.onError(e);
}
}
@Override
public boolean validateGet(Object obj)
{
return true;
}
@Override
public boolean validatePut(Object obj)
{
return true;
}
@Override
public synchronized void destroy(Object obj, boolean error, Callback<Object> callback)
{
callback.onSuccess(obj);
}
@Override
public PoolStats.LifecycleStats getStats()
{
return null;
}
}
}