/* * Copyright (C) 2011-2014 Chris Vest (mr.chrisvest@gmail.com) * * 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. */ package stormpot; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestRule; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; import javax.management.JMX; import javax.management.MBeanServer; import javax.management.MBeanServerFactory; import javax.management.ObjectName; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.LockSupport; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Consumer; import java.util.function.Function; import static java.util.function.Function.identity; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; import static org.junit.Assume.assumeThat; import static stormpot.AlloKit.*; import static stormpot.AlloKit.$countDown; import static stormpot.AlloKit.$if; import static stormpot.ExpireKit.*; import static stormpot.ExpireKit.$countDown; import static stormpot.ExpireKit.$if; import static stormpot.UnitKit.*; /** * This is the generic test for Pool implementations. The test ensures that * an implementation adheres to the general contract of the Pool interface, * given certain circumstances and standardised configurations. * <p> * Pools may have other properties, and may be configurable to deviate from * the standardised behaviour. However, such properties must not be * observable within the standardised spectrum of configurations. * <p> * The tests for any special properties that a pool may have, must be put in * a pool-specific test case. Do not use assumptions or other tricks to * pollute this test case with tests for pool-specific or non-standard * behaviours and configurations. * <p> * The test case uses theories to apply to the set of possible Pool * implementations. Each implementation must have a PoolFixture, which is * used to construct and initialise the pool, based on a {@link Config}. * <p> * The only assumptions used in this test, is whether the Pool is a * LifecycledPool or not. And most interesting pools are life-cycled. * LifecycledPools can be shut down. This is a required ability, in order to * test a number of behaviours, but also brings about its own set of new * behaviours and flows that needs to be tested for. Those tests are also * included in this test case. * <p> * <strong>Note:</strong> when adding, removing or modifying tests, also * remember to update the {@link Pool} javadoc - especially the part about * what promises are provided by the Pool interface and its implementations. * * @author Chris Vest <mr.chrisvest@gmail.com> * @see Pool */ @RunWith(Parameterized.class) public class PoolTest { private static final int TIMEOUT = 42424; @Rule public final TestRule failurePrinter = new FailurePrinterTestRule(); private static final Expiration<Poolable> oneMsTTL = new TimeExpiration<>(1, TimeUnit.MILLISECONDS); private static final Expiration<Poolable> fiveMsTTL = new TimeExpiration<>(5, TimeUnit.MILLISECONDS); private static final Timeout longTimeout = new Timeout(5, TimeUnit.MINUTES); private static final Timeout mediumTimeout = new Timeout(10, TimeUnit.MILLISECONDS); private static final Timeout shortTimeout = new Timeout(1, TimeUnit.MILLISECONDS); private static final Timeout zeroTimeout = new Timeout(0, TimeUnit.MILLISECONDS); private final PoolFixture fixture; private CountingAllocator allocator; private Config<GenericPoolable> config; private Pool<GenericPoolable> pool; @Parameters(name = "{0}") public static Object[][] dataPoints() { return new Object[][] { {"blazePool", new BlazePoolFixture()}, {"queuePool", new QueuePoolFixture()} }; } @SuppressWarnings("UnusedParameters") public PoolTest(String implementationName, PoolFixture fixture) { this.fixture = fixture; } @Before public void setUp() { allocator = allocator(); config = new Config<GenericPoolable>().setSize(1).setAllocator(allocator); } @After public void shutPoolDown() throws InterruptedException { if (pool != null) { String poolName = pool.getClass().getSimpleName(); assertTrue( "Pool did not shut down within timeout: " + poolName, pool.shutdown().await(longTimeout)); } } private void createPool() { pool = fixture.initPool(config); } private ManagedPool assumeManagedPool() { createPool(); assumeThat(pool, instanceOf(ManagedPool.class)); return (ManagedPool) pool; } @Test(expected = IllegalArgumentException.class) public void timeoutCannotBeNull() throws Exception { createPool(); pool.claim(null); } /** * A call to claim must return before the timeout elapses if it * can claim an object from the pool, and it must return that object. * The timeout for the claim is longer than the timeout for the test, so we * know that we won't get a null back here because the timeout wasn't long * enough. If we do, then the pool does not correctly implement the timeout * behaviour. */ @Test(timeout = TIMEOUT) public void claimMustReturnIfWithinTimeout() throws Exception { createPool(); Poolable obj = pool.claim(longTimeout); try { assertThat(obj, not(nullValue())); } finally { obj.release(); } } /** * A call to claim that fails to get an object before the * timeout elapses, must return null. * We test this by depleting a pool, and then make a call to claim with * a shot timeout. If that call returns <code>null</code>, then we're good. */ @Test(timeout = TIMEOUT) public void claimMustReturnNullIfTimeoutElapses() throws Exception { createPool(); GenericPoolable a = pool.claim(longTimeout); // pool is now depleted Poolable b = pool.claim(shortTimeout); try { assertThat(b, is(nullValue())); } finally { a.release(); } } /** * While the pool mustn't return null when we claim an object within the * timeout period, it likewise mustn't just come up with any random thing * that implements Poolable. * The objects have to come from the associated Allocator. * Or fixtures are required to count all allocations and deallocations, * so we can measure that our intended interactions do, in fact, reach * the Allocator. The PoolFixture will typically do this by wrapping the * source Allocator in a CountingAllocatorWrapper, but that is an * irrelevant detail. */ @Test(timeout = TIMEOUT) public void mustGetPooledObjectsFromAllocator() throws Exception { createPool(); GenericPoolable obj = pool.claim(longTimeout); try { assertThat(allocator.countAllocations(), is(greaterThan(0))); } finally { obj.release(); } } /** * If the pool has been depleted for objects, then a call to claim with * timeout will wait until either an object becomes available, or the timeout * elapses. Whichever comes first. * We test for this by observing that a thread that makes a claim-with-timeout * call to a depleted pool, will enter the TIMED_WAITING state. */ @Test(timeout = TIMEOUT) public void blockingClaimWithTimeoutMustWaitIfPoolIsEmpty() throws Exception { createPool(); GenericPoolable obj = pool.claim(longTimeout); assertNotNull("Did not deplete pool in time", obj); AtomicReference<GenericPoolable> ref = new AtomicReference<>(); Thread thread = fork(capture($claim(pool, longTimeout), ref)); try { waitForThreadState(thread, Thread.State.TIMED_WAITING); } finally { obj.release(); thread.join(); ref.get().release(); } } /** * A thread that is waiting in claim-with-timeout on a depleted pool must * wake up if another thread releases an object back into the pool. * So if we deplete a pool, make a thread wait in claim-with-timeout and * then release an object back into the pool, then we must be able to join * to that thread. */ @Test(timeout = TIMEOUT) public void blockingOnClaimWithTimeoutMustResumeWhenPoolablesAreReleased() throws Exception { createPool(); Poolable obj = pool.claim(longTimeout); assertNotNull("Did not deplete pool in time", obj); AtomicReference<GenericPoolable> ref = new AtomicReference<>(); Thread thread = fork(capture($claim(pool, longTimeout), ref)); waitForThreadState(thread, Thread.State.TIMED_WAITING); obj.release(); join(thread); ref.get().release(); } /** * One uses a pool because a certain type of objects are expensive to * create and we would like to recycle them. So when we claim and object, * then release it back into the pool, and then claim and release it again, * then we must observe that only a single object allocation has taken * place. * The pool has a size of 1, so we can safely base this test on the * allocation count - even for pools that like to eagerly saturate the * pool with objects. */ @Test(timeout = TIMEOUT) public void mustReuseAllocatedObjects() throws Exception { createPool(); pool.claim(longTimeout).release(); pool.claim(longTimeout).release(); assertThat(allocator.countAllocations(), is(1)); } /** * Be careful and prevent the creation of pools with a size less than one. * The contract of claim is to block indefinitely if one such pool were * to be created. * @see Config#setSize(int) */ @Test(timeout = TIMEOUT, expected = IllegalArgumentException.class) public void constructorMustThrowOnPoolSizeLessThanOne() { fixture.initPool(config.setSize(0)); } /** * Prevent the creation of pools with a null Expiration. * @see Config#setExpiration(Expiration) */ @Test(timeout = TIMEOUT, expected = IllegalArgumentException.class) public void constructorMustThrowOnNullExpiration() { fixture.initPool(config.setExpiration(null)); } /** * Prevent the creation of pools with a null Allocator. * @see Config#setAllocator(Allocator) */ @Test(timeout = TIMEOUT, expected = IllegalArgumentException.class) public void constructorMustThrowOnNullAllocator() { fixture.initPool(config.setAllocator(null)); } /** * Prevent the creation of pools with a null ThreadFactory. * @see Config#setThreadFactory(java.util.concurrent.ThreadFactory) */ @Test(timeout = TIMEOUT, expected = IllegalArgumentException.class) public void constructorMustThrowOnNullThreadFactory() { fixture.initPool(config.setThreadFactory(null)); } @Test(timeout = TIMEOUT, expected = NullPointerException.class) public void constructorMustThrowIfConfiguredThreadFactoryReturnsNull() { ThreadFactory factory = r -> null; config.setThreadFactory(factory); createPool(); } /** * Pools must use the provided expiration to determine whether slots * are invalid or not, instead of using their own ad-hoc mechanisms. * We test this by using an expiration that counts the number of times * it is invoked. Then we claim an object and assert that the expiration * was invoked at least once, presumably for that object. */ @Test(timeout = TIMEOUT) public void mustUseProvidedExpiration() throws Exception { config.setAllocator(allocator()); CountingExpiration expiration = expire($fresh); config.setExpiration(expiration); createPool(); pool.claim(longTimeout).release(); assertThat(expiration.countExpirations(), greaterThanOrEqualTo(1)); } /** * In the hopefully unlikely event that an Expiration throws an * exception, that exception should bubble out of the pool unspoiled. * * We test for this by configuring an Expiration that always throws. * No guarantees are being made about when, exactly, it is that the pool will * invoke the Expiration. Therefore we claim and release an object a * couple of times. That ought to do it. */ @Test(timeout = TIMEOUT, expected = SomeRandomException.class) public void exceptionsFromExpirationMustBubbleOut() throws Throwable { config.setExpiration(expire($throwExpire(new SomeRandomException()))); createPool(); try { // make a couple of calls because pools might optimise for freshly // created objects pool.claim(longTimeout).release(); pool.claim(longTimeout).release(); } catch (PoolException e) { throw e.getCause(); } } /** * If the Expiration throws an exception when evaluating a slot, then that * slot should be considered invalid. * * We test for this by configuring an expiration that always throws, * and then we make a claim and a release to make sure that it got invoked. * Then, since the pool size is one, we make another claim to make sure that * the invalid slot got reallocated. We don't care if that second claim * throws or not. All we're interested in, is whether the deallocation took * place. */ @Test(timeout = TIMEOUT) public void slotsThatMakeTheExpirationThrowAreInvalid() throws Exception { config.setExpiration(expire($throwExpire(new SomeRandomException()))); createPool(); try { pool.claim(longTimeout); fail("should throw"); } catch (PoolException e) { assertThat(e.getCause(), instanceOf(SomeRandomException.class)); } // second call to claim to ensure that the deallocation has taken place try { pool.claim(longTimeout); } catch (PoolException e) { assertThat(e.getCause(), instanceOf(SomeRandomException.class)); } // must have deallocated that object assertThat(allocator.countDeallocations(), greaterThanOrEqualTo(1)); } /** * SlotInfo objects offer a count of how many times the Poolable it * represents, has been claimed. Naturally, this count must increase every * time that object is claimed. * We test for this by creating an Expiration that writes the count to * an atomic every time it is called. Then we make a couple of claims and * releases, and assert that the recorded count has gone up. */ @Test(timeout = TIMEOUT) public void slotInfoClaimCountMustIncreaseWithClaims() throws Exception { final AtomicLong claims = new AtomicLong(); config.setExpiration(expire($capture($claimCount(claims), $fresh))); createPool(); pool.claim(longTimeout).release(); pool.claim(longTimeout).release(); pool.claim(longTimeout).release(); // We have made claims, and the expiration ought to have noted this. // We should observe a claim-count of 2, rather than 3, because the // expiration only gets to see past claims, not the one that is being // processed at the time the expiration check happens. assertThat(claims.get(), is(2L)); } /** * Expirations might require access to the actual object in question * being pooled, in order to implement advanced and/or domain specific * logic. * As with the claim count test above, we configure an expiration * that puts the value into an atomic. Then we assert that the value of the * atomic is one of the claimed objects. */ @Test(timeout = TIMEOUT) public void slotInfoMustHaveReferenceToItsPoolable() throws Exception { final AtomicReference<Poolable> lastPoolable = new AtomicReference<>(); config.setExpiration(expire($capture($poolable(lastPoolable), $fresh))); createPool(); GenericPoolable a = pool.claim(longTimeout); a.release(); GenericPoolable b = pool.claim(longTimeout); b.release(); GenericPoolable poolable = (GenericPoolable) lastPoolable.get(); assertThat(poolable, anyOf(is(a), is(b))); } /** * SlotInfo instances must have a means of acting as a non-contended source * of random numbers. We test this by getting a hold of a SlotInfo instance, * and then pulling a large quantity of random numbers from it. If the * numbers are random, then they will have a roughly even split between ones * and zero bits. */ @Test(timeout = TIMEOUT) public void slotInfoMustBeAbleToProduceRandomNumbers() throws Exception { final AtomicReference<SlotInfo<? extends Poolable>> slotInfoRef = new AtomicReference<>(); config.setExpiration(expire($capture($slotInfo(slotInfoRef), $fresh))); createPool(); pool.claim(longTimeout).release(); // Now we have a SlotInfo reference. SlotInfo<? extends Poolable> slotInfo = slotInfoRef.get(); // A full suite for testing the quality of the PRNG would be excessive here. // We just want a back-of-the-envelope estimate that it's random. int nums = 1000000; int bits = 32 * nums; int ones = 0; for (int i = 0; i < nums; i++) { ones += Integer.bitCount(slotInfo.randomInt()); } // In the random data that we collect, we should see a roughly even split // in the bits between ones and zeros. // So, if we count all the one bits and double that number, we should get // a number that is very close to the total number of random bits generated. double diff = Math.abs(bits - ones * 2); assertThat(diff, lessThan(bits * 0.005)); } /** * Pool implementations might reuse their SlotInfo instances. We need to * make sure that if an object is reallocated, then the claim count for that * slot is reset to zero. * We test for this by configuring an expiration that invalidates * objects that have been claimed more than once, and records the maximum * claim count it observes in an atomic. Then we make more claims than this * limit, and observe that precisely one more than the max have been observed. */ @Test(timeout = TIMEOUT) public void slotInfoClaimCountMustResetIfSlotsAreReused() throws Exception { final AtomicLong maxClaimCount = new AtomicLong(); Expiration<Poolable> expiration = info -> { maxClaimCount.set(Math.max(maxClaimCount.get(), info.getClaimCount())); return info.getClaimCount() > 1; }; config.setExpiration(expiration); createPool(); pool.claim(longTimeout).release(); pool.claim(longTimeout).release(); pool.claim(longTimeout).release(); // we've made 3 claims, while all objects w/ claimCount > 1 are invalid assertThat(maxClaimCount.get(), is(2L)); } /** * Verify that we can read back a stamp value from the SlotInfo that we have * set, when the Slot has not been re-allocated. * We test this with an Expiration that set the stamp value if it is zero, * or set a flag to signify that it has been remembered, if it is the value * we set it to. Then we claim and release a couple of times. If it works, * the second claim+release pair would have raised the flag. */ @Test(timeout = TIMEOUT) public void slotInfoMustRememberStamp() throws Exception { final AtomicBoolean rememberedStamp = new AtomicBoolean(); Expiration<Poolable> expiration = info -> { long stamp = info.getStamp(); if (stamp == 0) { info.setStamp(13); } else if (stamp == 13) { rememberedStamp.set(true); } return false; }; config.setExpiration(expiration); createPool(); pool.claim(longTimeout).release(); // First set it... pool.claim(longTimeout).release(); // ... then get it. assertTrue(rememberedStamp.get()); } /** * The SlotInfo stamp is zero by default. This must also be true of Slots * that has had their object reallocated. So if we set the stamp, then * reallocate the Poolable, then we should observe that the stamp is now back * to zero again. */ @Test(timeout = TIMEOUT) public void slotInfoStampMustResetIfSlotsAreReused() throws Exception { final AtomicLong zeroStampsCounted = new AtomicLong(0); Expiration<Poolable> expiration = info -> { long stamp = info.getStamp(); info.setStamp(15); if (stamp == 0) { zeroStampsCounted.incrementAndGet(); return false; } return true; }; config.setExpiration(expiration); createPool(); pool.claim(longTimeout).release(); pool.claim(longTimeout).release(); pool.claim(longTimeout).release(); assertThat(zeroStampsCounted.get(), is(3L)); } @Test(timeout = TIMEOUT) public void slotInfoMustHaveAgeInMillis() throws InterruptedException { final AtomicLong age = new AtomicLong(); config.setExpiration(expire($capture($age(age), $fresh))); createPool(); pool.claim(longTimeout).release(); long firstAge = age.get(); spinwait(5); pool.claim(longTimeout).release(); long secondAge = age.get(); assertThat(secondAge - firstAge, greaterThanOrEqualTo(5L)); } @Test(timeout = TIMEOUT) public void slotInfoAgeMustResetAfterAllocation() throws InterruptedException { final AtomicBoolean hasExpired = new AtomicBoolean(); final AtomicLong age = new AtomicLong(); config.setExpiration(expire( $capture($age(age), $expiredIf(hasExpired)))); // Reallocations will fail, causing the slot to be poisoned. // Then, the poisoned slot will not be reallocated again, but rather // go through the deallocate-allocate cycle. config.setAllocator(reallocator(realloc($throw(new Exception())))); createPool(); pool.claim(longTimeout).release(); Thread.sleep(100); // time transpires pool.claim(longTimeout).release(); long firstAge = age.get(); // age is now at least 5 ms hasExpired.set(true); try { pool.claim(longTimeout).release(); } catch (Exception e) { // ignore } hasExpired.set(false); // new object should have a new age pool.claim(longTimeout).release(); long secondAge = age.get(); // age should be less than age of prev. obj. assertThat(secondAge, lessThan(firstAge)); } @Test(timeout = TIMEOUT) public void slotInfoAgeMustResetAfterReallocation() throws InterruptedException { final AtomicBoolean hasExpired = new AtomicBoolean(); final AtomicLong age = new AtomicLong(); config.setExpiration(expire( $capture($age(age), $expiredIf(hasExpired)))); createPool(); pool.claim(longTimeout).release(); Thread.sleep(100); // time transpires pool.claim(longTimeout).release(); long firstAge = age.get(); hasExpired.set(true); assertNull(pool.claim(zeroTimeout)); // cause reallocation hasExpired.set(false); pool.claim(longTimeout).release(); // new object, new age long secondAge = age.get(); assertThat(secondAge, lessThan(firstAge)); } /** * It is not possible to claim from a pool that has been shut down. Doing * so will cause an IllegalStateException to be thrown. This must take * effect as soon as shutdown has been called. So the fact that claim() * becomes unusable happens-before the pool shutdown process completes. * The memory effects of this are not tested for, but I don't think it is * possible to implement in a thread-safe manner and not provide the * memory effects that we want. */ @Test(timeout = TIMEOUT, expected = IllegalStateException.class) public void preventClaimFromPoolThatIsShutDown() throws Exception { createPool(); pool.claim(longTimeout).release(); pool.shutdown(); pool.claim(longTimeout); } /** * Objects in the pool only live for a certain amount of time, and then * they must be replaced/renewed. Pools should generally try to renew * before the timeout elapses for the given object, but we don't test for * that here. * We set the TTL to be 1 milliseconds, because that is short enough that * we can wait for it in a spin-loop. This way, the objects will always * appear to have expired when checked. This means that every claim will * always allocate a new object, and so our two claims will translate to * at least two allocations, which is what we check for. Note that the TTL * is so short that newly allocated objects might actually expire before a * claim call can complete, and thus more than the expected two allocations * are possible. This is why we check for *at least* two allocations. */ @Test(timeout = TIMEOUT) public void mustReplaceExpiredPoolables() throws Exception { config.setExpiration(oneMsTTL); createPool(); pool.claim(longTimeout).release(); spinwait(2); pool.claim(longTimeout).release(); assertThat(allocator.countAllocations(), greaterThanOrEqualTo(2)); } /** * The size limit on a pool is strict, unless specially (as in a * non-standard way) configured otherwise. A pool is not allowed to * have more objects allocated than the size, under any circumstances. * So, when the pool renews an object it must make ensure that the * deallocation of the old object happens-before the allocation of the * new object. * We test for this by configuring a pool with a timeout of one millisecond. * Then we claim an release an object, and wait for 2 milliseconds. Now the * object is expired, and must therefore be re-allocated before the next * claim can return. So we do a claim (but no release) and check that we have * had at least one deallocation. Note that our TTL is so short, that an * object might expire before a claim call can complete, so this is why we * check for *at least* one deallocation. * @see Config#setSize(int) */ @Test(timeout = TIMEOUT) public void mustDeallocateExpiredPoolablesAndStayWithinSizeLimit() throws Exception { config.setExpiration(oneMsTTL); createPool(); pool.claim(longTimeout).release(); spinwait(2); pool.claim(longTimeout).release(); assertThat(allocator.countDeallocations(), greaterThanOrEqualTo(1)); } /** * When we call shutdown() on a pool, the shutdown process is initiated and * the call returns a Completion object. A call to await() on this * Completion object will not return until the shutdown process has been * completed. * A shutdown process is not complete until all Poolables in the pool have * been deallocated. This means that any claimed objects must be released, * all the deallocations must have returned. * We test for this effect by making a pool of size 2 and claim both objects. * Then we release them. The order is important, to prevent the allocation * of just one object that is then reused. Then we shut the pool down and * wait for it to finish. After this, we must observe that exactly 2 * deallocations have occurred. */ @Test(timeout = TIMEOUT) public void mustDeallocateAllPoolablesBeforeShutdownTaskReturns() throws Exception { config.setSize(2); createPool(); Poolable p1 = pool.claim(longTimeout); Poolable p2 = pool.claim(longTimeout); p1.release(); p2.release(); pool.shutdown().await(longTimeout); assertThat(allocator.countDeallocations(), is(2)); } /** * So awaiting the shut down completion cannot return before all * claimed objects are both released and deallocated. Likewise, the * initiation of the shut down process - the call to shutdown() - must * decidedly NOT wait for any claimed objects to be released, before the * call returns. * We test for this effect by creating a pool and claiming and object * without ever releasing it. Then we call shutdown, without ever awaiting * its completion. The test passes if this does not dead-lock, hence the * test timeout. */ @Test(timeout = TIMEOUT) public void shutdownCallMustReturnFastIfPoolablesAreStillClaimed() throws Exception { createPool(); GenericPoolable obj = pool.claim(longTimeout); try { pool.shutdown(); assertNotNull("Did not deplete pool in time", obj); } finally { obj.release(); } } /** * We have verified that the call to shutdown on a pool does not block on * claimed objects, and we have verified that all objects are deallocated * when the shut down completes. Now we need to verify that the release of * a claimed objects happens-before that object is deallocated as part of * the shut down process. * We test for this effect by claiming an object from a pool, never to * release it again. Then we initiate the shut down process. We await the * completion of the shut down process with a very short timeout, to be * sure that the process has actually started. This is to thwart any data * race that might otherwise be lurking. Then finally we assert that the * claimed object (the only one allocated) have not been deallocated. */ @Test(timeout = TIMEOUT) public void shutdownMustNotDeallocateClaimedPoolables() throws Exception { createPool(); GenericPoolable obj = pool.claim(longTimeout); assertNotNull("Did not deplete pool in time", obj); pool.shutdown().await(mediumTimeout); try { assertThat(allocator.countDeallocations(), is(0)); } finally { obj.release(); } } /** * We know from the previous test, that awaiting the shut down completion * will wait for any claimed objects to be released. However, once those * objects are released, we must also make sure that the shut down process * actually resumes and eventually completes as a result. * We test this by claiming and object and starting the shut down process. * Then we set another thread to await the completion of the shut down * process, and make sure that it actually enters the WAITING state. * Then we release the claimed object and try to join the thread. If we * manage to join the thread, then the shut down process has completed, and * the test pass if this all happens within the test timeout. * When a thread is in the WAITING state, it means that it is waiting for * another thread to do something that will let it resume. In our case, * the thread is waiting for someone to release the claimed object. */ @Test(timeout = TIMEOUT) public void awaitOnShutdownMustReturnWhenClaimedObjectsAreReleased() throws Exception { createPool(); Poolable obj = pool.claim(longTimeout); Completion completion = pool.shutdown(); Thread thread = fork($await(completion, longTimeout)); waitForThreadState(thread, Thread.State.TIMED_WAITING); obj.release(); join(thread); } /** * The await with timeout on the Completion of the shut down process * must return false if the timeout elapses, as is the typical contract * of such methods in java.util.concurrent. * We are going to assume that the implementation adheres to the requested * timeout within reasonable margins, because the implementations are * probably going to delegate this call to java.util.concurrent anyway. */ @Test(timeout = TIMEOUT) public void awaitWithTimeoutMustReturnFalseIfTimeoutElapses() throws Exception { createPool(); Poolable obj = pool.claim(longTimeout); assertFalse(pool.shutdown().await(shortTimeout)); obj.release(); } /** * We have verified that await with timeout returns false if the timeout * elapses. We also have to make sure that the call returns true if the * shut down process completes within the timeout. * We test for this by claiming an object, start the shut down process, * set a thread to await the completion with a timeout, then release the * claimed object and join the thread. The result will be put in an * AtomicBoolean, which then must contain true after the thread has been * joined. And this must all happen before the test itself times out. */ @Test(timeout = TIMEOUT) public void awaitWithTimeoutMustReturnTrueIfCompletesWithinTimeout() throws Exception { createPool(); Poolable obj = pool.claim(longTimeout); AtomicBoolean result = new AtomicBoolean(false); Completion completion = pool.shutdown(); Thread thread = fork($await(completion, longTimeout, result)); waitForThreadState(thread, Thread.State.TIMED_WAITING); obj.release(); join(thread); assertTrue(result.get()); } /** * We have verified that the await method works as intended, if you * begin your awaiting while the shut down process is still undergoing. * However, we must also make sure that further calls to await after the * shut down process has completed, do not block. * We do this by shutting a pool down, and then make a number of await calls * to the shut down Completion. These calls must all return before the * timeout of the test elapses. */ @Test(timeout = TIMEOUT) public void awaitingOnAlreadyCompletedShutDownMustNotBlock() throws Exception { createPool(); Completion completion = pool.shutdown(); completion.await(longTimeout); completion.await(longTimeout); } /** * A call to claim on a pool that has been, or is in the process of being, * shut down, will throw an IllegalStateException. So should calls that * are blocked on claim when the shut down process is initiated. * To test this, we create a pool with one object and claim it. Then we * set another thread to also claim an object. This thread will block * because the pool has been depleted. To make sure of this, we wait for * the thread to enter the WAITING state. Then we start the shut down * process of the pool, release the object and join the thread we started. * If the call to claim throws an exception in the other thread, then it * will be put in an AtomicReference, and we assert that it is indeed an * IllegalStateException. */ @Test(timeout = TIMEOUT) public void blockedClaimMustThrowWhenPoolIsShutDown() throws Exception { createPool(); AtomicReference<Exception> caught = new AtomicReference<>(); Poolable obj = pool.claim(longTimeout); Thread thread = fork($catchFrom($claim(pool, longTimeout), caught)); waitForThreadState(thread, Thread.State.TIMED_WAITING); pool.shutdown(); obj.release(); join(thread); assertThat(caught.get(), instanceOf(IllegalStateException.class)); } /** * Clients might hold on to objects after they have been released. This is * a user error, but pools must still maintain a coherent allocation and * deallocation pattern toward the Allocator. * We test this by configuring a pool with a short TTL so that the objects * will be deallocated as soon as possible. Then we claim an object, wait * the TTL out and release it numerous times. Then we claim another object * to guarantee that the deallocation of the first object have taken place * when we check the deallocation list for duplicates. The test pass if we * don't find any. */ @Test(timeout = TIMEOUT) public void mustNotDeallocateTheSameObjectMoreThanOnce() throws Exception { config.setExpiration(oneMsTTL); createPool(); Poolable obj = pool.claim(longTimeout); spinwait(2); obj.release(); for (int i = 0; i < 10; i++) { try { obj.release(); } catch (Exception ignore) { // we don't really care if the pool is able to detect this or not // we are still going to check with the Allocator. } } pool.claim(longTimeout).release(); // check if the deallocation list contains duplicates List<GenericPoolable> deallocations = allocator.getDeallocations(); for (Poolable elm : deallocations) { assertThat("Deallocations of " + elm, Collections.frequency(deallocations, elm), is(1)); } } /** * The shutdown procedure might be tempted to blindly iterate the pool * data structure and deallocate every possible slot. However, slots that * are empty should not be deallocated. In fact, the pool should never * try to deallocate any null value. * We attempt to test for this by having a special Allocator that flags * a boolean if a null was deallocated. Then we create a pool with the * Allocator and a negative TTL, and claim and release an object. * Then we shut the pool down. After the shut down procedure completes, * we check that no nulls were deallocated. */ @Test(timeout = TIMEOUT) public void shutdownMustNotDeallocateEmptySlots() throws Exception { final AtomicBoolean wasNull = new AtomicBoolean(); allocator = allocator(dealloc($observeNull(wasNull, $null))); config.setAllocator(allocator).setExpiration(oneMsTTL); createPool(); pool.claim(longTimeout).release(); pool.shutdown().await(longTimeout); assertFalse(wasNull.get()); } @Test(timeout = TIMEOUT) public void shutdownMustEventuallyDeallocateAllPoolables() throws Exception { int size = 10; config.setSize(size); createPool(); List<GenericPoolable> objs = new ArrayList<>(); for (int i = 0; i < size; i++) { objs.add(pool.claim(longTimeout)); } Completion completion = pool.shutdown(); completion.await(shortTimeout); for (GenericPoolable obj : objs) { obj.release(); } completion.await(longTimeout); assertThat(allocator.countDeallocations(), is(size)); } /** * Pools must be prepared in the event that an Allocator throws an * Exception from allocate. If it is not possible to allocate an object, * then the pool must throw a PoolException through claim. * @see PoolException */ @Test(timeout = TIMEOUT) public void mustPropagateExceptionsFromAllocateThroughClaim() throws Exception { final RuntimeException expectedException = new RuntimeException("boo"); allocator = allocator(alloc($throw(expectedException))); config.setAllocator(allocator); createPool(); try { pool.claim(longTimeout); fail("expected claim to throw"); } catch (PoolException poolException) { assertThat(poolException.getCause(), is((Throwable) expectedException)); } } /** * Pools must be prepared in the event that a Reallocator throws an * Exception from reallocate. If it is not possible to reallocate an object, * then the pool must throw a PoolException through claim. * @see PoolException */ @Test(timeout = TIMEOUT) public void mustPropagateExceptionsFromReallocateThroughClaim() throws Exception { final RuntimeException expectedException = new RuntimeException("boo"); AtomicBoolean hasExpired = new AtomicBoolean(); config.setAllocator(reallocator(realloc($throw(expectedException)))); config.setExpiration(expire($expiredIf(hasExpired))); config.setSize(2); createPool(); // Make sure the pool is fully allocated hasExpired.set(false); GenericPoolable obj1 = pool.claim(longTimeout); GenericPoolable obj2 = pool.claim(longTimeout); obj1.release(); obj2.release(); // Now consider it expired, send it back for reallocation, // the reallocation throws and poisons, and the poison gets thrown. // The pool will race to reallocate the poisoned objects, but it won't // win the race, because there are two slots circulating in the pool, // so it should be highly probable that we are able to grab one of them // while the other one is being reallocated. hasExpired.set(true); try { pool.claim(longTimeout); fail("expected claim to throw"); } catch (PoolException poolException) { assertThat(poolException.getCause(), is((Throwable) expectedException)); } } /** * A pool must not break its internal invariants if an Allocator throws an * exception in allocate, and it must still be usable after the exception * has bubbled out. * We test this by configuring an Allocator that throws an exception from * the first allocation, and then allocates as normal if not. On the first * call to claim, we catch the exception that propagates out of the pool * and flip the boolean. Then the next call to claim must return a non-null * object within the test timeout. * If it does not, then the pool might have broken locks or it might have * garbage in the slot location. * The first claim might race with eager reallocation, though. So we don't * assert on the exception, and make sure to release any object we might * get back. */ @Test(timeout = TIMEOUT) public void mustStillBeUsableAfterExceptionInAllocate() throws Exception { allocator = allocator(alloc($throw(new RuntimeException("boo")), $new)); config.setAllocator(allocator); createPool(); try { pool.claim(longTimeout).release(); } catch (PoolException ignore) {} GenericPoolable obj = pool.claim(longTimeout); assertThat(obj, is(notNullValue())); obj.release(); } /** * Likewise as above, a pool must not break its internal invariants if a * Reallocator throws an exception in reallocate, and it must still be * usable after the exception has bubbled out. * We test for this by configuring a Reallocator that always throws on * reallocate, and we also configure an Expiration that will mark the first * slot it checks as expired. Then, when we call claim, that first live slot * will be sent back for reallocation, which will throw and poison the slot. * Then the slot comes back to our still on-going claim, which throws * because of the poison. The slot then gets sent back again, and now, * because of the poison, it will not be reallocated, but instead have a * fresh Poolable allocated anew. This new good Poolable is what we get out * of the last call to claim. */ @Test(timeout = TIMEOUT) public void mustStillBeUsableAfterExceptionInReallocate() throws Exception { final AtomicBoolean throwInAllocate = new AtomicBoolean(); final AtomicBoolean hasExpired = new AtomicBoolean(); final CountDownLatch allocationLatch = new CountDownLatch(2); config.setAllocator(reallocator( alloc($if(throwInAllocate, $throw(new RuntimeException("boo")), $countDown(allocationLatch, $new))), realloc($throw(new RuntimeException("boo"))))); config.setExpiration(expire($expiredIf(hasExpired))); createPool(); pool.claim(longTimeout).release(); // object now allocated throwInAllocate.set(true); hasExpired.set(true); try { pool.claim(longTimeout); fail("claim should have thrown"); } catch (PoolException ignore) {} throwInAllocate.set(false); hasExpired.set(false); allocationLatch.await(); GenericPoolable claim = pool.claim(longTimeout); assertThat(claim, is(notNullValue())); claim.release(); } /** * We cannot guarantee that exceptions from deallocating objects will * propagate out through release, because the actual deallocation might be * done asynchronously in a different thread. * So instead, we are going to guarantee the opposite: that calling release * on an object will never propagate, or even throw, any exceptions ever. * Users of a pool who are interested in logging what exceptions might be * thrown by their allocators deallocate method, are going to have to wrap * their allocators in try-catching and logging code. * We test this by configuring the pool with an Allocator that always throws * on deallocate, and a very short TTL. Then we claim and release an object, * spin the TTL out and then claim another one. This ensures that the * deallocation actually takes place, because full pools guarantee that * the deallocation of an expired object happens before the allocation * of its replacement. */ @Test(timeout = TIMEOUT) public void mustSwallowExceptionsFromDeallocateThroughRelease() throws Exception { allocator = allocator(dealloc($throw(new RuntimeException("boo")))); config.setAllocator(allocator); config.setExpiration(oneMsTTL); createPool(); pool.claim(longTimeout).release(); spinwait(2); pool.claim(longTimeout).release(); } /** * While it is technically possible to propagate exceptions from an * Allocators deallocate method during the shutdown procedure, it would not * be a desirable behaviour because it would be inconsistent with how this * works for the release method on Poolable - and Slot, for that matter. * People who are interested in the exceptions that deallocate might throw, * should wrap their Allocators in implementations that log them. If they * do this, then they will already have a means for accessing the exceptions * thrown. As such, there is no point in also logging the exceptions in the * shut down procedure. * We test this by configuring a pool with an Allocator that always throws * on deallocate, in addition to counting deallocations. We also keep the * standard TTL configuration to prevent the objects from being immediately * deallocated when they are released, and we set the size to 2. Then we * claim two objects and then release them. This means that two objects are * now live in the pool. Then we shut the pool down. * The test passes if the shut down procedure completes without throwing * any exceptions, and we observe exactly 2 deallocations. */ @Test(timeout = TIMEOUT) public void mustSwallowExceptionsFromDeallocateThroughShutdown() throws Exception { CountingAllocator allocator = allocator( dealloc($throw(new RuntimeException("boo")))); config.setAllocator(allocator).setSize(2); createPool(); Poolable obj= pool.claim(longTimeout); pool.claim(longTimeout).release(); obj.release(); pool.shutdown().await(longTimeout); assertThat(allocator.countDeallocations(), is(2)); } /** * Calling await on a completion when your thread is interrupted, must * throw an InterruptedException. * In this particular case we make sure that the shut down procedure has * not yet completed, by claiming an object from the pool without releasing * it. */ @Test(timeout = TIMEOUT, expected = InterruptedException.class) public void awaitOnCompletionWhenInterruptedMustThrow() throws Exception { createPool(); GenericPoolable obj = pool.claim(longTimeout); assertNotNull("Did not deplete pool in time", obj); Completion completion = pool.shutdown(); Thread.currentThread().interrupt(); try { completion.await(longTimeout); } finally { obj.release(); } } /** * A thread that is awaiting the completion of a shut down procedure with * a timeout, must throw an InterruptedException if it is interrupted. * We test this the same way we test without the timeout. The only difference * is that our thread will enter the TIMED_WAITING state because of the * timeout. */ @Test(timeout = TIMEOUT, expected = InterruptedException.class) public void awaitWithTimeoutOnCompletionMustThrowUponInterruption() throws Exception { createPool(); GenericPoolable obj = pool.claim(longTimeout); assertNotNull("Did not deplete pool in time", obj); Completion completion = pool.shutdown(); forkFuture($interruptUponState( Thread.currentThread(), Thread.State.TIMED_WAITING)); try { completion.await(longTimeout); } finally { obj.release(); } } /** * As per the contract of throwing an InterruptedException, if the * await of an unfinished completion throws an InterruptedException, then * they must also clear the interrupted status. */ @Test(timeout = TIMEOUT) public void awaitOnCompletionWhenInterruptedMustClearInterruption() throws Exception { try { awaitOnCompletionWhenInterruptedMustThrow(); } catch (InterruptedException ignore) {} assertFalse(Thread.interrupted()); try { awaitWithTimeoutOnCompletionMustThrowUponInterruption(); } catch (InterruptedException ignore) {} assertFalse(Thread.interrupted()); } private Completion givenFinishedInterruptedCompletion() throws InterruptedException { createPool(); pool.shutdown().await(longTimeout); Thread.currentThread().interrupt(); return pool.shutdown(); } /** * Calling await with a timeout on a finished completion when your thread * is interrupted must, just as with calling await without a timeout, * throw an InterruptedException. */ @Test(timeout = TIMEOUT, expected = InterruptedException.class) public void awaitWithTimeoutOnFinishedCompletionWhenInterruptedMustThrow() throws InterruptedException { givenFinishedInterruptedCompletion().await(longTimeout); } /** * As per the contract of throwing an InterruptedException, the above must * also clear the threads interrupted status. */ @Test(timeout = TIMEOUT) public void awaitOnFinishedCompletionMustClearInterruption() { try { awaitWithTimeoutOnFinishedCompletionWhenInterruptedMustThrow(); } catch (InterruptedException ignore) {} assertFalse(Thread.interrupted()); } /** * Allocators must never return <code>null</code>, and if they do, then a * call to claim must throw a PoolException to indicate this fact. * We test this by configuring the pool with an Allocator that always * returns null from allocate, and then we try to claim from this pool. * This call to claim must then throw a PoolException. * @see Allocator#allocate(Slot) * @see PoolException */ @Test(timeout = TIMEOUT, expected = PoolException.class) public void claimMustThrowIfAllocationReturnsNull() throws Exception { Allocator<GenericPoolable> allocator = allocator(alloc($null)); Pool<GenericPoolable> pool = fixture.initPool(config.setAllocator(allocator)); pool.claim(longTimeout); } /** * Reallocators must never return <code>null</code> from * {@link Reallocator#reallocate(Slot, Poolable)}, and if they do, then a * call to claim must throw a PoolException to indicate this fact. * We test this by configuring a Reallocator that always return null on * reallocation, and an Expiration that will mark the first Poolable we try * to claim, as expired. Then we call claim, and object will be allocated, * it will be considered expired and sent back, then it gets reallocated * and the reallocate method returns null. Now the slot is poisoned, gets * back to the still on-going call to claim, that now throws a * PoolException. * @see Allocator#allocate(Slot) * @see Reallocator#reallocate(Slot, Poolable) * @see PoolException */ @Test(timeout = TIMEOUT, expected = PoolException.class) public void claimMustThrowIfReallocationReturnsNull() throws Exception { allocator = reallocator(realloc($null)); AtomicBoolean hasExpired = new AtomicBoolean(); config.setAllocator(allocator); config.setExpiration(expire($expiredIf(hasExpired))); config.setSize(2); createPool(); // Make sure the pool is fully allocated hasExpired.set(false); GenericPoolable obj1 = pool.claim(longTimeout); GenericPoolable obj2 = pool.claim(longTimeout); obj1.release(); obj2.release(); // Now consider it expired. This will send it back for reallocation, // the reallocation turns it into null, which gets thrown as poison. // The pool will race to reallocate the poisoned objects, but it won't // win the race, because there are two slots circulating in the pool, // so it should be highly probable that we are able to grab one of them // while the other one is being reallocated. hasExpired.set(true); pool.claim(longTimeout); } /** * Threads that are already interrupted upon entry to the claim method, must * promptly be met with an InterruptedException. This behaviour matches that * of other interruptible methods in java.util.concurrent. * @see Pool */ @Test(timeout = TIMEOUT, expected = InterruptedException.class) public void claimWhenInterruptedMustThrow() throws Exception { createPool(); Thread.currentThread().interrupt(); pool.claim(longTimeout); } /** * The claim methods checks whether the current thread is interrupted upon * entry, but perhaps what is more important is the interruption of a claim * call that is already waiting when the thread is interrupted. * We test for this by setting a thread to interrupt us, when our thread * enters the WAITING or TIMED_WAITING states. Then we make a call to the * appropriate claim method. If it throws an InterruptException, then the * test passes. */ @Test(timeout = TIMEOUT, expected = InterruptedException.class) public void blockedClaimWithTimeoutMustThrowUponInterruption() throws Exception { createPool(); GenericPoolable a = pool.claim(longTimeout); assertNotNull("Did not deplete pool in time", a); forkFuture($interruptUponState( Thread.currentThread(), Thread.State.TIMED_WAITING)); try { pool.claim(longTimeout); } finally { a.release(); } } /** * As per the general contract of interruptible methods, throwing an * InterruptedException will clear the interrupted flag on the thread. * This must also hold for the claim methods. */ @Test(timeout = TIMEOUT) public void throwingInterruptedExceptionFromClaimMustClearInterruptedFlag() throws Exception { try { blockedClaimWithTimeoutMustThrowUponInterruption(); fail("expected InterruptedException from claim"); } catch (InterruptedException ignore) {} assertFalse(Thread.interrupted()); } /** * A call to claim with time-out must complete within the time-out period * even if the Allocator never returns. * We test for this by configuring an Allocator that will never return from * any calls to allocate, and then calling claim with a time-out on the pool. * This claim-call must then complete before the time-out on the test case * itself elapses. * @see Pool */ @Test(timeout = TIMEOUT) public void claimMustStayWithinDeadlineEvenIfAllocatorBlocks() throws Exception { Semaphore semaphore = new Semaphore(0); allocator = allocator(alloc($acquire(semaphore, $new))); config.setAllocator(allocator); createPool(); try { pool.claim(shortTimeout); } finally { semaphore.release(10); } } /** * Claim with timeout must adhere to its timeout value. Some pool * implementations do the waiting in a loop, and if they don't do it right, * they might end up resetting the timeout every time they loop. This test * tries to ensure that that no such resetting can happen because an object * is released back into the pool. This may not cover all cases that are * possible with the different pool implementations, but it is at least a * start. And one that can be generally tested for across pool * implementations. Chances are, that if a pool handles this specific case, * then it handles all cases that are relevant to its implementation. * * We test for this by depleting a big pool with a very short TTL. After the * pool has been depleted, the allocator will no longer create any more * objects, so no expired objects will be renewed. Then we try to claim one * more object, and that will block. Meanwhile, another thread will perform * a trickle of releases. No useful objects will come back to the pool, but * there will still be some amount of activity. While all this goes on, the * blocked claim call must adhere to its specified timeout. * try to claim one more object */ @Test(timeout = TIMEOUT) public void claimMustStayWithinTimeoutEvenIfExpiredObjectIsReleased() throws Exception { // NOTE: This test is a little slow and may hit the 42424 ms timeout even // if it was actually supposed to pass. Try running it again if there are // any problems. I may have to revisit this one in the future. final Poolable[] objs = new Poolable[50]; final Lock lock = new ReentrantLock(); allocator = allocator(alloc($sync(lock, $new))); config.setAllocator(allocator); config.setExpiration(fiveMsTTL); config.setSize(objs.length); createPool(); for (int i = 0; i < objs.length; i++) { objs[i] = pool.claim(longTimeout); assertNotNull("Did not claim an object in time", objs[i]); } lock.lock(); // prevent new allocations Thread thread = fork($delayedReleases(objs, 10, TimeUnit.MILLISECONDS)); try { // must return before test times out: GenericPoolable obj = pool.claim(new Timeout(50, TimeUnit.MILLISECONDS)); if (obj != null) { obj.release(); } } finally { thread.interrupt(); //noinspection ThrowFromFinallyBlock thread.join(); lock.unlock(); } } /** * When claim is called with a timeout less than one, then it means that * no (observable amount of) waiting should take place. * <p> * We test for this by going through the numbers 0 to 99, both inclusive, * and call claim with those numbers as timeout values. The test is * considered to have passed, if this process completes within the 42424 * millisecond timeout on the test case. * @see Pool#claim(Timeout) */ @Test(timeout = TIMEOUT) public void claimWithTimeoutValueLessThanOneMustReturnImmediately() throws Exception { createPool(); GenericPoolable obj = pool.claim(longTimeout); assertNotNull("Did not deplete pool in time", obj); try { pool.claim(zeroTimeout); } finally { obj.release(); } } /** * When await is called on a shutdown Completion with a timeout value less * than one, then no amount of waiting must take place. * <p> * We test for this by depleting a pool and initiating the shut-down * procedure. The fact that the pool is depleted, means that the shutdown * procedure will not be able to finish. In other words, we know that it is * in progress for our subsequent await calls. * Then we call await on the shut-down Completion object, giving timeout * values ranging from zero to -99. These awaits must all complete before the * test itself times out. * @see Completion */ @Test(timeout = TIMEOUT) public void awaitCompletionWithTimeoutLessThanOneMustReturnImmediately() throws Exception { createPool(); GenericPoolable obj = pool.claim(longTimeout); try { assertNotNull("Did not deplete pool in time", obj); pool.shutdown().await(zeroTimeout); } finally { obj.release(); } } /** * One must provide a Timeout argument when awaiting the completion of a * shut-down procedure. Passing null is thus an illegal argument. * @see Completion */ @Test(timeout = TIMEOUT, expected = IllegalArgumentException.class) public void awaitCompletionWithNullTimeUnitMustThrow() throws Exception { createPool(); pool.shutdown().await(null); } /** * Even if the Allocator has never made a successful allocation, the pool * must still be able to complete its shut-down procedure. * In this case we test with an allocator that always returns null. */ @Test(timeout = TIMEOUT) public void mustCompleteShutDownEvenIfAllSlotsHaveNullErrors() throws Exception { Allocator<GenericPoolable> allocator = allocator(alloc($null)); Pool<GenericPoolable> pool = givenPoolWithFailedAllocation(allocator); // the shut-down procedure must complete before the test times out. pool.shutdown().await(longTimeout); } private Pool<GenericPoolable> givenPoolWithFailedAllocation( Allocator<GenericPoolable> allocator) { config.setAllocator(allocator); createPool(); try { // ensure at least one allocation attempt has taken place pool.claim(longTimeout); fail("allocation attempt should have failed!"); } catch (Exception ignore) { // we don't care about this one } return pool; } /** * As with * {@link #mustCompleteShutDownEvenIfAllSlotsHaveNullErrors()}, * the pool must be able to shut down if it has never been able to allocate * anything. * In this case we test with an allocator that always throws an exception. */ @Test(timeout = TIMEOUT) public void mustCompleteShutDownEvenIfAllSlotsHaveAllocationErrors() throws Exception { Allocator<GenericPoolable> allocator = allocator(alloc($throw(new Exception("it's terrible stuff!!!")))); Pool<GenericPoolable> pool = givenPoolWithFailedAllocation(allocator); // must complete before the test timeout: pool.shutdown().await(longTimeout); } @Test(timeout = TIMEOUT) public void mustBeAbleToShutDownWhenAllocateAlwaysThrows() throws Exception { AtomicLong counter = new AtomicLong(); allocator = allocator(alloc( $incrementAnd(counter, $throw(new RuntimeException("boo"))))); config.setAllocator(allocator); config.setSize(3); createPool(); //noinspection StatementWithEmptyBody while (counter.get() < 2) { // do nothing } pool.shutdown().await(longTimeout); } /** * Calling shutdown on a pool while being interrupted must still start the * shut-down procedure. * We test for this by initiating the shut-down procedure on a pool while * being interrupted. Then we clear our interrupted flag and await the * completion of the shut-down procedure. The procedure must complete within * the test timeout. If it does not, then that is taken as evidence that * the procedure did NOT start, and so the test fails. * @see Pool */ @Test(timeout = TIMEOUT) public void mustBeAbleToShutDownEvenIfInterrupted() throws Exception { createPool(); Thread.currentThread().interrupt(); Completion completion = pool.shutdown(); Thread.interrupted(); // clear interrupted flag completion.await(longTimeout); // must complete before test timeout } /** * Initiating the shut-down procedure must not influence the threads * interruption status. * We test for this by calling shutdown on the pool while being interrupted. * Then we check that we are still interrupted. * @see Pool */ @Test(timeout = TIMEOUT) public void callingShutdownMustNotAffectInterruptionStatus() throws Exception { createPool(); Thread.currentThread().interrupt(); pool.shutdown(); assertTrue(Thread.interrupted()); } /** * Pool implementations might do some kind of biasing to reduce contention. * We need to make sure that if there are not enough objects for all the * threads, then even biased objects must participate in the circulation. * If they don't, then some threads might starve. */ @Test(timeout = TIMEOUT) public void mustUnbiasObjectsNoLongerClaimed() throws Exception { createPool(); Poolable obj = pool.claim(longTimeout); obj.release(); // item now biased to our thread // claiming in a different thread should give us the same object. AtomicReference<GenericPoolable> ref = new AtomicReference<>(); join(forkFuture(capture($claim(pool, longTimeout), ref))); try { assertThat(ref.get(), is(obj)); } finally { ref.get().release(); } } /** * Pool implementations that do biasing need to ensure, that claimed objects * are not available for other threads, even if the object is claimed with * a biased kind of claim. * In other words, if we claim an object, it might become biased to us. If we * then release it and then call claim again, we might go through a different * path through the code. And this new path needs to ensure that the object * is properly claimed, such that no other threads can claim it as well. */ @Test(timeout = TIMEOUT) public void biasedClaimMustUpgradeToOrdinaryClaimIfTheObjectIsPulledFromTheQueue() throws Exception { createPool(); pool.claim(longTimeout).release(); // bias the object to our thread GenericPoolable obj = pool.claim(longTimeout); // this is now our biased claim AtomicReference<GenericPoolable> ref = new AtomicReference<>(); // the biased claim will be upgraded to an ordinary claim: join(forkFuture(capture($claim(pool, zeroTimeout), ref))); try { assertThat(ref.get(), nullValue()); } finally { obj.release(); } } /** * If a pool has been depleted, and then shut down, and another call to claim * comes in, then it must immediately throw an IllegalStateException. * Importantly, it must not block the thread to wait for any objects to be * released. */ @Test(timeout = TIMEOUT, expected = IllegalStateException.class) public void depletedPoolThatHasBeenShutDownMustThrowUponClaim() throws Exception { createPool(); GenericPoolable obj = pool.claim(longTimeout); // depleted pool.shutdown(); try { pool.claim(longTimeout); } finally { obj.release(); } } /** * Basically the same test as above, except now we wait for the shutdown * process to make a bit of progress. This might expose different race bugs. */ @Test(timeout = TIMEOUT, expected = IllegalStateException.class) public void depletedPoolThatHasBeenShutDownMustThrowUponClaimEvenAfterSomeTime() throws Exception { createPool(); GenericPoolable obj = pool.claim(longTimeout); // depleted pool.shutdown(); spinwait(10); try { pool.claim(longTimeout); } finally { obj.release(); } } /** * We must ensure that, for pool implementation that do biasing, the checking * of whether the pool has been shut down must come before even a biased * claim. Even though a biased claim might not do any waiting that a normal * claim might do, it is still important that the shutdown notification takes * precedence, because we don't know for how long the claimed object will be * held, or if there will be other biased claims in the future. */ @Test(timeout = TIMEOUT, expected = IllegalStateException.class) public void poolThatHasBeenShutDownMustThrowUponClaimEvenIfItHasAvailableUnbiasedObjects() throws Exception { config.setSize(4); createPool(); GenericPoolable a = pool.claim(longTimeout); GenericPoolable b = pool.claim(longTimeout); GenericPoolable c = pool.claim(longTimeout); GenericPoolable d = pool.claim(longTimeout); a.release(); // placed ahead of any poison pills pool.shutdown(); b.release(); c.release(); d.release(); pool.claim(longTimeout); } /** * It is explicitly permitted that the thread that releases an object back * into the pool, can be a different thread than the one that claimed the * particular object. */ @Test(timeout = TIMEOUT) public void mustNotThrowWhenReleasingObjectClaimedByAnotherThread() throws Exception { createPool(); GenericPoolable obj = forkFuture($claim(pool, longTimeout)).get(); obj.release(); } /** * Here's the scenario we're trying to target: * * - You (or your thread) do a successful claim, and triumphantly stores it * in the ThreadLocal cache. * - You then return the object after use, so now it's back in the * live-queue for others to grab. * - Someone else tries to claim the object, but decides that it has expired, * and sends it off through the dead-queue to be reallocated. * - The reallocation fails for some reason, and the slot is now poisoned. * - You want to claim an object again, and start by looking in the * ThreadLocal cache. * - You find the slot for the object you had last, but the slot is poisoned. * - Now, because you found it in the ThreadLocal cache – and notably did * *not* pull it off of the live-queue – you cannot just put it on the * dead-queue, because that could lead to unbounded memory use. * - Instead, it has to be marked as live, and we instead have to wait for * someone to pull it off of the live-queue, check the poison again, and * *then* put it on the dead-queue. * - Your ThreadLocal reclaim attempt then end in throwing the poison, * wrapped in a PoolException. * - Sadly, this process does not involve clearing out the ThreadLocal cache, * so if you quickly catch the exception and try to claim again, you will * find the same exact poisoned slot and go through the same routine, that * ends in a thrown exception and a poisoned slot still left in the * ThreadLocal cache. */ @Test(timeout = TIMEOUT) public void mustNotCachePoisonedSlots() throws Exception { // First we prime the possible thread-local cache to a particular object. // Then we instruct the allocator to always throw an exception when it is // told to allocate on that particular slot. // Then, in another thread, we mark all objects in the pool as expired. // Once we have observed a reallocation attempt at our primed slot, we // try to reclaim it. The reclaim must not throw an exception because of // the cached poisoned slot. config.setSize(3); // Enough permits for each initial allocation: final Semaphore semaphore = new Semaphore(3); final AtomicBoolean hasExpired = new AtomicBoolean(false); config.setExpiration(expire($expiredIf(hasExpired))); final String allocationCause = "allocation blew up!"; final AtomicReference<Slot> failOnAllocatingSlot = new AtomicReference<>(); final AtomicInteger observedFailedAllocation = new AtomicInteger(); Action observeFailure = (slot, obj) -> { if (slot == failOnAllocatingSlot.get()) { observedFailedAllocation.incrementAndGet(); throw new RuntimeException(allocationCause); } return new GenericPoolable(slot); }; allocator = allocator(alloc($acquire(semaphore, observeFailure))); config.setAllocator(allocator); createPool(); // Wait for the pool to fill while (allocator.countAllocations() < 3) { Thread.yield(); } // Prime any thread-local cache GenericPoolable obj = pool.claim(longTimeout); failOnAllocatingSlot.set(obj.slot); obj.release(); // Places slot at end of queue // Expire all poolables hasExpired.set(true); AtomicReference<GenericPoolable> ref = new AtomicReference<>(); try { forkFuture(capture($claim(pool, shortTimeout), ref)).get(); } catch (ExecutionException ignore) { // This is okay. We just got a failed reallocation } assertNull(ref.get()); // Give the allocator enough permits to reallocate the whole pool, again semaphore.release(3); // Wait for our primed slot to get reallocated while(observedFailedAllocation.get() < 1) { Thread.yield(); } spinwait(15); // Heuristically wait for the slot to become live // Things no longer expire... hasExpired.set(false); // ... so we should be able to claim without trouble pool.claim(longTimeout).release(); } @Test(expected = IllegalArgumentException.class) public void targetSizeMustBeGreaterThanZero() { createPool(); pool.setTargetSize(0); } @Test public void targetSizeMustBeConfiguredSizeByDefault() { config.setSize(23); createPool(); assertThat(pool.getTargetSize(), is(23)); } @Test public void getTargetSizeMustReturnLastSetTargetSize() { createPool(); pool.setTargetSize(3); assertThat(pool.getTargetSize(), is(3)); } /** * When we increase the size of a depleted pool, it should be possible to * make claim again and get out newly allocated objects. * * We test for this by depleting a pool, upping the size and then claiming * again with a timeout that is longer than the timeout of the test. The test * pass if it does not timeout. */ @Test(timeout = TIMEOUT) public void increasingSizeMustAllowMoreAllocations() throws Exception { createPool(); GenericPoolable a = pool.claim(longTimeout); // depleted pool.setTargetSize(2); // now this mustn't block: GenericPoolable b = pool.claim(longTimeout); a.release(); b.release(); } /** * We must somehow ensure that the pool starts deallocating more than it * allocates, when the pool is shrunk. This is difficult because the pool * cannot tell us when it reaches the target size, so we have to figure this * out by using a special allocator. * * We test for this by configuring a CountingAllocator that also unparks a * thread (namely ours, the main thread for the test) at every allocation * and deallocation. We also configure the pool to have a somewhat large * initial size, so we can shrink it later. Then we deplete the pool, and * set a smaller target size. After setting the new target size, we release * just enough objects for the pool to reach it, and then we wait for the * allocator to register that same number of deallocations. This has to * happen before the test times out. After that, we check that the difference * between the allocations and the deallocations matches the new target size. */ @Test(timeout = TIMEOUT) public void decreasingSizeMustEventuallyDeallocateSurplusObjects() throws Exception { int startingSize = 5; int newSize = 1; allocator = allocator(); config.setSize(startingSize); config.setAllocator(allocator); createPool(); List<GenericPoolable> objs = new ArrayList<>(); while (allocator.countAllocations() != startingSize) { objs.add(pool.claim(longTimeout)); // force the pool to do work } pool.setTargetSize(newSize); while (allocator.countDeallocations() != startingSize - newSize) { if (objs.size() > 0) { objs.remove(0).release(); // give the pool objects to deallocate } else { pool.claim(longTimeout).release(); // prod it & poke it } LockSupport.parkNanos(10000000); // 10 millis } int actualSize = allocator.countAllocations() - allocator.countDeallocations(); try { assertThat(actualSize, is(newSize)); } finally { for (GenericPoolable obj : objs) { obj.release(); } } } /** * Similar to the decreasingSizeMustEventuallyDeallocateSurplusObjects test * above, but this time the objects are all expired after the pool has been * shrunk. * * Again, we deplete the pool. Once depleted, our expiration has been * configured such, that all subsequent items one tries to claim, will be * expired. * * Then we set the new lower target size, and release just enough for the * pool to reach the new target. * * Then we try to claim an object from the pool with a very short timeout. * This will return null because the pool is still depleted. We also check * that the pool has not made any new allocations, even though we have been * releasing objects. We don't check the deallocations because it's * complicated and we did it in the * decreasingSizeMustEventuallyDeallocateSurplusObjects test above. */ @Test(timeout = TIMEOUT) public void mustNotReallocateWhenReleasingExpiredObjectsIntoShrunkPool() throws Exception { int startingSize = 5; int newSize = 1; Expiration<GenericPoolable> expiration = expire( // our 5 items are not expired when we deplete the pool $fresh, $fresh, $fresh, $fresh, $fresh, // but all items we try to claim after that *are* expired. $expired ); config.setExpiration(expiration).setAllocator(allocator); config.setSize(startingSize); createPool(); List<GenericPoolable> objs = new ArrayList<>(); for (int i = 0; i < startingSize; i++) { objs.add(pool.claim(longTimeout)); } assertThat(objs.size(), is(startingSize)); pool.setTargetSize(newSize); for (int i = 0; i < startingSize - newSize; i++) { // release the surplus expired objects back into the pool objs.remove(0).release(); } // now the released objects should not cause reallocations, so claim // returns null (it's still depleted) and allocation count stays put try { assertThat(pool.claim(shortTimeout), nullValue()); assertThat(allocator.countAllocations(), is(startingSize)); } finally { objs.remove(0).release(); } } @Test public void settingTargetSizeOnPoolThatHasBeenShutDownDoesNothing() { config.setSize(3); createPool(); pool.shutdown(); pool.setTargetSize(10); // this should do nothing, because it's shut down assertThat(pool.getTargetSize(), is(3)); } /** * Make sure that the pool does not get into a bad state, caused by concurrent * background resizing jobs interfering with each other. * * We test this by creating a small pool, then resizing it larger (so much so that * any resizing job is unlikely to finish before we can make our next move) and then * immediately resizing it smaller again. This should put multiple resizing jobs in * flight. When all the background jobs complete, we should observe that the pool * ended up with exactly the target size number of items in it. */ @Test(timeout = TIMEOUT) public void increasingAndDecreasingSizeInQuickSuccessionMustEventuallyReachTargetSize() throws Exception { createPool(); // Fiddle with the target size. pool.setTargetSize(20); pool.setTargetSize(1); // Then wait for the size of the pool to settle int deallocations; int allocations; do { Thread.sleep(1); deallocations = allocator.countDeallocations(); allocations = allocator.countAllocations(); } while (allocations - deallocations > 1); // Now we should be left with exactly one object that we can claim: GenericPoolable obj = pool.claim(longTimeout); try { assertThat(pool.claim(shortTimeout), nullValue()); } finally { obj.release(); } } /** * The specification only promises to correctly handle Expirations that throw * Exceptions, but we also test with Throwable, just in case we might be able * to recover from them as well. */ @Test(timeout = TIMEOUT) public void mustNotLeakSlotsIfExpirationThrowsThrowableInsteadOfException() throws InterruptedException { final AtomicBoolean shouldThrow = new AtomicBoolean(true); config.setExpiration(expire( $if(shouldThrow, $throwExpire(new SomeRandomThrowable("Boom!")), $fresh))); createPool(); try { pool.claim(longTimeout); fail("Expected claim to throw"); } catch (PoolException pe) { assertThat(pe.getCause(), instanceOf(SomeRandomThrowable.class)); } // Now, the slot should not have leaked, so the next claim should succeed: shouldThrow.set(false); pool.claim(longTimeout).release(); pool.shutdown(); } @Test(timeout = TIMEOUT) public void mustProactivelyReallocatePoisonedSlotsWhenAllocatorStopsThrowingExceptions() throws Exception { final CountDownLatch allocationLatch = new CountDownLatch(1); allocator = allocator(alloc( $throw(new Exception("boom")), $countDown(allocationLatch, $new))); config.setAllocator(allocator); createPool(); allocationLatch.await(); GenericPoolable obj = pool.claim(longTimeout); try { assertThat(obj, is(notNullValue())); } finally { obj.release(); } } @Test(timeout = TIMEOUT) public void mustProactivelyReallocatePoisonedSlotsWhenReallocatorStopsThrowingExceptions() throws Exception { final AtomicBoolean expired = new AtomicBoolean(); final CountDownLatch allocationLatch = new CountDownLatch(2); allocator = reallocator( alloc($countDown(allocationLatch, $new)), realloc($throw(new Exception("boom")), $new)); config.setAllocator(allocator); config.setExpiration(expire($expiredIf(expired))); createPool(); expired.set(false); // first claimed object is not expired pool.claim(longTimeout).release(); // first object is fully allocated expired.set(true); // the next object we claim is expired GenericPoolable obj = pool.claim(zeroTimeout); // send back to reallocation assertThat(obj, is(nullValue())); allocationLatch.await(); expired.set(false); obj = pool.claim(longTimeout); try { assertThat(obj, is(notNullValue())); } finally { obj.release(); } } @Test(timeout = TIMEOUT) public void mustProactivelyReallocatePoisonedSlotsWhenAllocatorStopsReturningNull() throws Exception { final CountDownLatch allocationLatch = new CountDownLatch(1); allocator = allocator( alloc($null, $countDown(allocationLatch, $new))); config.setAllocator(allocator); createPool(); allocationLatch.await(); GenericPoolable obj = pool.claim(longTimeout); try { assertThat(obj, is(notNullValue())); } finally { obj.release(); } } @Test(timeout = TIMEOUT) public void mustProactivelyReallocatePoisonedSlotsWhenReallocatorStopsReturningNull() throws Exception { AtomicBoolean expired = new AtomicBoolean(); CountDownLatch allocationLatch = new CountDownLatch(1); Semaphore fixReallocLatch = new Semaphore(0); allocator = reallocator( alloc($new, $countDown(allocationLatch, $acquire(fixReallocLatch, $new))), realloc($null, $new)); config.setAllocator(allocator); config.setExpiration(expire($expiredIf(expired))); createPool(); expired.set(false); // first claimed object is not expired pool.claim(longTimeout).release(); // first object is fully allocated expired.set(true); // the next object we claim is expired GenericPoolable obj = pool.claim(zeroTimeout); // send back to reallocation assertThat(obj, is(nullValue())); fixReallocLatch.release(); allocationLatch.await(); expired.set(false); obj = pool.claim(longTimeout); try { assertThat(obj, is(notNullValue())); } finally { obj.release(); } } @Test(timeout = TIMEOUT) public void mustNotFrivolouslyReallocateNonPoisonedSlotsDuringEagerRecovery() throws Exception { final CountDownLatch allocationLatch = new CountDownLatch(3); allocator = allocator(alloc( $countDown(allocationLatch, $null), $countDown(allocationLatch, $new))); config.setAllocator(allocator).setSize(2); createPool(); allocationLatch.await(); // The pool should now be fully allocated and healed GenericPoolable a = pool.claim(longTimeout); GenericPoolable b = pool.claim(longTimeout); try { assertThat(allocator.countAllocations(), is(3)); assertThat(allocator.countDeallocations(), is(0)); // allocation failed assertThat(allocator.getDeallocations(), not(contains(a, b))); } finally { a.release(); b.release(); } } /** * If the interrupt happen inside the Allocator.allocate(Slot) method, and * then the allocator either clears the interruption status, or throws an * InterruptedException which in turn will be understood as poison, then the * allocation thread can miss the shutdown signal, and never begin the * shutdown sequence. */ @Test(timeout = TIMEOUT) public void mustCompleteShutdownEvenIfAllocatorEatsTheInterruptSignal() throws Exception { config.setAllocator(reallocator( alloc($sleep(1000, $new)), realloc($sleep(1000, $new)))); // Give the allocation thread a head-start to get stuck sleeping in the // Allocator.allocate method: createPool(); GenericPoolable obj = pool.claim(mediumTimeout); if (obj != null) { obj.release(); } // The interrupt signal from shutdown will get caught by the Allocator: pool.shutdown().await(longTimeout); } @Test(timeout = TIMEOUT) public void poolMustTolerateInterruptedExceptionFromAllocatorWhenNotShutDown() throws InterruptedException { config.setAllocator( allocator(alloc($throw(new InterruptedException("boom")), $new))); AtomicBoolean hasExpired = new AtomicBoolean(); config.setExpiration(expire($expiredIf(hasExpired))); createPool(); // This will capture the failed allocation: try { pool.claim(longTimeout).release(); } catch (PoolException e) { assertThat(e.getCause(), instanceOf(InterruptedException.class)); } // This should succeed like nothing happened: pool.claim(longTimeout).release(); // Cause an extra reallocation to make sure that works: hasExpired.set(true); assertNull(pool.claim(shortTimeout)); hasExpired.set(false); // These should again succeed like nothing happened: pool.claim(longTimeout).release(); pool.claim(longTimeout).release(); shutPoolDown(); } @Test(timeout = TIMEOUT) public void poolMustUseConfiguredThreadFactoryWhenCreatingBackgroundThreads() throws InterruptedException { final ThreadFactory delegateThreadFactory = config.getThreadFactory(); final List<Thread> createdThreads = new ArrayList<>(); ThreadFactory factory = r -> { Thread thread = delegateThreadFactory.newThread(r); createdThreads.add(thread); return thread; }; config.setThreadFactory(factory); createPool(); pool.claim(longTimeout).release(); assertThat(createdThreads.size(), is(1)); assertTrue(createdThreads.get(0).isAlive()); pool.shutdown().await(longTimeout); assertThat(createdThreads.size(), is(1)); Thread thread = createdThreads.get(0); thread.join(); assertFalse(thread.isAlive()); } @Test public void managedPoolInterfaceMustBeMXBeanConformant() { assertTrue(JMX.isMXBeanInterface(ManagedPool.class)); } @Test(timeout = TIMEOUT) public void managedPoolMustBeExposableThroughAnMBeanServerAsAnMXBean() throws Exception { config.setSize(3); ManagedPool managedPool = assumeManagedPool(); GenericPoolable a = pool.claim(longTimeout); GenericPoolable b = pool.claim(longTimeout); GenericPoolable c = pool.claim(longTimeout); try { MBeanServer server = MBeanServerFactory.newMBeanServer("domain"); ObjectName name = new ObjectName("domain:pool=stormpot"); server.registerMBean(managedPool, name); ManagedPool proxy = JMX.newMBeanProxy(server, name, ManagedPool.class); assertThat(proxy.getAllocationCount(), is(3L)); } finally { a.release(); b.release(); c.release(); } } @Test(timeout = TIMEOUT) public void managedPoolMustCountAllocations() throws InterruptedException { AtomicBoolean hasExpired = new AtomicBoolean(); config.setExpiration(expire($expiredIf(hasExpired))); CountingReallocator reallocator = reallocator(); config.setAllocator(reallocator); ManagedPool managedPool = assumeManagedPool(); pool.claim(longTimeout).release(); // expiration = false ; allocations = 1 pool.claim(longTimeout).release(); // expiration = false ; allocations = 1 hasExpired.set(true); assertNull(pool.claim(zeroTimeout)); // expiration = true, false ; allocations = 2+ // we're racing with how quickly the allocation thread can replenish the // pool, so we might get more than one allocation out of this hasExpired.set(false); pool.claim(longTimeout).release(); // expiration = false ; allocations = 2+ long allocationCount = managedPool.getAllocationCount(); long expectedCount = reallocator.countAllocations() + reallocator.countReallocations(); assertThat(allocationCount, allOf( greaterThanOrEqualTo(2L), equalTo(expectedCount))); } @Test(timeout = TIMEOUT) public void managedPoolMustCountAllocationsFailingWithExceptions() throws Exception { CountDownLatch latch = new CountDownLatch(1); Exception exception = new Exception("boo"); config.setSize(2).setAllocator(allocator(alloc( $new, $throw(exception), $throw(exception), $countDown(latch, $new)))); ManagedPool managedPool = assumeManagedPool(); // simply wait for the proactive healing to replace the failed allocations latch.await(); assertThat(managedPool.getFailedAllocationCount(), is(2L)); } @Test(timeout = TIMEOUT) public void managedPoolMustCountAllocationsFailingByReturningNull() throws Exception { config.setSize(2).setAllocator( allocator(alloc($new, $null, $null, $new))); ManagedPool managedPool = assumeManagedPool(); GenericPoolable a = null; GenericPoolable b = null; // we have to loop both claims because proactive reallocation can move the // poisoned slots around inside the pool. do { try { a = pool.claim(longTimeout); } catch (PoolException ignore) {} } while (a == null); do { try { b = pool.claim(longTimeout); } catch (PoolException ignore) {} } while (b == null); a.release(); b.release(); assertThat(managedPool.getFailedAllocationCount(), is(2L)); } @Test(timeout = TIMEOUT) public void managedPoolMustCountReallocationsFailingWithExceptions() throws Exception { config.setSize(1); Exception exception = new Exception("boo"); config.setAllocator(reallocator(realloc($throw(exception), $new))); config.setExpiration(expire($expired, $fresh)); ManagedPool managedPool = assumeManagedPool(); GenericPoolable obj = null; do { try { obj = pool.claim(longTimeout); } catch (PoolException ignore) {} } while (obj == null); obj.release(); assertThat(managedPool.getFailedAllocationCount(), is(1L)); } @Test(timeout = TIMEOUT) public void managedPoolMustCountReallocationsFailingByReturningNull() throws Exception { config.setSize(1); config.setAllocator(reallocator(realloc($null, $new))); config.setExpiration(expire($expired, $fresh)); ManagedPool managedPool = assumeManagedPool(); GenericPoolable obj = null; do { try { obj = pool.claim(longTimeout); } catch (PoolException ignore) {} } while (obj == null); obj.release(); assertThat(managedPool.getFailedAllocationCount(), is(1L)); } @Test(timeout = TIMEOUT) public void managedPoolMustAllowGettingAndSettingPoolTargetSize() { config.setSize(2); ManagedPool managedPool = assumeManagedPool(); assertThat(managedPool.getTargetSize(), is(2)); managedPool.setTargetSize(5); assertThat(managedPool.getTargetSize(), is(5)); } @Test(timeout = TIMEOUT) public void managedPoolMustGivePoolState() { ManagedPool managedPool = assumeManagedPool(); assertFalse(managedPool.isShutDown()); } @Test(timeout = TIMEOUT) public void managedPoolMustReturnNaNWhenNoMetricsRecorderHasBeenConfigured() { ManagedPool managedPool = assumeManagedPool(); assertThat(managedPool.getAllocationLatencyPercentile(0.5), is(Double.NaN)); assertThat(managedPool.getObjectLifetimePercentile(0.5), is(Double.NaN)); assertThat(managedPool.getAllocationFailureLatencyPercentile(0.5), is(Double.NaN)); assertThat(managedPool.getReallocationLatencyPercentile(0.5), is(Double.NaN)); assertThat(managedPool.getReallocationFailureLatencyPercentile(0.5), is(Double.NaN)); assertThat(managedPool.getDeallocationLatencyPercentile(0.5), is(Double.NaN)); } @Test(timeout = TIMEOUT) public void managedPoolMustGetLatencyPercentilesFromConfiguredMetricsRecorder() { config.setMetricsRecorder( new FixedMeanMetricsRecorder(1.37, 2.37, 3.37, 4.37, 5.37, 6.37)); ManagedPool managedPool = assumeManagedPool(); assertThat(managedPool.getObjectLifetimePercentile(0.5), is(1.37)); assertThat(managedPool.getAllocationLatencyPercentile(0.5), is(2.37)); assertThat(managedPool.getAllocationFailureLatencyPercentile(0.5), is(3.37)); assertThat(managedPool.getReallocationLatencyPercentile(0.5), is(4.37)); assertThat(managedPool.getReallocationFailureLatencyPercentile(0.5), is(5.37)); assertThat(managedPool.getDeallocationLatencyPercentile(0.5), is(6.37)); } @Test(timeout = TIMEOUT) public void managedPoolMustRecordObjectLifetimeOnDeallocateInConfiguredMetricsRecorder() throws InterruptedException { CountDownLatch deallocLatch = new CountDownLatch(1); config.setMetricsRecorder(new LastSampleMetricsRecorder()); config.setSize(2); config.setAllocator(reallocator(dealloc($countDown(deallocLatch, $null)))); ManagedPool managedPool = assumeManagedPool(); GenericPoolable a = pool.claim(longTimeout); GenericPoolable b = pool.claim(longTimeout); spinwait(5); a.release(); b.release(); pool.setTargetSize(1); deallocLatch.await(); assertThat(managedPool.getObjectLifetimePercentile(0.0), allOf( greaterThanOrEqualTo(5.0), not(Double.NaN), lessThan(50000.0))); } @Test(timeout = TIMEOUT) public void managedPoolMustNotRecordObjectLifetimeLatencyBeforeFirstDeallocation() throws InterruptedException { config.setMetricsRecorder(new LastSampleMetricsRecorder()); ManagedPool managedPool = assumeManagedPool(); GenericPoolable obj = pool.claim(longTimeout); try { assertThat(managedPool.getObjectLifetimePercentile(0.0), is(Double.NaN)); } finally { obj.release(); } } @Test(timeout = TIMEOUT) public void managedPoolMustRecordObjectLifetimeOnReallocateInConfiguredMetricsRecorder() throws InterruptedException { config.setMetricsRecorder(new LastSampleMetricsRecorder()); Semaphore semaphore = new Semaphore(0); config.setAllocator(reallocator(realloc($acquire(semaphore, $new), $new))); AtomicBoolean hasExpired = new AtomicBoolean(); config.setExpiration(expire($expiredIf(hasExpired))); ManagedPool managedPool = assumeManagedPool(); GenericPoolable obj = pool.claim(longTimeout); spinwait(5); obj.release(); hasExpired.set(true); assertNull(pool.claim(zeroTimeout)); hasExpired.set(false); semaphore.release(1); obj = pool.claim(longTimeout); // wait for reallocation try { assertThat(managedPool.getObjectLifetimePercentile(0.0), allOf( greaterThanOrEqualTo(5.0), not(Double.NaN), lessThan(50000.0))); } finally { obj.release(); } } @Test(timeout = TIMEOUT)public void managedPoolLeakedObjectCountMustStartAtZero() { ManagedPool managedPool = assumeManagedPool(); // Pools that don't support this feature return -1L. assertThat(managedPool.getLeakedObjectsCount(), is(0L)); } @Test(timeout = TIMEOUT) public void managedPoolMustCountLeakedObjects() throws Exception { config.setSize(2); ManagedPool managedPool = assumeManagedPool(); pool.claim(longTimeout); // leak! // Clear any thread-local reference to the leaked object: pool.claim(longTimeout).release(); // Clear any references held by our particular allocator: allocator.clearLists(); // A run of the GC will null out the weak-refs: System.gc(); System.gc(); System.gc(); pool = null; // null out the pool because we can no longer shut it down. assertThat(managedPool.getLeakedObjectsCount(), is(1L)); } @SuppressWarnings("UnusedAssignment") @Test(timeout = TIMEOUT)public void mustNotHoldOnToDeallocatedObjectsWhenLeakDetectionIsEnabled() throws Exception { // It's enabled by default AtomicBoolean hasExpired = new AtomicBoolean(); config.setExpiration(expire($expiredIf(hasExpired))); createPool(); // Allocate an object GenericPoolable obj = pool.claim(longTimeout); WeakReference<GenericPoolable> weakReference = new WeakReference<>(obj); obj.release(); obj = null; // Send it back for reallocation hasExpired.set(true); obj = pool.claim(zeroTimeout); if (obj != null) { obj.release(); } // Wait for the reallocation to complete hasExpired.set(false); pool.claim(longTimeout).release(); // Clear the allocator lists to remove the last references allocator.clearLists(); // GC to force the object through finalization life cycle System.gc(); System.gc(); System.gc(); // Now our weakReference must have been cleared assertNull(weakReference.get()); } @Test(timeout = TIMEOUT) public void mustNotHoldOnToDeallocatedObjectsWhenLeakDetectionIsDisabled() throws Exception { // It's enabled by default, so just change that setting and run the same // test as above config.setPreciseLeakDetectionEnabled(false); mustNotHoldOnToDeallocatedObjectsWhenLeakDetectionIsEnabled(); } @Test(timeout = TIMEOUT) public void managedPoolMustNotCountShutDownAsLeak() throws Exception { config.setSize(2); ManagedPool managedPool = assumeManagedPool(); claimRelease(2, pool, longTimeout); pool.shutdown().await(longTimeout); allocator.clearLists(); System.gc(); System.gc(); System.gc(); assertThat(managedPool.getLeakedObjectsCount(), is(0L)); } @Test(timeout = TIMEOUT) public void managedPoolMustNotCountResizeAsLeak() throws Exception { config.setSize(2); ManagedPool managedPool = assumeManagedPool(); claimRelease(2, pool, longTimeout); managedPool.setTargetSize(4); claimRelease(4, pool, longTimeout); managedPool.setTargetSize(1); while (allocator.countDeallocations() < 3) { spinwait(1); } allocator.clearLists(); System.gc(); System.gc(); System.gc(); assertThat(managedPool.getLeakedObjectsCount(), is(0L)); } @Test(timeout = TIMEOUT) public void managedPoolMustReturnMinusOneForLeakedObjectCountWhenDetectionIsDisabled() { config.setPreciseLeakDetectionEnabled(false); ManagedPool managedPool = assumeManagedPool(); assertThat(managedPool.getLeakedObjectsCount(), is(-1L)); } @Test(timeout = TIMEOUT) public void disabledLeakDetectionMustNotBreakResize() throws Exception { config.setPreciseLeakDetectionEnabled(false); config.setSize(2); ManagedPool managedPool = assumeManagedPool(); claimRelease(2, pool, longTimeout); pool.setTargetSize(6); claimRelease(6, pool, longTimeout); pool.setTargetSize(2); while (allocator.countDeallocations() < 4) { spinwait(1); } assertThat(managedPool.getLeakedObjectsCount(), is(-1L)); } @Test(timeout = TIMEOUT) public void mustCheckObjectExpirationInBackgroundWhenEnabled() throws Exception { CountDownLatch latch = new CountDownLatch(1); CountingReallocator reallocator = reallocator(); CountingExpiration expiration = expire($expired, $countDown(latch, $fresh)); config.setExpiration(expiration); config.setAllocator(reallocator); config.setBackgroundExpirationEnabled(true); createPool(); latch.await(); assertThat(reallocator.countAllocations(), is(1)); assertThat(reallocator.countDeallocations(), is(0)); assertThat(reallocator.countReallocations(), is(1)); } @Test(timeout = TIMEOUT) public void objectMustBeClaimableAfterBackgroundReallocation() throws Exception { CountDownLatch latch = new CountDownLatch(1); CountingExpiration expiration = expire($countDown(latch, $expired), $fresh); config.setExpiration(expiration); config.setBackgroundExpirationEnabled(true); createPool(); latch.await(); pool.claim(longTimeout).release(); } @Test(timeout = TIMEOUT) public void mustNotReallocateObjectsThatAreNotExpiredByTheBackgroundCheck() throws Exception { CountDownLatch latch = new CountDownLatch(2); CountingExpiration expiration = expire($countDown(latch, $fresh)); CountingReallocator reallocator = reallocator(); config.setExpiration(expiration); config.setBackgroundExpirationEnabled(true); createPool(); latch.await(); assertThat(reallocator.countReallocations(), is(0)); assertThat(reallocator.countDeallocations(), is(0)); } @Test(timeout = TIMEOUT) public void backgroundExpirationMustExpireObjectsWhenExpirationThrows() throws Exception { CountDownLatch latch = new CountDownLatch(1); CountingExpiration expiration = expire( $throwExpire(new Exception()), $countDown(latch, $fresh)); config.setExpiration(expiration); config.setBackgroundExpirationEnabled(true); createPool(); latch.await(); assertThat(allocator.countAllocations(), is(2)); assertThat(allocator.countDeallocations(), is(1)); } @Test(timeout = TIMEOUT) public void backgroundExpirationMustNotExpireObjectsThatAreClaimed() throws Exception { AtomicBoolean hasExpired = new AtomicBoolean(); CountDownLatch latch = new CountDownLatch(4); CountingExpiration expiration = expire( $countDown(latch, $expiredIf(hasExpired))); config.setExpiration(expiration); config.setBackgroundExpirationEnabled(true); config.setSize(2); createPool(); // If applicable, do a thread-local reclaim pool.claim(longTimeout).release(); GenericPoolable obj = pool.claim(longTimeout); hasExpired.set(true); latch.await(); hasExpired.set(false); List<GenericPoolable> deallocations = allocator.getDeallocations(); // Synchronized to guard against concurrent modification from the allocator synchronized (deallocations) { assertThat(deallocations, not(hasItem(obj))); } obj.release(); } @Test(timeout = TIMEOUT) public void mustDeallocateExplicitlyExpiredObjects() throws Exception { int poolSize = 2; config.setSize(poolSize); createPool(); // Explicitly expire a pool size worth of objects for (int i = 0; i < poolSize; i++) { GenericPoolable obj = pool.claim(longTimeout); obj.expire(); obj.release(); } // Grab and release a pool size worth objects as a barrier for reallocating // the expired objects List<GenericPoolable> objs = new ArrayList<>(); for (int i = 0; i < poolSize; i++) { objs.add(pool.claim(longTimeout)); } for (GenericPoolable obj : objs) { obj.release(); } // Now we should see a pool size worth of reallocations assertThat("allocations", allocator.countAllocations(), is(2 * poolSize)); assertThat("deallocations", allocator.countDeallocations(), is(poolSize)); } @Test(timeout = TIMEOUT) public void mustReallocateExplicitlyExpiredObjectsInBackgroundWithBackgroundExpiration() throws Exception { CountDownLatch latch = new CountDownLatch(2); allocator = allocator(alloc($countDown(latch, $new))); config.setAllocator(allocator).setSize(1); config.setBackgroundExpirationEnabled(true); createPool(); GenericPoolable obj = pool.claim(longTimeout); obj.expire(); obj.release(); latch.await(); } @Test(timeout = TIMEOUT) public void mustReallocateExplicitlyExpiredObjectsInBackgroundWithoutBgExpiration() throws Exception { CountDownLatch latch = new CountDownLatch(2); allocator = allocator(alloc($countDown(latch, $new))); config.setAllocator(allocator).setSize(1); config.setBackgroundExpirationEnabled(false); createPool(); GenericPoolable obj = pool.claim(longTimeout); obj.expire(); obj.release(); latch.await(); } @Test(timeout = TIMEOUT) public void mustReplaceExplicitlyExpiredObjectsEvenIfDeallocationFails() throws Exception { allocator = allocator(dealloc($throw(new Exception("Boom!")))); config.setAllocator(allocator).setSize(1); createPool(); GenericPoolable a = pool.claim(longTimeout); a.expire(); a.release(); GenericPoolable b = pool.claim(longTimeout); b.release(); assertThat(a, not(sameInstance(b))); } @Test(timeout = TIMEOUT) public void explicitExpiryFromExpirationMustAllowOneClaimPerObject() throws Exception { config.setExpiration(expire($explicitExpire)); createPool(); GenericPoolable a = pool.claim(longTimeout); a.release(); GenericPoolable b = pool.claim(longTimeout); b.release(); assertThat(a, not(sameInstance(b))); } @Test(timeout = TIMEOUT) public void explicitlyExpiryMustBeIdempotent() throws Exception { createPool(); GenericPoolable a = pool.claim(longTimeout); a.expire(); a.expire(); a.expire(); a.release(); GenericPoolable b = pool.claim(longTimeout); b.release(); assertThat(a, not(sameInstance(b))); assertThat(allocator.countAllocations(), is(2)); assertThat(allocator.countDeallocations(), is(1)); } private static final Consumer<GenericPoolable> nullConsumer = (obj) -> {}; @Test(timeout = TIMEOUT, expected = IllegalArgumentException.class) public void applyMustThrowOnNullTimeout() throws Exception { createPool(); pool.apply(null, identity()); } @Test(timeout = TIMEOUT, expected = IllegalArgumentException.class) public void supplyMustThrowOnNullTimeout() throws Exception { createPool(); pool.supply(null, nullConsumer); } @Test(timeout = TIMEOUT, expected = NullPointerException.class) public void applyMustThrowOnNullFunction() throws Exception { createPool(); pool.apply(longTimeout, null); } @Test(timeout = TIMEOUT, expected = NullPointerException.class) public void supplyMustThrowOnNullConsumer() throws Exception { createPool(); pool.supply(longTimeout, null); } @Test(timeout = TIMEOUT) public void applyMustReturnEmptyIfTimeoutElapses() throws Exception { createPool(); GenericPoolable obj = pool.claim(longTimeout); try { assertFalse(pool.apply(shortTimeout, identity()).isPresent()); } finally { obj.release(); } } @Test(timeout = TIMEOUT) public void supplyMustReturnFalseIfTimeoutElapses() throws Exception { createPool(); GenericPoolable obj = pool.claim(longTimeout); try { assertFalse(pool.supply(shortTimeout, nullConsumer)); } finally { obj.release(); } } @Test(timeout = TIMEOUT) public void applyMustNotCallFunctionIfTimeoutElapses() throws Exception { createPool(); GenericPoolable obj = pool.claim(longTimeout); try { AtomicInteger counter = new AtomicInteger(); pool.apply(shortTimeout, (x) -> (Object) counter.incrementAndGet()); assertThat(counter.get(), is(0)); } finally { obj.release(); } } @Test(timeout = TIMEOUT) public void supplyMustNotCallConsumerIfTimeoutElapses() throws Exception { createPool(); GenericPoolable obj = pool.claim(longTimeout); try { AtomicReference<GenericPoolable> ref = new AtomicReference<>(); pool.supply(shortTimeout, ref::set); assertThat(ref.get(), is(nullValue())); } finally { obj.release(); } } @Test(timeout = TIMEOUT) public void applyMustCallFunctionIfObjectClaimedWithinTimeout() throws Exception { createPool(); AtomicInteger counter = new AtomicInteger(); pool.apply(longTimeout, (x) -> (Object) counter.incrementAndGet()); assertThat(counter.get(), is(1)); } @Test(timeout = TIMEOUT) public void supplyMustCallConsumerIfObjectClaimedWithinTimeout() throws Exception { createPool(); GenericPoolable obj = pool.claim(longTimeout); obj.release(); AtomicReference<GenericPoolable> ref = new AtomicReference<>(); pool.supply(shortTimeout, ref::set); assertThat(ref.get(), is(sameInstance(obj))); } @Test(timeout = TIMEOUT) public void applyMustReturnResultOfFunction() throws Exception { createPool(); String expectedResult = "Result!"; Optional<String> actualResult = pool.apply(longTimeout, (obj) -> expectedResult); assertTrue(actualResult.isPresent()); assertThat(actualResult.get(), is(expectedResult)); } @Test(timeout = TIMEOUT) public void applyMustReturnEmptyIfFunctionReturnsNull() throws Exception { createPool(); assertThat(pool.apply(longTimeout, (obj) -> null), is(Optional.empty())); } @Test(timeout = TIMEOUT) public void applyMustReleaseClaimedObject() throws Exception { createPool(); pool.apply(longTimeout, identity()); pool.apply(longTimeout, identity()); fork(() -> pool.apply(longTimeout, identity())).join(); fork(() -> pool.apply(longTimeout, identity())).join(); pool.apply(longTimeout, identity()); pool.apply(longTimeout, identity()); } @Test(timeout = TIMEOUT) public void supplyMustReleaseClaimedObject() throws Exception { createPool(); pool.supply(longTimeout, nullConsumer); pool.supply(longTimeout, nullConsumer); fork(() -> pool.supply(longTimeout, nullConsumer)).join(); fork(() -> pool.supply(longTimeout, nullConsumer)).join(); pool.supply(longTimeout, nullConsumer); pool.supply(longTimeout, nullConsumer); } private static Object expectException(Callable<?> callable) { try { callable.call(); fail("The ExpectedException was not thrown"); } catch (ExpectedException ignore) { // We expect this } catch (Exception e) { throw new AssertionError("Failed for other reason", e); } return null; } @Test(timeout = TIMEOUT) public void applyMustReleaseClaimedObjectEvenIfFunctionThrows() throws Exception { createPool(); Function<GenericPoolable,Object> thrower = (obj) -> { throw new ExpectedException(); }; expectException(() -> pool.apply(longTimeout, thrower)); expectException(() -> pool.apply(longTimeout, thrower)); fork(() -> expectException(() -> pool.apply(longTimeout, thrower))).join(); fork(() -> expectException(() -> pool.apply(longTimeout, thrower))).join(); expectException(() -> pool.apply(longTimeout, thrower)); expectException(() -> pool.apply(longTimeout, thrower)); } @Test(timeout = TIMEOUT) public void supplyMustReleaseClaimedObjectEvenIfConsumerThrows() throws Exception { createPool(); Consumer<GenericPoolable> thrower = (obj) -> { throw new ExpectedException(); }; expectException(() -> pool.supply(longTimeout, thrower)); expectException(() -> pool.supply(longTimeout, thrower)); fork(() -> expectException(() -> pool.supply(longTimeout, thrower))).join(); fork(() -> expectException(() -> pool.supply(longTimeout, thrower))).join(); expectException(() -> pool.supply(longTimeout, thrower)); expectException(() -> pool.supply(longTimeout, thrower)); } @Test(timeout = TIMEOUT, expected = IllegalStateException.class) public void applyMustThrowIfThePoolIsShutDown() throws Exception { createPool(); pool.shutdown().await(longTimeout); pool.apply(longTimeout, identity()); } @Test(timeout = TIMEOUT, expected = IllegalStateException.class) public void supplyMustThrowIfThePoolIsShutDown() throws Exception { createPool(); pool.shutdown().await(longTimeout); pool.supply(longTimeout, nullConsumer); } @Test(timeout = TIMEOUT, expected = IllegalStateException.class) public void shuttingPoolDownMustUnblockApplyAndThrow() throws Throwable { createPool(); GenericPoolable obj = pool.claim(longTimeout); Thread thread = fork(() -> pool.apply(longTimeout, identity())); AtomicReference<Throwable> exception = capture(thread); waitForThreadState(thread, Thread.State.TIMED_WAITING); Completion shutdown = pool.shutdown(); join(thread); obj.release(); shutdown.await(longTimeout); throw exception.get(); } @Test(timeout = TIMEOUT, expected = IllegalStateException.class) public void shuttingPoolDownMustUnblockSupplyAndThrow() throws Throwable { createPool(); GenericPoolable obj = pool.claim(longTimeout); Thread thread = fork(() -> pool.supply(longTimeout, nullConsumer)); AtomicReference<Throwable> exception = capture(thread); waitForThreadState(thread, Thread.State.TIMED_WAITING); Completion shutdown = pool.shutdown(); join(thread); obj.release(); shutdown.await(longTimeout); throw exception.get(); } @Test(timeout = TIMEOUT, expected = InterruptedException.class) public void applyMustThrowOnInterrupt() throws Exception { createPool(); Thread.currentThread().interrupt(); pool.apply(longTimeout, identity()); } @Test(timeout = TIMEOUT, expected = InterruptedException.class) public void supplyMustThrowOnInterrupt() throws Exception { createPool(); Thread.currentThread().interrupt(); pool.supply(longTimeout, nullConsumer); } @Test(timeout = TIMEOUT, expected = InterruptedException.class) public void blockedApplyMustThrowOnInterrupt() throws Throwable { createPool(); GenericPoolable obj = pool.claim(longTimeout); Thread thread = fork(() -> pool.apply(longTimeout, identity())); AtomicReference<Throwable> exception = capture(thread); waitForThreadState(thread, Thread.State.TIMED_WAITING); thread.interrupt(); join(thread); obj.release(); throw exception.get(); } @Test(timeout = TIMEOUT, expected = InterruptedException.class) public void blockedSupplyMustThrowOnInterrupt() throws Throwable { createPool(); GenericPoolable obj = pool.claim(longTimeout); Thread thread = fork(() -> pool.supply(longTimeout, nullConsumer)); AtomicReference<Throwable> exception = capture(thread); waitForThreadState(thread, Thread.State.TIMED_WAITING); thread.interrupt(); join(thread); obj.release(); throw exception.get(); } @Test(timeout = TIMEOUT) public void applyMustUnblockByConcurrentRelease() throws Exception { createPool(); GenericPoolable obj = pool.claim(longTimeout); Thread thread = fork(() -> pool.apply(longTimeout, identity())); waitForThreadState(thread, Thread.State.TIMED_WAITING); obj.release(); join(thread); } @Test(timeout = TIMEOUT) public void supplyMustUnblockByConcurrentRelease() throws Exception { createPool(); GenericPoolable obj = pool.claim(longTimeout); Thread thread = fork(() -> pool.supply(longTimeout, nullConsumer)); waitForThreadState(thread, Thread.State.TIMED_WAITING); obj.release(); join(thread); } // NOTE: When adding, removing or modifying tests, also remember to update // the javadocs and docs pages. }