/** Copyright (C) SYSTAP, LLC DBA Blazegraph 2006-2016. All rights reserved. Contact: SYSTAP, LLC DBA Blazegraph 2501 Calvert ST NW #106 Washington, DC 20008 licenses@blazegraph.com This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; version 2 of the License. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ /* * Created on Jan 31, 2012 */ package com.bigdata.cache; import java.util.Stack; import junit.framework.AssertionFailedError; import junit.framework.TestCase2; import com.bigdata.cache.SynchronizedHardReferenceQueueWithTimeout.IRef; /** * Test suite for {@link SynchronizedHardReferenceQueueWithTimeout}. * * @author <a href="mailto:thompsonbry@users.sourceforge.net">Bryan Thompson</a> * @version $Id$ * * TODO Write a unit test for the timeout mechanism. Note that it is * invoked with a minimum granularity of 5 seconds by the JVM wide * cleaner thread. */ public class TestSynchronizedHardReferenceQueueWithTimeout extends TestCase2 { /** * */ public TestSynchronizedHardReferenceQueueWithTimeout() { } /** * @param name */ public TestSynchronizedHardReferenceQueueWithTimeout(String name) { super(name); } /** * Test constructor and its post-conditions. */ public void test_ctor() { final SynchronizedHardReferenceQueueWithTimeout<String> cache = new SynchronizedHardReferenceQueueWithTimeout<String>( 100/* capacity */, 20/* nscan */, 10000L/* timeout */); // assertEquals("listener", listener, cache.getListener()); assertEquals("timeout", 10000L, cache.timeout()); assertEquals("capacity", 100, cache.capacity()); assertEquals("size", 0, cache.size()); assertEquals("nscan", 20, cache.nscan()); assertEquals("isEmpty", true, cache.isEmpty()); assertEquals("isFull", false, cache.isFull()); } /** * Correct rejection tests for the constructor. */ public void test_ctor_correct_rejection() { // try { // new HardReferenceQueue<String>(null, 100); // fail("Expecting: " + IllegalArgumentException.class); // } catch (IllegalArgumentException ex) { // System.err.println("Ignoring expectedRefs exception: " + ex); // } // nscan MAY be ZERO (0) new SynchronizedHardReferenceQueueWithTimeout<String>( 10/* capacity */, 0/* nscan */, 10000L/* timeout */); // capacity MAY NOT be ZERO (0) try { new SynchronizedHardReferenceQueueWithTimeout<String>( 0/* capacity */, 0/* nscan */, 10000L/* timeout */); fail("Expecting: " + IllegalArgumentException.class); } catch (IllegalArgumentException ex) { if(log.isInfoEnabled()) log.info("Ignoring expected exception: " + ex); } // nscan MAY be EQ capacity new SynchronizedHardReferenceQueueWithTimeout<String>( 10/* capacity */, 10/* nscan */, 10000L/* timeout */); // nscan MAY NOT be GT capacity try { new SynchronizedHardReferenceQueueWithTimeout<String>( 10/* capacity */, 11/* nscan */, 10000L/* timeout */); fail("Expecting: " + IllegalArgumentException.class); } catch (IllegalArgumentException ex) { if(log.isInfoEnabled()) log.info("Ignoring expected exception: " + ex); } // timeout MAY be ZERO (0) (implies NO timeout). new SynchronizedHardReferenceQueueWithTimeout<String>( 10/* capacity */, 10/* nscan */, 0L/* timeout */); } /** * Correct rejection test for appending a null reference to the cache. */ public void test_append_null() { final SynchronizedHardReferenceQueueWithTimeout<String> cache = new SynchronizedHardReferenceQueueWithTimeout<String>( 100/* capacity */, 2/* nscan */, 0L/* timeout */); try { cache.add(null); fail("Expecting: " + IllegalArgumentException.class); } catch (IllegalArgumentException ex) { if(log.isInfoEnabled()) log.info("Ignoring expected exception: " + ex); } } /** * Verify that the indirected references on the innerQueue are the same as * the given references. * * @param expected * The expected references in the expected order. * @param innerQueue * The innerQueue whose order is to be verified. */ private void assertSameOrder(final String[] expected, final HardReferenceQueue<IRef<String>> innerQueue) { final IRef<String>[] actual = innerQueue.toArray(new IRef[0]); final String[] actual2 = new String[actual.length]; for (int i = 0; i < actual.length; i++) { // Indirection. actual2[i] = actual[i] == null ? null : actual[i].get(); } assertEquals("order", expected, actual2); } /** * <p> * Test verifies that we can add distinct references until the cache is full * and that a subsequent add causes an eviction notice. While the cache is * full, we then explicitly evict the LRU reference and verify that the * cache state correctly reflects the eviction. Finally, we test * evictAll(false) (does not clear the references from the cache) and * evictAll(true) (clears the references from the cache). * </p> * <p> * Note that scanning of the last N references added is disabled for this * test. * </p> */ public void test_add_evict() { final int capacity = 5; final int nscan = 0; final long timeoutNanos = 0L; final MyListener<String, IRef<String>> listener = new MyListener<String, IRef<String>>(); final SynchronizedHardReferenceQueueWithTimeout<String> cache = new SynchronizedHardReferenceQueueWithTimeout<String>( listener, capacity, nscan, timeoutNanos); final HardReferenceQueue<IRef<String>> innerQueue = cache.getQueue(); final String ref0 = "0"; final String ref1 = "1"; final String ref2 = "2"; final String ref3 = "3"; final String ref4 = "4"; final String ref5 = "5"; assertEquals("size",0,cache.size()); assertEquals("tail",0,innerQueue.getTailIndex()); assertEquals("head",0,innerQueue.getHeadIndex()); assertTrue("empty",cache.isEmpty()); assertFalse("full",cache.isFull()); assertSameOrder(new String[]{}, innerQueue); // assertEquals("order",new String[]{},innerQueue.toArray(new String[0])); assertTrue(cache.add(ref0)); assertEquals("size",1,cache.size()); assertEquals("tail",0,innerQueue.getTailIndex()); assertEquals("head",1,innerQueue.getHeadIndex()); assertFalse("empty",cache.isEmpty()); assertFalse("full",cache.isFull()); assertSameOrder(new String[]{ref0}, innerQueue); // assertEquals("order",new String[]{ref0},innerQueue.toArray(new String[0])); assertTrue(cache.add(ref1)); assertEquals("size",2,cache.size()); assertEquals("tail",0,innerQueue.getTailIndex()); assertEquals("head",2,innerQueue.getHeadIndex()); assertFalse("empty",cache.isEmpty()); assertFalse("full",cache.isFull()); assertSameOrder(new String[]{ref0,ref1}, innerQueue); // assertEquals("order",new String[]{ref0,ref1},innerQueue.toArray(new String[0])); assertTrue(cache.add(ref2)); assertEquals("size",3,cache.size()); assertEquals("tail",0,innerQueue.getTailIndex()); assertEquals("head",3,innerQueue.getHeadIndex()); assertFalse("empty",cache.isEmpty()); assertFalse("full",cache.isFull()); assertSameOrder(new String[]{ref0,ref1,ref2}, innerQueue); // assertEquals("order",new String[]{ref0,ref1,ref2},innerQueue.toArray(new String[0])); assertTrue(cache.add(ref3)); assertEquals("size",4,cache.size()); assertEquals("tail",0,innerQueue.getTailIndex()); assertEquals("head",4,innerQueue.getHeadIndex()); assertFalse("empty",cache.isEmpty()); assertFalse("full",cache.isFull()); assertSameOrder(new String[]{ref0,ref1,ref2,ref3}, innerQueue); // assertEquals("order",new String[]{ref0,ref1,ref2,ref3},innerQueue.toArray(new String[0])); assertTrue(cache.add(ref4)); assertEquals("size",5,cache.size()); assertEquals("tail",0,innerQueue.getTailIndex()); assertEquals("head",0,innerQueue.getHeadIndex()); assertFalse("empty",cache.isEmpty()); assertTrue("full",cache.isFull()); assertSameOrder(new String[]{ref0,ref1,ref2,ref3,ref4}, innerQueue); // assertEquals("order",new String[]{ref0,ref1,ref2,ref3,ref4},innerQueue.toArray(new String[0])); listener.setExpectedRef(ref0); assertTrue(cache.add(ref5)); listener.assertEvicted(); assertEquals("size",5,cache.size()); assertEquals("tail",1,innerQueue.getTailIndex()); assertEquals("head",1,innerQueue.getHeadIndex()); assertFalse("empty",cache.isEmpty()); assertTrue("full",cache.isFull()); assertSameOrder(new String[]{ref1,ref2,ref3,ref4,ref5}, innerQueue); // assertEquals("order",new String[]{ref1,ref2,ref3,ref4,ref5},innerQueue.toArray(new String[0])); /* * Evict the LRU reference and verify that the cache size goes down by * one. */ listener.setExpectedRef(ref1); assertTrue(cache.evict()); listener.assertEvicted(); assertEquals("size",4,cache.size()); assertEquals("tail",2,innerQueue.getTailIndex()); assertEquals("head",1,innerQueue.getHeadIndex()); assertFalse("empty",cache.isEmpty()); assertFalse("full",cache.isFull()); assertSameOrder(new String[]{ref2,ref3,ref4,ref5}, innerQueue); // assertEquals("order",new String[]{ref2,ref3,ref4,ref5},innerQueue.toArray(new String[0])); /* * add a reference - no eviction since the cache was not at capacity. As * a post-condition, the cache is once again at capacity. */ assertTrue(cache.add(ref4)); assertEquals("size",5,cache.size()); assertEquals("tail",2,innerQueue.getTailIndex()); assertEquals("head",2,innerQueue.getHeadIndex()); assertFalse("empty",cache.isEmpty()); assertTrue("full",cache.isFull()); assertSameOrder(new String[]{ref2,ref3,ref4,ref5,ref4}, innerQueue); // assertEquals("order",new String[]{ref2,ref3,ref4,ref5,ref4},innerQueue.toArray(new String[0])); /* * Add another reference and verify that an eviction occurs. */ listener.setExpectedRef(ref2); assertTrue(cache.add(ref2)); listener.assertEvicted(); assertEquals("size",5,cache.size()); assertEquals("tail",3,innerQueue.getTailIndex()); assertEquals("head",3,innerQueue.getHeadIndex()); assertFalse("empty",cache.isEmpty()); assertTrue("full",cache.isFull()); assertSameOrder(new String[]{ref3,ref4,ref5,ref4,ref2}, innerQueue); // assertEquals("order",new String[]{ref3,ref4,ref5,ref4,ref2},innerQueue.toArray(new String[0])); /* * Test evictAll(false) (does not change the cache state). */ int nevicted = listener.getEvictionCount(); listener.setExpectedRefs(new String[]{ref3,ref4,ref5,ref4,ref2}); cache.evictAll(false); listener.assertEvictionCount(nevicted+5); assertEquals("size",5,cache.size()); assertEquals("tail",3,innerQueue.getTailIndex()); assertEquals("head",3,innerQueue.getHeadIndex()); assertFalse("empty",cache.isEmpty()); assertTrue("full",cache.isFull()); assertSameOrder(new String[]{ref3,ref4,ref5,ref4,ref2}, innerQueue); // assertEquals("order",new String[]{ref3,ref4,ref5,ref4,ref2},innerQueue.toArray(new String[0])); /* * Test evictAll(false) (actually evicts the references from the cache). */ nevicted = listener.getEvictionCount(); listener.setExpectedRefs(new String[]{ref3,ref4,ref5,ref4,ref2}); cache.evictAll(true); listener.assertEvictionCount(nevicted+5); assertEquals("size",0,cache.size()); assertEquals("tail",3,innerQueue.getTailIndex()); assertEquals("head",3,innerQueue.getHeadIndex()); assertTrue("empty",cache.isEmpty()); assertFalse("full",cache.isFull()); assertSameOrder(new String[]{}, innerQueue); // assertEquals("order",new String[]{},innerQueue.toArray(new String[0])); } /** * Test verifies scan of the last N references when adding a reference to * the cache. When the test starts the tail is at index 0, but eventually we * wrap the cache around and continue testing to make sure that scans * function correctly with a head index of 0 (this requires continuing the * scan from the array capacity). * <p> * Note: This is more complex than for the simple {@link HardReferenceQueue} * due to the indirection through the {@link IRef} object, which breaks * reference testing for equality. The scanHead() method in the inner queue * was modified in order for this test to pass. * * @see <a * href="https://sourceforge.net/apps/trac/bigdata/ticket/465#comment:2"> * Too many GRS reads</a> */ public void test_add_scan() { final int capacity = 5; final int nscan = 2; final long timeoutNanos = 0L; final MyListener<String, IRef<String>> listener = new MyListener<String, IRef<String>>(); final SynchronizedHardReferenceQueueWithTimeout<String> cache = new SynchronizedHardReferenceQueueWithTimeout<String>( listener, capacity, nscan, timeoutNanos); final HardReferenceQueue<IRef<String>> innerQueue = cache.getQueue(); // final MyListener<String> listener = new MyListener<String>(); // final HardReferenceQueue<String> cache = new HardReferenceQueue<String>( // listener, 5, 2 ); final String ref0 = "0"; final String ref1 = "1"; final String ref2 = "2"; // initial conditions. assertEquals("size",0,cache.size()); assertEquals("tail",0,innerQueue.getTailIndex()); assertEquals("head",0,innerQueue.getHeadIndex()); assertTrue("empty",cache.isEmpty()); assertFalse("full",cache.isFull()); assertSameOrder(new String[]{}, innerQueue); // assertEquals("order",new String[]{},cache.toArray(new String[0])); // append and check post-conditions. assertTrue(cache.add(ref0)); assertEquals("size",1,cache.size()); assertEquals("tail",0,innerQueue.getTailIndex()); assertEquals("head",1,innerQueue.getHeadIndex()); assertFalse("empty",cache.isEmpty()); assertFalse("full",cache.isFull()); assertSameOrder(new String[]{ref0}, innerQueue); // assertEquals("order",new String[]{ref0},cache.toArray(new String[0])); // verify scan finds ref. assertFalse(cache.add(ref0)); // append and check post-conditions. assertTrue(cache.add(ref1)); assertEquals("size",2,cache.size()); assertEquals("tail",0,innerQueue.getTailIndex()); assertEquals("head",2,innerQueue.getHeadIndex()); assertFalse("empty",cache.isEmpty()); assertFalse("full",cache.isFull()); assertSameOrder(new String[]{ref0,ref1}, innerQueue); // assertEquals("order",new String[]{ref0,ref1},cache.toArray(new String[0])); // verify scan finds ref. assertFalse(cache.add(ref1)); // verify scan finds ref. assertFalse(cache.add(ref0)); // append and check post-conditions. assertTrue(cache.add(ref2)); assertEquals("size",3,cache.size()); assertEquals("tail",0,innerQueue.getTailIndex()); assertEquals("head",3,innerQueue.getHeadIndex()); assertFalse("empty",cache.isEmpty()); assertFalse("full",cache.isFull()); assertSameOrder(new String[]{ref0,ref1,ref2}, innerQueue); // assertEquals("order",new String[]{ref0,ref1,ref2},cache.toArray(new String[0])); // verify scan finds ref. assertFalse(cache.add(ref2)); // verify scan finds ref. assertFalse(cache.add(ref1)); /* * Verify scan does NOT find ref. The reference is still in the cache * but the scan does not reach back that far. */ assertTrue(cache.add(ref0)); assertEquals("size",4,cache.size()); assertEquals("tail",0,innerQueue.getTailIndex()); assertEquals("head",4,innerQueue.getHeadIndex()); assertFalse("empty",cache.isEmpty()); assertFalse("full",cache.isFull()); assertSameOrder(new String[]{ref0,ref1,ref2,ref0}, innerQueue); // assertEquals("order",new String[]{ref0,ref1,ref2,ref0},cache.toArray(new String[0])); // verify scan finds ref. assertFalse(cache.add(ref0)); // verify scan finds ref. assertFalse(cache.add(ref2)); /* * Verify scan does NOT find ref. The reference is still in the cache * but the scan does not reach back that far. At this point the cache * is at capacity. */ assertTrue(cache.add(ref1)); assertEquals("size",5,cache.size()); assertEquals("tail",0,innerQueue.getTailIndex()); assertEquals("head",0,innerQueue.getHeadIndex()); assertFalse("empty",cache.isEmpty()); assertTrue("full",cache.isFull()); assertSameOrder(new String[]{ref0,ref1,ref2,ref0,ref1}, innerQueue); // assertEquals("order",new String[]{ref0,ref1,ref2,ref0,ref1},cache.toArray(new String[0])); // verify scan finds ref. assertFalse(cache.add(ref1)); // verify scan finds ref. assertFalse(cache.add(ref0)); /* * Verify scan does NOT find ref. The reference is still in the cache * but the scan does not reach back that far. At this point the cache * overflows and evicts the LRU reference. */ listener.setExpectedRef(ref0); assertTrue(cache.add(ref2)); listener.assertEvicted(); assertEquals("size",5,cache.size()); assertEquals("tail",1,innerQueue.getTailIndex()); assertEquals("head",1,innerQueue.getHeadIndex()); assertFalse("empty",cache.isEmpty()); assertTrue("full",cache.isFull()); assertSameOrder(new String[]{ref1,ref2,ref0,ref1,ref2}, innerQueue); // assertEquals("order",new String[]{ref1,ref2,ref0,ref1,ref2},cache.toArray(new String[0])); // verify scan finds ref. assertFalse(cache.add(ref2)); // verify scan finds ref. assertFalse(cache.add(ref1)); // verify scan does NOT find ref. listener.setExpectedRef(ref1); assertTrue(cache.add(ref0)); listener.assertEvicted(); assertEquals("size",5,cache.size()); assertEquals("tail",2,innerQueue.getTailIndex()); assertEquals("head",2,innerQueue.getHeadIndex()); assertFalse("empty",cache.isEmpty()); assertTrue("full",cache.isFull()); assertSameOrder(new String[]{ref2,ref0,ref1,ref2,ref0}, innerQueue); // assertEquals("order",new String[]{ref2,ref0,ref1,ref2,ref0},cache.toArray(new String[0])); } /** * Helper class for testing correct behavior of the cache and the listener * interface. * * @author <a href="mailto:thompsonbry@users.sourceforge.net">Bryan Thompson</a> * @param <T> */ private class MyListener<G,T extends IRef<G>> implements HardReferenceQueueEvictionListener<T> { /** * Constructor. */ public MyListener() { } /** * Set the next N expected references for eviction notices. You can * only do this when nothing is currently expected. * * @param refs * The expected references. * * @exception IllegalStateExecption * unless there is no current expected reference. */ public void setExpectedRefs(final G[] refs) { if( expectedRef != null ) { throw new IllegalStateException(); } assert refs != null; assert refs.length > 0; for (int i = refs.length - 1; i >= 0; i--) { final G ref = refs[i]; assert ref != null; expectedRefs.push(ref); } setExpectedRef( expectedRefs.pop() ); } private Stack<G> expectedRefs = new Stack<G>(); /** * Set the expected reference for the next eviction notice. The listener * will thrown an exception if there is a cache eviction unless you * first invoke this method. * * @param ref * The expected reference or null to cause the listener to * throw an exception if a reference is evicted. */ public void setExpectedRef(final G ref) { this.expectedRef = ref; this.evicted = false; } private G expectedRef = null; /** * Test for an eviction event. * * @exception AssertionFailedError * if nothing was evicted since the last time an expected * eviction reference was set. */ public void assertEvicted() { if(!evicted) { TestHardReferenceQueue.fail("Expected "+expectedRef+" to have been evicted."); } } private boolean evicted = false; /** * Test for the expected #of eviction notices to date. * * @param expected */ public void assertEvictionCount(int expected) { assertEquals("evictionCount", expected, nevicted); } /** * The #of eviction notices to date. */ public int getEvictionCount() { return nevicted; } private int nevicted = 0; /** * @throws AssertionFailedError * if the evicted reference is not the next expected * eviction reference or if no eviction is expected. */ public void evicted(final IHardReferenceQueue<T> cache, final T ref) { assertNotNull("cache", cache); assertNotNull("ref", ref); if( expectedRef == null && expectedRefs.size() > 0 ) { /* * There is no current expectation, but there is one on the * stack, so we pop it off the stack and continue. * * Note: We pop the expectation off of the stack lazily so that * the unit tests have the opportunity to verify that an * expected reference was evicted. */ setExpectedRef( expectedRefs.pop() ); } if( expectedRef == null ) { fail("Not expecting a cache eviction: ref="+ref); } /* * Note: This is a reference test, but it is against the indirected * reference! */ assertEquals("ref", expectedRef, ref.get()); // assertTrue("ref", expectedRef == ref); // Reset the expectated ref to null. expectedRef = null; // Note that the eviction occurred. evicted = true; nevicted ++; } } }