/* * 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.slow; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.experimental.categories.Category; import org.junit.rules.TestRule; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; import stormpot.*; import java.lang.management.ManagementFactory; import java.lang.management.ThreadMXBean; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import static java.lang.System.identityHashCode; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; import static org.junit.Assume.assumeTrue; import static org.junit.internal.matchers.ThrowableMessageMatcher.hasMessage; import static stormpot.AlloKit.$countDown; import static stormpot.AlloKit.*; import static stormpot.ExpireKit.*; @SuppressWarnings("unchecked") @Category(SlowTest.class) @RunWith(Parameterized.class) public class PoolIT { @Rule public final TestRule failurePrinter = new FailurePrinterTestRule(); @Rule public final ExecutorTestRule executorTestRule = new ExecutorTestRule(); private static final Timeout longTimeout = new Timeout(1, TimeUnit.MINUTES); private static final Timeout shortTimeout = new Timeout(1, TimeUnit.SECONDS); @Parameters(name = "{0}") public static Object[][] dataPoints() { return new Object[][] { {"blazePool", new BlazePoolFixture()}, {"queuePool", new QueuePoolFixture()} }; } private final PoolFixture fixture; // Initialised by setUp() private CountingAllocator allocator; private Config<GenericPoolable> config; private ExecutorService executor; // Initialised in the tests private Pool<GenericPoolable> pool; @SuppressWarnings("UnusedParameters") public PoolIT(String implementationName, PoolFixture fixture) { this.fixture = fixture; } @Before public void setUp() { allocator = allocator(); config = new Config<GenericPoolable>().setSize(1).setAllocator(allocator); executor = executorTestRule.getExecutorService(); } @After public void verifyObjectsAreNeverDeallocatedMoreThanOnce() throws InterruptedException { assertTrue("pool should have been shut down by the test", pool.shutdown().await(shortTimeout)); pool = null; List<GenericPoolable> deallocated = allocator.getDeallocations(); // Synchronize to avoid ConcurrentModification with background thread synchronized (deallocated) { Collections.sort(deallocated, (a,b) -> Integer.compare(identityHashCode(a), identityHashCode(b))); Iterator<GenericPoolable> iter = deallocated.iterator(); List<GenericPoolable> duplicates = new ArrayList<>(); if (iter.hasNext()) { GenericPoolable a = iter.next(); while (iter.hasNext()) { GenericPoolable b = iter.next(); if (a == b) { duplicates.add(b); } a = b; } } assertThat(duplicates, is(emptyIterableOf(GenericPoolable.class))); } allocator = null; } private void createPool() { pool = fixture.initPool(config); } @Test(timeout = 16010) public void highContentionMustNotCausePoolLeakage() throws Exception { createPool(); Runnable runner = createTaskClaimReleaseUntilShutdown(pool); Future<?> future = executor.submit(runner); executorTestRule.printOnFailure(future); long deadline = System.currentTimeMillis() + 5000; do { pool.claim(longTimeout).release(); } while (System.currentTimeMillis() < deadline); assertTrue(pool.shutdown().await(longTimeout)); future.get(); } private Runnable createTaskClaimReleaseUntilShutdown( final Pool<GenericPoolable> pool, final Class<? extends Throwable>... acceptableExceptions) { return () -> { for (;;) { try { pool.claim(longTimeout).release(); } catch (InterruptedException ignore) { // This is okay } catch (IllegalStateException e) { assertThat(e, hasMessage(equalTo("Pool has been shut down"))); break; } catch (PoolException e) { assertThat(e.getCause().getClass(), isOneOf(acceptableExceptions)); } } }; } @Test(timeout = 16010) public void shutdownMustCompleteSuccessfullyEvenAtHighContention() throws Exception { int size = 100000; config.setSize(size); createPool(); List<Future<?>> futures = new ArrayList<>(); for (int i = 0; i < 64; i++) { Runnable runner = createTaskClaimReleaseUntilShutdown(pool); futures.add(executor.submit(runner)); } executorTestRule.printOnFailure(futures); // Wait for all the objects to be created while (allocator.countAllocations() < size) { Thread.sleep(10); } // Very good, now shut down everything assertTrue(pool.shutdown().await(longTimeout)); // Check that the shut down was orderly for (Future<?> future : futures) { future.get(); } } @Test(timeout = 16010) public void highObjectChurnMustNotCausePoolLeakage() throws Exception { config.setSize(8); Action fallibleAction = new Action() { private final Random rnd = new Random(); @Override public GenericPoolable apply(Slot slot, GenericPoolable obj) throws Exception { // About 20% of allocations, deallocations and reallocations will throw if (rnd.nextInt(1024) < 201) { throw new SomeRandomException(); } return new GenericPoolable(slot); } }; allocator = reallocator( alloc(fallibleAction), dealloc(fallibleAction), realloc(fallibleAction)); config.setAllocator(allocator); config.setExpiration(info -> { int x = info.randomInt(); if ((x & 0xFF) > 250) { // About 3% of checks throw an exception throw new SomeRandomException(); } // About 1 in 8 checks causes expiration return (x & 0x0F) < 0x02; }); createPool(); List<Future<?>> futures = new ArrayList<>(); for (int i = 0; i < 64; i++) { Runnable runner = createTaskClaimReleaseUntilShutdown( pool, SomeRandomException.class); futures.add(executor.submit(runner)); } executorTestRule.printOnFailure(futures); Thread.sleep(5000); // The shutdown completes if no objects are leaked assertTrue(pool.shutdown().await(longTimeout)); for (Future<?> future : futures) { // Also verify that no unexpected exceptions were thrown future.get(); } } @Test(timeout = 16010) public void backgroundExpirationMustDoNothingWhenPoolIsDepleted() throws Exception { AtomicBoolean hasExpired = new AtomicBoolean(); CountingExpiration expiration = expire($expiredIf(hasExpired)); config.setExpiration(expiration); config.setBackgroundExpirationEnabled(true); createPool(); // Do a thread-local reclaim, if applicable, to keep the object in // circulation pool.claim(longTimeout).release(); GenericPoolable obj = pool.claim(longTimeout); int expirationsCount = expiration.countExpirations(); hasExpired.set(true); Thread.sleep(1000); assertThat(allocator.countDeallocations(), is(0)); assertThat(expiration.countExpirations(), is(expirationsCount)); obj.release(); } @Test(timeout = 16010) public void backgroundExpirationMustNotFailWhenThereAreNoObjectsInCirculation() throws Exception { AtomicBoolean hasExpired = new AtomicBoolean(); CountingExpiration expiration = expire($expiredIf(hasExpired)); config.setExpiration(expiration); config.setBackgroundExpirationEnabled(true); createPool(); GenericPoolable obj = pool.claim(longTimeout); int expirationsCount = expiration.countExpirations(); hasExpired.set(true); Thread.sleep(1000); assertThat(allocator.countDeallocations(), is(0)); assertThat(expiration.countExpirations(), is(expirationsCount)); obj.release(); } @Test(timeout = 160100) public void decreasingSizeOfDepletedPoolMustOnlyDeallocateAllocatedObjects() throws Exception { int startingSize = 256; CountDownLatch startLatch = new CountDownLatch(startingSize); Semaphore semaphore = new Semaphore(0); allocator = allocator( alloc($countDown(startLatch, $new)), dealloc($release(semaphore, $null))); config.setSize(startingSize); config.setAllocator(allocator); createPool(); startLatch.await(); List<GenericPoolable> objs = new ArrayList<>(); for (int i = 0; i < startingSize; i++) { objs.add(pool.claim(longTimeout)); } int size = startingSize; List<GenericPoolable> subList = objs.subList(0, startingSize - 1); for (GenericPoolable obj : subList) { size--; pool.setTargetSize(size); // It's important that the wait mask produces values greater than the // allocation threads idle wait time. assertFalse(semaphore.tryAcquire(size & 127, TimeUnit.MILLISECONDS)); obj.release(); semaphore.acquire(); } assertThat(allocator.getDeallocations(), equalTo(subList)); objs.get(startingSize - 1).release(); } @Test(timeout = 160100) public void mustNotDeallocateNullsFromLiveQueueDuringShutdown() throws Exception { int startingSize = 256; CountDownLatch startLatch = new CountDownLatch(startingSize); Semaphore semaphore = new Semaphore(0); allocator = allocator( alloc($countDown(startLatch, $new)), dealloc($release(semaphore, $null))); config.setSize(startingSize); config.setAllocator(allocator); createPool(); startLatch.await(); List<GenericPoolable> objs = new ArrayList<>(); for (int i = 0; i < startingSize; i++) { objs.add(pool.claim(longTimeout)); } Completion completion = pool.shutdown(); int size = startingSize; List<GenericPoolable> subList = objs.subList(0, startingSize - 1); for (GenericPoolable obj : subList) { size--; // It's important that the wait mask produces values greater than the // allocation threads idle wait time. assertFalse(semaphore.tryAcquire(size & 127, TimeUnit.MILLISECONDS)); obj.release(); semaphore.acquire(); } assertThat(allocator.getDeallocations(), equalTo(subList)); objs.get(startingSize - 1).release(); assertTrue("shutdown timeout elapsed", completion.await(longTimeout)); } @Test public void explicitlyExpiredSlotsMustNotCauseBackgroundCPUBurn() throws InterruptedException { final ThreadMXBean threads = ManagementFactory.getThreadMXBean(); final AtomicLong lastUserTimeIncrement = new AtomicLong(); assumeTrue(threads.isCurrentThreadCpuTimeSupported()); allocator = allocator(alloc( measureLastCPUTime(threads, lastUserTimeIncrement))); config.setAllocator(allocator); config.setSize(2); createPool(); GenericPoolable a = pool.claim(longTimeout); GenericPoolable b = pool.claim(longTimeout); a.expire(); Thread.sleep(10); a.release(); a = pool.claim(longTimeout); long millisecondsAllowedToBurnCPU = 5000; Thread.sleep(millisecondsAllowedToBurnCPU); b.expire(); b.release(); b = pool.claim(longTimeout); a.release(); b.release(); long millisecondsSpentBurningCPU = TimeUnit.NANOSECONDS.toMillis(lastUserTimeIncrement.get()); assertThat(millisecondsSpentBurningCPU, is(lessThan(millisecondsAllowedToBurnCPU / 2))); } private Action measureLastCPUTime(final ThreadMXBean threads, final AtomicLong lastUserTimeIncrement) { return new Action() { boolean first = true; @Override public GenericPoolable apply(Slot slot, GenericPoolable obj) throws Exception { if (first) { threads.setThreadCpuTimeEnabled(true); first = false; } long userTime = threads.getCurrentThreadUserTime(); lastUserTimeIncrement.set(userTime - lastUserTimeIncrement.get()); return $new.apply(slot, obj); } }; } @Test public void explicitlyExpiredSlotsThatAreDeallocatedThroughPoolShrinkingMustNotCauseBackgroundCPUBurn() throws InterruptedException { final ThreadMXBean threads = ManagementFactory.getThreadMXBean(); final AtomicLong lastUserTimeIncrement = new AtomicLong(); final AtomicLong maxUserTimeIncrement = new AtomicLong(); assumeTrue(threads.isCurrentThreadCpuTimeSupported()); allocator = allocator(alloc(new Action() { boolean first = true; @Override public GenericPoolable apply(Slot slot, GenericPoolable obj) throws Exception { if (first) { threads.setThreadCpuTimeEnabled(true); first = false; } long userTime = threads.getCurrentThreadUserTime(); long delta = userTime - lastUserTimeIncrement.get(); lastUserTimeIncrement.set(delta); long existingDelta; do { existingDelta = maxUserTimeIncrement.get(); } while ( !maxUserTimeIncrement.compareAndSet( existingDelta, Math.max(delta, existingDelta))); return $new.apply(slot, obj); } })); config.setAllocator(allocator); int size = 30; config.setSize(size); createPool(); LinkedList<GenericPoolable> objs = new LinkedList<>(); for (int i = 0; i < size; i++) { GenericPoolable obj = pool.claim(longTimeout); objs.offer(obj); obj.expire(); } int newSize = size / 3; pool.setTargetSize(newSize); Iterator<GenericPoolable> itr = objs.iterator(); for (int i = size; i >= newSize; i--) { itr.next().release(); itr.remove(); } long millisecondsAllowedToBurnCPU = 5000; Thread.sleep(millisecondsAllowedToBurnCPU); while (itr.hasNext()) { itr.next().release(); } pool.claim(longTimeout).release(); long millisecondsSpentBurningCPU = TimeUnit.NANOSECONDS.toMillis(maxUserTimeIncrement.get()); assertThat(millisecondsSpentBurningCPU, is(lessThan(millisecondsAllowedToBurnCPU / 2))); } @Test public void explicitlyExpiredButUnreleasedSlotsMustNotCauseBackgroundCPUBurn() throws InterruptedException { final ThreadMXBean threads = ManagementFactory.getThreadMXBean(); final AtomicLong lastUserTimeIncrement = new AtomicLong(); assumeTrue(threads.isCurrentThreadCpuTimeSupported()); allocator = allocator(alloc( measureLastCPUTime(threads, lastUserTimeIncrement))); config.setAllocator(allocator); createPool(); GenericPoolable a = pool.claim(longTimeout); a.expire(); long millisecondsAllowedToBurnCPU = 5000; Thread.sleep(millisecondsAllowedToBurnCPU); a.release(); pool.claim(longTimeout).release(); long millisecondsSpentBurningCPU = TimeUnit.NANOSECONDS.toMillis(lastUserTimeIncrement.get()); assertThat(millisecondsSpentBurningCPU, is(lessThan(millisecondsAllowedToBurnCPU / 2))); } @Test public void mustNotBurnTooMuchCPUWhileThePoolIsWorkingOnShrinking() throws InterruptedException { final ThreadMXBean threads = ManagementFactory.getThreadMXBean(); final AtomicLong lastUserTimeIncrement = new AtomicLong(); int size = 20; assumeTrue(threads.isCurrentThreadCpuTimeSupported()); allocator = allocator(alloc( measureLastCPUTime(threads, lastUserTimeIncrement))); config.setAllocator(allocator); config.setSize(size); createPool(); LinkedList<GenericPoolable> objs = new LinkedList<>(); for (int i = 0; i < size; i++) { objs.add(pool.claim(longTimeout)); } pool.setTargetSize(1); long millisecondsAllowedToBurnCPU = 5000; Thread.sleep(millisecondsAllowedToBurnCPU); for (GenericPoolable obj : objs) { obj.expire(); obj.release(); } pool.claim(longTimeout).release(); long millisecondsSpentBurningCPU = TimeUnit.NANOSECONDS.toMillis(lastUserTimeIncrement.get()); assertThat(millisecondsSpentBurningCPU, is(lessThan(millisecondsAllowedToBurnCPU / 2))); } }