/** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 org.apache.openejb.util; import junit.framework.TestCase; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import static java.util.concurrent.TimeUnit.MILLISECONDS; /** * @version $Rev$ $Date$ */ @SuppressWarnings({"unchecked", "StatementWithEmptyBody"}) public class PoolTest extends TestCase { private Pool pool; @Override protected void setUp() throws Exception { Bean.instances.set(0); } @Override protected void tearDown() throws Exception { if (pool != null) pool.stop(); } public void testStrictBasics() throws Exception { System.out.println("PoolTest.testStrictBasics"); exerciseStrictPool(1, 0); exerciseStrictPool(3, 0); exerciseStrictPool(4, 2); exerciseStrictPool(5, 5); } public void testEmptyPool() throws Exception { System.out.println("PoolTest.testEmptyPool"); final int max = 4; final int min = 2; final Pool<Bean> pool = new Pool<Bean>(max, min, true); final List<Pool<Bean>.Entry> entries = drain(pool); // Should have received "max" number of nulls checkMax(max, entries); // All entries should be null for (final Pool<Bean>.Entry entry : entries) { assertNull(entry); } // Pool is drained and no permits are available // Test that an add does not work assertFalse(pool.add(new Bean())); // Fill the pool via push for (int i = 0; i < max; i++) { pool.push(new Bean()); } // Drain, check, then discard all drainCheckPush(max, min, pool); drainCheckPush(max, min, pool); drainCheckPush(max, min, pool); discard(pool, drain(pool)); // Fill the pool one item at a time, check it's integrity for (int i = 1; i <= max; i++) { assertTrue("i=" + i + ", max=" + max, pool.add(new Bean())); final List<Pool<Bean>.Entry> list = drain(pool); checkMax(max, list); checkMin(Math.min(i, min), list); checkEntries(i, list); push(pool, list); } } public void testNonStrictDiscard() throws Exception { System.out.println("PoolTest.testNonStrictDiscard"); final Pool.Builder builder = new Pool.Builder(); builder.setMinSize(0); builder.setMaxSize(1); builder.setStrictPooling(false); builder.setSupplier(new Pool.Supplier<Bean>() { public void discard(final Bean bean, final Pool.Event reason) { bean.discard(); } public Bean create() { // Should never be called return new Bean(); } }); final Pool pool = builder.build().start(); assertNull(pool.pop(0, TimeUnit.MILLISECONDS)); assertNull(pool.pop(0, TimeUnit.MILLISECONDS)); assertNull(pool.pop(0, TimeUnit.MILLISECONDS)); final Bean bean = new Bean(); assertTrue(pool.push(bean)); assertFalse(pool.push(bean)); assertFalse(pool.push(bean)); assertTrue(bean.discarded > 0); assertNotNull(pool.pop(0, TimeUnit.MILLISECONDS)); assertNull(pool.pop(0, TimeUnit.MILLISECONDS)); assertNull(pool.pop(0, TimeUnit.MILLISECONDS)); } private <T> void drainCheckPush(final int max, final int min, final Pool<T> pool) throws InterruptedException { final List<Pool<T>.Entry> list = drain(pool); checkMax(max, list); checkMin(min, list); push(pool, list); } private <T> void discard(final Pool<T> pool, final List<Pool<T>.Entry> list) { for (final Pool<T>.Entry entry : list) { pool.discard(entry); } } private <T> void push(final Pool<T> pool, final List<Pool<T>.Entry> list) { for (final Pool<T>.Entry entry : list) { if (entry != null && entry.get() instanceof Bean) { final Bean bean = (Bean) entry.get(); bean.push(); } pool.push(entry); } } private void exerciseStrictPool(final int max, final int min) throws InterruptedException { Bean.instances.set(0); final Pool<String> pool = new Pool<String>(max, min, true); // Fill the pool for (int i = 0; i < max; i++) { assertTrue(pool.add("a" + i)); } // Add one past the max assertFalse(pool.add("extra")); // Check the contents of the pool final List<Pool<String>.Entry> entries = drain(pool); checkMax(max, entries); checkMin(min, entries); // Push one back and check pool pool.push(entries.remove(0)); final List<Pool<String>.Entry> entries2 = drain(pool); assertEquals(max, entries2.size() + entries.size()); // discard all instances and add new ones entries.addAll(entries2); entries2.clear(); final Iterator<Pool<String>.Entry> iterator = entries.iterator(); while (iterator.hasNext()) { // Attempt two discards, followed by two adds pool.discard(iterator.next()); if (iterator.hasNext()) { pool.discard(iterator.next()); pool.add("s" + System.currentTimeMillis()); } pool.add("s" + System.currentTimeMillis()); } // Once again check min and max final List<Pool<String>.Entry> list = drain(pool); checkMax(max, list); checkMin(min, list); } public void testStrictMultiThreaded() throws Exception { System.out.println("PoolTest.testStrictMultiThreaded"); final int threadCount = 200; final Pool pool = new Pool(10, 5, true); final CountDownLatch startPistol = new CountDownLatch(1); final CountDownLatch startingLine = new CountDownLatch(10); final CountDownLatch finishingLine = new CountDownLatch(threadCount); // Do a business method... final Runnable r = new Runnable() { public void run() { startingLine.countDown(); try { startPistol.await(); final Pool.Entry entry = pool.pop(1000, MILLISECONDS); Thread.sleep(50); if (entry == null) { pool.push(new Bean()); } else { pool.push(entry); } } catch (final TimeoutException e) { // Simple timeout while waiting on pop() } catch (final InterruptedException e) { Thread.interrupted(); } finishingLine.countDown(); } }; // -- READY -- // How much ever the no of client invocations the count should be 10 as only 10 instances will be created. for (int i = 0; i < threadCount; i++) { final Thread t = new Thread(r); t.start(); } // Wait for the beans to reach the finish line startingLine.await(1000, TimeUnit.MILLISECONDS); // -- SET -- assertEquals(0, Bean.instances.get()); // -- GO -- startPistol.countDown(); // go assertTrue(finishingLine.await(5000, TimeUnit.MILLISECONDS)); // -- DONE -- assertEquals(10, Bean.instances.get()); } public void testClose() throws Exception { System.out.println("PoolTest.testClose"); final int min = 4; final int max = 9; final int sweepInterval = 200; final int pause = 1000; final List<Bean> discarded = new CopyOnWriteArrayList<Bean>(); final CountDownLatch discard = new CountDownLatch(max); final Pool.Builder builder = new Pool.Builder(); builder.setMinSize(min); builder.setMaxSize(max); builder.setSweepInterval(new Duration(sweepInterval, TimeUnit.MILLISECONDS)); builder.setSupplier(new Pool.Supplier<Bean>() { public void discard(final Bean bean, final Pool.Event reason) { try { Thread.sleep(pause); } catch (final InterruptedException e) { Thread.interrupted(); } bean.discard(); discarded.add(bean); discard.countDown(); } public Bean create() { return new Bean(); } }); final Pool pool = this.pool = builder.build().start(); // Fill pool to max Bean.instances.set(0); for (int i = 0; i < max; i++) { assertTrue(pool.add(new Bean())); } { // Should have a full, non-null pool final List entries = drain(pool, 100); checkMin(min, entries); checkEntries(max, entries); push(pool, entries); } final long start = System.currentTimeMillis(); assertTrue(pool.close(10, TimeUnit.SECONDS)); final long time = System.currentTimeMillis() - start; // All instances should have been removed assertEquals(max, discarded.size()); // Should have taken at least three seconds assertTrue(time >= pause); } /** * Tests the idle timeout as well as the Thread pool * used to invoke the discard/create jobs. * * @throws Exception exception */ public void testIdleTimeout() throws Exception { System.out.println("PoolTest.testIdleTimeout"); final int min = 4; final int max = 9; final int idleTimeout = 1000; final int sweepInterval = idleTimeout / 4; final List<Bean> discarded = new CopyOnWriteArrayList<Bean>(); final CountDownLatch discard = new CountDownLatch(max - min); final CountDownLatch hold = new CountDownLatch(1); final Pool.Builder<Bean> builder = new Pool.Builder<Bean>(); builder.setMinSize(min); builder.setMaxSize(max); builder.setExecutor(Executors.newFixedThreadPool(5)); builder.setIdleTimeout(new Duration(idleTimeout, TimeUnit.MILLISECONDS)); builder.setSweepInterval(new Duration(sweepInterval, TimeUnit.MILLISECONDS)); builder.setSupplier(new Pool.Supplier<Bean>() { public void discard(final Bean bean, final Pool.Event reason) { bean.discard(); discarded.add(bean); discard.countDown(); try { // Executor should have enough threads // to execute removes on all the // timed out objects. hold.await(); } catch (final InterruptedException e) { Thread.interrupted(); } } public Bean create() { // Should never be called return new Bean(); } }); final Pool pool = this.pool = builder.build().start(); // Fill pool to max Bean.instances.set(0); for (int i = 0; i < max; i++) { assertTrue(pool.add(new Bean())); } { // Should have a full, non-null pool final List entries = drain(pool, 100); checkMin(min, entries); checkEntries(max, entries); // Wait for the timeout -- items should not expire while in use Thread.sleep((long) (idleTimeout * 1.2)); push(pool, entries); } await(discard, 20, TimeUnit.SECONDS); // All non-min instances should have been removed // no more, no less assertEquals(max - min, discarded.size()); for (final Bean bean : discarded) { final long inactive = bean.discarded - bean.accessed; // Actual idle time should not be less than our setting assertTrue("timed out too soon: timeout=" + idleTimeout + ", idle=" + inactive, inactive >= idleTimeout); // It shouldn't be too much more either assertTrue("timed out too long", inactive < idleTimeout + (sweepInterval * 2)); } { // Pool should only have min number of non-null entries final List entries = drain(pool, 100); checkMin(min, entries); checkEntries(min, entries); push(pool, entries); } // -- DONE -- assertEquals(max, Bean.instances.get()); } public void testFlush() throws Exception { System.out.println("PoolTest.testFlush"); final int min = 4; final int max = 9; final int sweepInterval = 200; final List<Bean> discarded = new CopyOnWriteArrayList<Bean>(); final CountDownLatch discard = new CountDownLatch(max); final CountDownLatch created = new CountDownLatch(min); final CountDownLatch createInstances = new CountDownLatch(1); final Pool.Builder builder = new Pool.Builder(); builder.setMinSize(min); builder.setMaxSize(max); builder.setSweepInterval(new Duration(sweepInterval, TimeUnit.MILLISECONDS)); builder.setSupplier(new Pool.Supplier<Bean>() { public void discard(final Bean bean, final Pool.Event reason) { bean.discard(); discarded.add(bean); discard.countDown(); } public Bean create() { try { createInstances.await(); } catch (final InterruptedException e) { Thread.interrupted(); } try { return new Bean(); } finally { created.countDown(); } } }); final Pool pool = this.pool = builder.build().start(); // Fill pool to max Bean.instances.set(0); for (int i = 0; i < max; i++) { assertTrue(pool.add(new Bean())); } { // Should have a full, non-null pool final List entries = drain(pool, 100); checkMin(min, entries); checkEntries(max, entries); push(pool, entries); } pool.flush(); // Wait for the Evictor to come around and sweep out the pool await(discard, 10, TimeUnit.SECONDS); // All instances should have been removed assertEquals(max, discarded.size()); for (final Bean bean : discarded) { final long flushTime = bean.discarded - bean.accessed; // It shouldn't be too much more either assertTrue("flush took too long", flushTime < (sweepInterval * 2)); } // Minimum instances are still being created // The rest of the pool should be empty (null) { final List entries = drain(pool, 100); // Should have "non-min" number of entries checkMax(max - min, entries); // Nothing should be a "min" item as those // are still being created checkMin(0, entries); // And as the pool was just drained all the // entries we get should be null checkNull(entries); push(pool, entries); } Bean.instances.set(0); // Try and trick the pool into adding more "min" items // Fill the pool as much as we can -- should only let us // fill to the max factoring in the "min" instances that // are currently being created { final List entries = drain(pool, 100); // Should reject the instance we try to add assertFalse(pool.add(new Bean())); // Empty the pool discard(pool, entries); // Now count how many instances it lets us add Bean.instances.set(0); while (pool.add(new Bean())) ; // As the "min" instances are still being created // it should only let us fill max - min, then + 1 // to account for the instance that gets rejected // and terminates the while loop final int expected = max - min + 1; assertEquals(expected, Bean.instances.getAndSet(0)); } // Ok, let the "min" instance creation threads continue createInstances.countDown(); // Wait for the "min" instance creation to complete assertTrue(created.await(sweepInterval * 10, TimeUnit.MILLISECONDS)); { // Pool should be full again final List entries = drain(pool, 100); checkMin(min, entries); checkEntries(max, entries); push(pool, entries); } // -- DONE -- } public void testMaxAge() throws Exception { System.out.println("PoolTest.testMaxAge"); final int min = 4; final int max = 9; final int maxAge = 1000; final int sweepInterval = maxAge / 4; final List<Bean> discarded = new CopyOnWriteArrayList<Bean>(); final CountDownLatch discard = new CountDownLatch(max); final CountDownLatch created = new CountDownLatch(min); final CountDownLatch createInstances = new CountDownLatch(1); final Pool.Builder builder = new Pool.Builder(); builder.setMinSize(min); builder.setMaxSize(max); builder.setMaxAge(new Duration(maxAge, MILLISECONDS)); builder.setSweepInterval(new Duration(sweepInterval, MILLISECONDS)); builder.setSupplier(new Pool.Supplier<Bean>() { public void discard(final Bean bean, final Pool.Event reason) { bean.discard(); discarded.add(bean); countDown(discard, bean, "discarded"); } public Bean create() { try { createInstances.await(); } catch (final InterruptedException e) { Thread.interrupted(); } try { return new Bean(); } finally { created.countDown(); } } }); final Pool pool = this.pool = builder.build().start(); // Fill pool to max Bean.instances.set(0); for (int i = 0; i < max; i++) { assertTrue(pool.add(new Bean())); } { // Should have a full, non-null pool final List entries = drain(pool, 100); checkMin(min, entries); checkEntries(max, entries); push(pool, entries); } // Now wait for the instances in the pool to expire await(discard, 10, TimeUnit.SECONDS); // All instances should have been removed assertEquals(max, discarded.size()); for (final Bean bean : discarded) { final long age = bean.discarded - bean.created; // Actual idle time should not be less than our setting assertTrue("died too soon", age >= maxAge); // It shouldn't be too much more either assertTrue("lived too long", age < maxAge + (sweepInterval * 2)); } // Minimum instances are still being created // The rest of the pool should be empty (null) { final List entries = drain(pool, 100); // Nothing should be a "min" item as those // are still being created checkMin(0, entries); // And as the pool was just drained all the // entries we get should be null checkNull(entries); // Should have "non-min" number of entries checkMax(max - min, entries); push(pool, entries); } Bean.instances.set(0); // Try and trick the pool into adding more "min" items // Fill the pool as much as we can -- should only let us // fill to the max factoring in the "min" instances that // are currently being created { final List entries = drain(pool, 100); // Should reject the instance we try to add assertFalse(pool.add(new Bean())); // Empty the pool discard(pool, entries); // Now count how many instances it lets us add Bean.instances.set(0); while (pool.add(new Bean())) ; // As the "min" instances are still being created // it should only let us fill max - min, then + 1 // to account for the instance that gets rejected // and terminates the while loop final int expected = max - min + 1; assertEquals(expected, Bean.instances.getAndSet(0)); } // Ok, let the "min" instance creation threads continue createInstances.countDown(); // Wait for the "min" instance creation to complete await(created, 10, TimeUnit.SECONDS); { // Pool should be full again final List entries = drain(pool, 100); checkMin(min, entries); checkEntries(max, entries); push(pool, entries); } // -- DONE -- } private void countDown(final CountDownLatch discarded, final Bean o, final String event) { discarded.countDown(); // System.out.format("%1$tH:%1$tM:%1$tS.%1$tL " + event + " %2$s\n", System.currentTimeMillis(), o); // try { // Thread.sleep(50); // } catch (InterruptedException e) { // Thread.interrupted(); // } } /** * What happens if we fail to create a "min" instance after a flush? * <p/> * The pool should naturally balance itself out by promoting regular instances * to "min" instance as things are popped and pushed to and from the pool. * * @throws Exception exception */ public void testFlushFailedCreation() throws Exception { System.out.println("PoolTest.testFlushFailedCreation"); final int min = 4; final int max = 9; final int poll = 200; final CountDownLatch discarded = new CountDownLatch(max); final CountDownLatch created = new CountDownLatch(min); final CountDownLatch createInstances = new CountDownLatch(1); final Pool.Builder builder = new Pool.Builder(); builder.setMinSize(min); builder.setMaxSize(max); builder.setSweepInterval(new Duration(poll, TimeUnit.MILLISECONDS)); builder.setSupplier(new Pool.Supplier() { public void discard(final Object o, final Pool.Event reason) { discarded.countDown(); } public Object create() { try { createInstances.await(); } catch (final InterruptedException e) { Thread.interrupted(); } try { throw new RuntimeException(); } finally { created.countDown(); } } }); final Pool pool = this.pool = builder.build().start(); // Fill pool to max Bean.instances.set(0); for (int i = 0; i < max; i++) { assertTrue(pool.add(new Bean())); } { // Should have a full, non-null pool final List entries = drain(pool, 100); checkMin(min, entries); checkEntries(max, entries); push(pool, entries); } pool.flush(); // Wait for the Evictor to come around and sweep out the pool await(discarded, 10, TimeUnit.SECONDS); // Minimum instances are still being created // The rest of the pool should be empty (null) { final List entries = drain(pool, 100); // Should have "non-min" number of entries checkMax(max - min, entries); // Nothing should be a "min" item as those // are still being created checkMin(0, entries); // And as the pool was just drained all the // entries we get should be null checkNull(entries); push(pool, entries); } Bean.instances.set(0); // Try and trick the pool into adding more "min" items // Fill the pool as much as we can -- should only let us // fill to the max factoring in the "min" instances that // are currently being created { final List entries = drain(pool, 100); // Should reject the instance we try to add assertFalse(pool.add(new Bean())); // Empty the pool discard(pool, entries); // Now count how many instances it lets us add Bean.instances.set(0); while (pool.add(new Bean())) ; // As the "min" instances are still being created // it should only let us fill max - min, then + 1 // to account for the instance that gets rejected // and terminates the while loop final int expected = max - min + 1; assertEquals(expected, Bean.instances.getAndSet(0)); } // Ok, let the "min" instance creation threads continue createInstances.countDown(); // Wait for the "min" instance creation to complete await(created, 10, TimeUnit.SECONDS); { // Pool should be full but... final List entries = drain(pool, 100); // we failed to create the min instances checkMin(0, entries); // the "min" entries should have been freed up // and we should have all the possible entires checkMax(max, entries); // though there should be "min" quantities of nulls checkEntries(max - min, entries); // Now when we push these back in, the right number // of entries should be converted to "min" entries push(pool, entries); } { // Pool should be full but... final List entries = drain(pool, 100); // now we should have the right number of mins checkMin(min, entries); // should still have a full pool checkMax(max, entries); // though there should still be "min" quantities of nulls // as we still haven't created any more instances, we just // converted some of our instances into "min" entries checkEntries(max - min, entries); } // -- DONE -- } /** * What happens if we fail to create a "min" instance after a maxAge expiration? * <p/> * The pool should naturally balance itself out by promoting regular instances * to "min" instance as things are popped and pushed to and from the pool. * * @throws Exception exception */ public void testMaxAgeFailedCreation() throws Exception { System.out.println("PoolTest.testMaxAgeFailedCreation"); final int min = 4; final int max = 9; final int maxAge = 5000; final int poll = 100; final CountDownLatch discarded = new CountDownLatch(max); final CountDownLatch created = new CountDownLatch(min); final CountDownLatch createInstances = new CountDownLatch(1); final Pool.Builder builder = new Pool.Builder(); builder.setMinSize(min); builder.setMaxSize(max); builder.setMaxAge(new Duration(maxAge, MILLISECONDS)); builder.setSweepInterval(new Duration(poll, MILLISECONDS)); builder.setSupplier(new Pool.Supplier<Bean>() { public void discard(final Bean o, final Pool.Event reason) { countDown(discarded, o, "discarded"); } public Bean create() { try { createInstances.await(); } catch (final InterruptedException e) { Thread.interrupted(); } try { throw new RuntimeException(); } finally { created.countDown(); } } }); final Pool pool = this.pool = builder.build().start(); // Fill pool to max Bean.instances.set(0); for (int i = 0; i < max; i++) { assertTrue("bean not added " + i, pool.add(new Bean())); } { // Should have a full, non-null pool final List entries = drain(pool, 1000); checkMin(min, entries); checkEntries(max, entries); push(pool, entries); } // Now wait for the instances in the pool to expire await(discarded, maxAge * 4, TimeUnit.MILLISECONDS); // Minimum instances are still being created // The rest of the pool should be empty (null) { final List entries = drain(pool, 1000); // Should have "non-min" number of entries checkMax(max - min, entries); // Nothing should be a "min" item as those // are still being created checkMin(0, entries); // And as the pool was just drained all the // entries we get should be null checkNull(entries); push(pool, entries); } Bean.instances.set(0); // Try and trick the pool into adding more "min" items // Fill the pool as much as we can -- should only let us // fill to the max factoring in the "min" instances that // are currently being created { final List entries = drain(pool, 1000); // Should reject the instance we try to add assertFalse(pool.add(new Bean())); // Empty the pool discard(pool, entries); // Now count how many instances it lets us add Bean.instances.set(0); while (pool.add(new Bean())) ; // As the "min" instances are still being created // it should only let us fill max - min, then + 1 // to account for the instance that gets rejected // and terminates the while loop final int expected = max - min + 1; assertEquals(expected, Bean.instances.getAndSet(0)); } // Ok, let the "min" instance creation threads continue createInstances.countDown(); // Wait for the "min" instance creation to complete await(created, 10, TimeUnit.SECONDS); { // Pool should be full but... final List entries = drain(pool, 1000); // we failed to create the min instances checkMin(0, entries); // the "min" entries should have been freed up // and we should have all the possible entires checkMax(max, entries); // though there should be "min" quantities of nulls checkEntries(max - min, entries); // Now when we push these back in, the right number // of entries should be converted to "min" entries push(pool, entries); } { // Pool should be full but... final List entries = drain(pool, 1000); // now we should have the right number of mins checkMin(min, entries); // should still have a full pool checkMax(max, entries); // though there should still be "min" quantities of nulls // as we still haven't created any more instances, we just // converted some of our instances into "min" entries checkEntries(max - min, entries); } // -- DONE -- } /** * When an item is in use, it should still be flushed * upon return to the pool if the pool was flushed * <p/> * Active items should still be flushed the moment they * become inactive (returned to the pool). * * @throws Exception */ public void testFlushOnReturn() throws Exception { System.out.println("PoolTest.testFlushOnReturn"); final int min = 4; final int max = 9; // Effectively disable sweeping the pool and // verify that Flush is still enforced by the // simple act of trying to return a flushed // item to the pool final int sweepInterval = Integer.MAX_VALUE; final CountDownLatch created = new CountDownLatch(min); final CountDownLatch createInstances = new CountDownLatch(1); final Pool.Builder builder = new Pool.Builder(); builder.setMinSize(min); builder.setMaxSize(max); builder.setSweepInterval(new Duration(sweepInterval, TimeUnit.MILLISECONDS)); builder.setSupplier(new Pool.Supplier<Bean>() { public void discard(final Bean bean, final Pool.Event reason) { bean.discard(); } public Bean create() { try { createInstances.await(); } catch (final InterruptedException e) { Thread.interrupted(); } try { return new Bean(); } finally { created.countDown(); } } }); final Pool pool = this.pool = builder.build().start(); // Fill pool to max Bean.instances.set(0); for (int i = 0; i < max; i++) { assertTrue(pool.add(new Bean())); } { // Should have a full, non-null pool final List entries = drain(pool, 100); checkMin(min, entries); checkEntries(max, entries); push(pool, entries); } { // Drain pool again, flush, then try and return them final List<Pool<Bean>.Entry> entries = drain(pool, 100); checkEntries(max, entries); // Now call flush pool.flush(); for (final Pool<Bean>.Entry entry : entries) { assertFalse("entry should not be accepted", pool.push(entry)); } } // Minimum instances are still being created // The rest of the pool should be empty (null) { final List entries = drain(pool, 100); // Should have "non-min" number of entries checkMax(max - min, entries); // Nothing should be a "min" item as those // are still being created checkMin(0, entries); // And as the pool was just drained all the // entries we get should be null checkNull(entries); push(pool, entries); } Bean.instances.set(0); // Try and trick the pool into adding more "min" items // Fill the pool as much as we can -- should only let us // fill to the max factoring in the "min" instances that // are currently being created { final List entries = drain(pool, 100); // Should reject the instance we try to add assertFalse(pool.add(new Bean())); // Empty the pool discard(pool, entries); // Now count how many instances it lets us add Bean.instances.set(0); while (pool.add(new Bean())) ; // As the "min" instances are still being created // it should only let us fill max - min, then + 1 // to account for the instance that gets rejected // and terminates the while loop final int expected = max - min + 1; assertEquals(expected, Bean.instances.getAndSet(0)); } // Ok, let the "min" instance creation threads continue createInstances.countDown(); // Wait for the "min" instance creation to complete await(created, 10, TimeUnit.SECONDS); { // Pool should be full again final List entries = drain(pool, 100); checkMin(min, entries); checkEntries(max, entries); push(pool, entries); } // -- DONE -- } /** * When an item is in use, it should still be expired * upon return to the pool if the item has lived too long * <p/> * Active items that have lived too long should still be * expired the moment they become inactive (returned to the pool). * * @throws Exception */ public void testMaxAgeOnReturn() throws Exception { System.out.println("PoolTest.testMaxAgeOnReturn"); final int min = 4; final int max = 9; final int maxAge = 200; final int sweepInterval = Integer.MAX_VALUE; final CountDownLatch created = new CountDownLatch(min); final CountDownLatch createInstances = new CountDownLatch(1); final Pool.Builder builder = new Pool.Builder(); builder.setMinSize(min); builder.setMaxSize(max); builder.setMaxAge(new Duration(maxAge, MILLISECONDS)); builder.setSweepInterval(new Duration(sweepInterval, MILLISECONDS)); builder.setSupplier(new Pool.Supplier<Bean>() { public void discard(final Bean bean, final Pool.Event reason) { bean.discard(); } public Bean create() { try { createInstances.await(); } catch (final InterruptedException e) { Thread.interrupted(); } try { return new Bean(); } finally { created.countDown(); } } }); final Pool pool = this.pool = builder.build().start(); // Fill pool to max Bean.instances.set(0); for (int i = 0; i < max; i++) { assertTrue(pool.add(new Bean())); } { // Should have a full, non-null pool final List entries = drain(pool, 100); checkMin(min, entries); checkEntries(max, entries); push(pool, entries); } { // Drain pool again, flush, then try and return them final List<Pool<Bean>.Entry> entries = drain(pool, 100); checkEntries(max, entries); // Now wait for max age Thread.sleep(maxAge); for (final Pool<Bean>.Entry entry : entries) { assertFalse("entry should not be accepted", pool.push(entry)); } } // Minimum instances are still being created // The rest of the pool should be empty (null) { final List entries = drain(pool, 100); // Nothing should be a "min" item as those // are still being created checkMin(0, entries); // And as the pool was just drained all the // entries we get should be null checkNull(entries); // Should have "non-min" number of entries checkMax(max - min, entries); push(pool, entries); } Bean.instances.set(0); // Try and trick the pool into adding more "min" items // Fill the pool as much as we can -- should only let us // fill to the max factoring in the "min" instances that // are currently being created { final List entries = drain(pool, 100); checkMax(max - min, entries); // Should reject the instance we try to add assertFalse(pool.add(new Bean())); // Empty the pool discard(pool, entries); // Now count how many instances it lets us add Bean.instances.set(0); while (pool.add(new Bean())) ; // As the "min" instances are still being created // it should only let us fill max - min, then + 1 // to account for the instance that gets rejected // and terminates the while loop final int expected = max - min + 1; assertEquals(expected, Bean.instances.getAndSet(0)); } // Ok, let the "min" instance creation threads continue createInstances.countDown(); // Wait for the "min" instance creation to complete await(created, 10, TimeUnit.SECONDS); { // Pool should be full again final List entries = drain(pool, 100); checkMin(min, entries); checkEntries(max, entries); push(pool, entries); } // -- DONE -- } private static void await(final CountDownLatch latch, final int timeout, final TimeUnit seconds) throws InterruptedException { if (!latch.await(timeout, seconds)) { // String path = "<dump-failed>"; // try { // File tmp = File.createTempFile(PoolTest.class.getSimpleName(), "-dump.txt"); // path = HeapDump.to(tmp.getAbsolutePath()); // } catch (IOException e) { // e.printStackTrace(); // } fail("latch.await timed out: " + latch); } } private static <T> void checkMax(final int max, final List<Pool<T>.Entry> entries) { assertEquals(max, entries.size()); } private static <T> void checkMin(final int min, final List<Pool<T>.Entry> entries) { assertEquals(min, getMin(entries).size()); } private static <T> void checkNull(final List<Pool<T>.Entry> entries) { for (final Pool<T>.Entry entry : entries) { assertNull(entry); } } private static <T> List<Pool<T>.Entry> getMin(final List<Pool<T>.Entry> entries) { final List<Pool<T>.Entry> list = new ArrayList<Pool<T>.Entry>(); for (final Pool<T>.Entry entry : entries) { if (entry != null && entry.hasHardReference()) list.add(entry); } return list; } private static <T> void checkEntries(final int expected, final List<Pool<T>.Entry> entries) { int found = 0; for (final Pool<T>.Entry entry : entries) { if (entry == null) continue; found++; assertNotNull(entry.get()); } assertEquals(expected, found); } private static <T> List<Pool<T>.Entry> drain(final Pool<T> pool) throws InterruptedException { return drain(pool, 0); } private static <T> List<Pool<T>.Entry> drain(final Pool<T> pool, final int timeout) throws InterruptedException { final List<Pool<T>.Entry> entries = new ArrayList<Pool<T>.Entry>(); try { while (true) { entries.add(pool.pop(timeout, MILLISECONDS)); } } catch (final TimeoutException e) { // pool drained } return entries; } public static final class Bean { public static AtomicInteger instances = new AtomicInteger(); private final int count; private final long created; private long accessed; private long discarded; public Bean() { created = System.currentTimeMillis(); count = instances.incrementAndGet(); } public long getCreated() { return created; } public long getAccessed() { return accessed; } public long getDiscarded() { return discarded; } public void push() { accessed = System.currentTimeMillis(); } public void discard() { discarded = System.currentTimeMillis(); } public int count() { return count; } @Override public String toString() { return "Bean{" + "" + count + // "count=" + count + // ", created=" + created + // ", accessed=" + accessed + // ", discarded=" + discarded + '}'; } } }