/************************************************************************* * * * CESeCore: CE Security Core * * * * This software is free software; you can redistribute it and/or * * modify it under the terms of the GNU Lesser General Public * * License as published by the Free Software Foundation; either * * version 2.1 of the License, or any later version. * * * * See terms of license at gnu.org. * * * *************************************************************************/ package org.cesecore.util; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.util.Random; import org.apache.log4j.Level; import org.apache.log4j.Logger; import org.junit.Test; /** * * @version $Id: ConcurrentCacheTest.java 23098 2016-03-30 10:35:11Z samuellb $ */ public final class ConcurrentCacheTest { private static final Logger log = Logger.getLogger(ConcurrentCacheTest.class); // Internal IDs for threads private static final int THREAD_ONE = 101; private static final int THREAD_TWO = 102; private static final int THREAD_RANDOM = 200; private static final int CONCURRENT_OPEN_TIMEOUT = 2000; private static final int NUM_RANDOM_THREADS = 100; @Test public void testSingleThreaded() throws Exception { log.trace(">testSingleThreaded"); final ConcurrentCache cache = new ConcurrentCache<>(); ConcurrentCache.Entry entry; // Create new entry entry = cache.openCacheEntry("A", 1); assertNotNull("openCacheEntry timed out in single-threaded test", entry); assertFalse("isInCache should return false for non-existent entry", entry.isInCache()); try { entry.getValue(); fail("getValue should throw for non-existent entry"); } catch (IllegalStateException e) { // NOPMD } entry.putValue(111); entry.setCacheValidity(60*1000); entry.close(); // Read it and modify the value entry = cache.openCacheEntry("A", 1); assertNotNull("openCacheEntry timed out in single-threaded test", entry); assertTrue("isInCache should return true for existing entry", entry.isInCache()); assertEquals(Integer.valueOf(111), entry.getValue()); entry.putValue(222); assertEquals(Integer.valueOf(222), entry.getValue()); entry.setCacheValidity(60*1000); entry.close(); // Read it again and modify the timeout entry = cache.openCacheEntry("A", 1); assertNotNull("openCacheEntry timed out in single-threaded test", entry); assertTrue("isInCache should return true for existing entry", entry.isInCache()); assertEquals(Integer.valueOf(222), entry.getValue()); entry.setCacheValidity(1); entry.close(); Thread.sleep(5); // Try to read it now. It should have expired and a fresh entry should be returned. entry = cache.openCacheEntry("A", 1); assertFalse("isInCache should return false for expired entry", entry.isInCache()); try { entry.getValue(); fail("getValue should throw for expired entry"); } catch (IllegalStateException e) { // NOPMD } entry.close(); log.trace("testDisabled"); final ConcurrentCache cache = new ConcurrentCache<>(); cache.setEnabled(false); ConcurrentCache.Entry entry; // Create entries. All operations should be "no-ops" for (int i = 0; i < 10; i++) { long timeBefore = System.currentTimeMillis(); entry = cache.openCacheEntry("A", 1); assertTrue("openCacheEntry took too long. It should be a no-op when cache is disabled", System.currentTimeMillis() < timeBefore+10); assertNotNull("openCacheEntry timed out when cache is disabled", entry); assertFalse("isInCache should return false when cache is disabled", entry.isInCache()); try { entry.getValue(); fail("getValue should throw when caching is disabled"); } catch (IllegalStateException e) { // NOPMD } entry.putValue(111); entry.setCacheValidity(60*1000); entry.close(); // This should have been a no-op cache.checkNumberOfEntries(0, 0); } log.trace("testMultiThreaded"); final ConcurrentCache cache = new ConcurrentCache<>(); final Thread thread1 = new Thread(new CacheTestRunner(cache, THREAD_ONE), "CacheTestThread1"); final Thread thread2 = new Thread(new CacheTestRunner(cache, THREAD_TWO), "CacheTestThread2"); thread1.start(); thread2.start(); thread1.join(); thread2.join(); log.trace("testExpiryBehavior"); final ConcurrentCache cache = new ConcurrentCache<>(); cache.setCleanupInterval(60000L); // don't run cleanup during test // Create an entry that will expire final ConcurrentCache.Entry entry1 = cache.openCacheEntry("A", 1000); entry1.putValue(456); entry1.setCacheValidity(1L); entry1.close(); Thread.sleep(5); // We should get an expired entry now. Start updating it but don't finish final ConcurrentCache.Entry entry2 = cache.openCacheEntry("A", 1000); assertFalse("Should be forced to update the cache.", entry2.isInCache()); // technically, it is still in the cache, but ConcurrentCache will pretend it's not, so the caller will update it // Start a new request for the same entry. Should use the old expired entry final long startTime = System.currentTimeMillis(); final ConcurrentCache.Entry entry3 = cache.openCacheEntry("A", 1000); final long stopTime = System.currentTimeMillis(); assertTrue("openCacheEntry should not block.", stopTime - startTime < 500L); assertTrue("Should be able to use the existing cached data, because someone is updating the new value", entry3.isInCache()); assertEquals(Integer.valueOf(456), entry3.getValue()); entry3.close(); // Now write the new value entry2.putValue(768); entry2.setCacheValidity(500); entry2.close(); Thread.sleep(5); // We should see the new entry now final ConcurrentCache.Entry entry4 = cache.openCacheEntry("A", 1000); assertTrue("Entry should still be in cache.", entry4.isInCache()); assertEquals("Entry should have been updated.", Integer.valueOf(768), entry4.getValue()); entry4.close(); log.trace("testRandomMultiThreaded"); // This tests outputs as LOT of debug/trace messages. JUnit even runs out of heap space if those are enabled. Logger.getRootLogger().setLevel(Level.INFO); Logger.getLogger(ConcurrentCache.class).setLevel(Level.INFO); try { final ConcurrentCache cache = new ConcurrentCache<>(); cache.setMaxEntries(20); cache.setCleanupInterval(100); // To stress the system a bit more final Thread[] threads = new Thread[NUM_RANDOM_THREADS]; final CacheTestRunner[] runners = new CacheTestRunner[NUM_RANDOM_THREADS]; for (int i = 0; i < threads.length; i++) { runners[i] = new CacheTestRunner(cache, THREAD_RANDOM); threads[i] = new Thread(runners[i], "CacheTestThread"+i); } try { log.info("Now starting the threads"); for (int i = 0; i < threads.length; i++) { threads[i].start(); } log.info("All threads started"); Thread.sleep(2000); } finally { log.info("Asking threads to stop"); for (int i = 0; i < threads.length; i++) { runners[i].shouldExit = true; } log.info("Waiting for join of first thread (1 sec timeout)"); threads[0].join(1000); log.info("Waiting for other threads"); Thread.sleep(100); for (int i = 1; i < threads.length; i++) { threads[i].join(1); } } long timeout = System.currentTimeMillis() + 2000; // if a thread stops for more than 2 s in cleanup() then that's a problem by itself for (int i = 0; i < threads.length; i++) { if (threads[i].isAlive() && System.currentTimeMillis() < timeout) { threads[i].join(2000); } assertFalse("Thread "+i+" was still alive", threads[i].isAlive()); } } finally { // Preferably, the log level should be restored to the original value, // but neither of the getLevel/getEffectiveLevel methods return the correct value // so we hard-code Level.TRACE here. Logger.getRootLogger().setLevel(Level.TRACE); Logger.getLogger(ConcurrentCache.class).setLevel(Level.TRACE); log.trace("testMaxEntries"); final ConcurrentCache cache = new ConcurrentCache<>(); cache.setMaxEntries(MAXENTRIES); cache.setCleanupInterval(100); ConcurrentCache.Entry entry; try { Logger.getLogger(ConcurrentCache.class).setLevel(Level.INFO); // Create initial entries log.debug("Creating initial entries"); for (int i = 0; i < MAXENTRIES; i++) { entry = cache.openCacheEntry(String.valueOf(i), 1); assertNotNull("openCacheEntry timed out", entry); assertFalse("isInCache should return false for non-existent entry", entry.isInCache()); entry.putValue(i); entry.setCacheValidity(60*1000); entry.close(); } cache.checkNumberOfEntries(MAXENTRIES, MAXENTRIES); // Add some more. Cleanup is guaranteed to run if we overshoot by 50% log.debug("Creating more entries (to overshoot the limit)"); for (int i = MAXENTRIES; i <= OVERSHOOT; i++) { entry = cache.openCacheEntry(String.valueOf(i), 1); assertNotNull("openCacheEntry timed out", entry); assertFalse("isInCache should return false for non-existent entry", entry.isInCache()); entry.putValue(i); entry.setCacheValidity(60*1000); entry.close(); } log.debug("Sleeping to allow for cleanup to run again"); Thread.sleep(100); // Access the cache once more to trigger a cleanup entry = cache.openCacheEntry(String.valueOf("x"), 1); assertNotNull("openCacheEntry timed out", entry); assertFalse("isInCache should return false for non-existent entry", entry.isInCache()); entry.putValue(-123456); entry.setCacheValidity(60*1000); entry.close(); log.debug("Done creating entries"); // Cleanup should have run now cache.checkNumberOfEntries(MIN_ENTRIES_AFTER_CLEANUP, MAXENTRIES-1); } finally { Logger.getLogger(ConcurrentCache.class).setLevel(Level.TRACE); log.trace(" cache; private final static String[] KEYS = {"A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"}; public volatile boolean shouldExit = false; public CacheTestRunner(final ConcurrentCache cache, final int id) { this.cache = cache; this.id = id; } @Override public void run() { ConcurrentCache.Entry entry; try { switch (id) { case THREAD_ONE: // 0 ms entry = cache.openCacheEntry("A", 1); assertFalse("non-existent entry should not be in cache", entry.isInCache()); Thread.sleep(200); entry.putValue(123); entry.setCacheValidity(200); // = valid to ~400 ms entry.close(); break; case THREAD_TWO: Thread.sleep(100); // 100 ms entry = cache.openCacheEntry("A", CONCURRENT_OPEN_TIMEOUT); // 200 ms assertTrue("existing entry should be in cache", entry.isInCache()); assertEquals("wrong value of cache entry", Integer.valueOf(123), entry.getValue()); entry.close(); Thread.sleep(300); // 500 ms entry = cache.openCacheEntry("A", CONCURRENT_OPEN_TIMEOUT); assertFalse("entry should have expired", entry.isInCache()); entry.close(); break; case THREAD_RANDOM: final Random random = new Random(id); final Integer value = Integer.valueOf(id); while (!shouldExit) { final String key = KEYS[random.nextInt(KEYS.length)]; entry = cache.openCacheEntry(key, CONCURRENT_OPEN_TIMEOUT); if (entry.isInCache()) { assertNotNull("got null value in cache entry", entry.getValue()); if (random.nextInt(10000) == 123) { entry.setCacheValidity(2000+random.nextInt(100)); } entry.close(); } else { Thread.sleep(1+random.nextInt(50)); entry.putValue(value); entry.setCacheValidity(2000+random.nextInt(100)); entry.close(); } } break; default: throw new IllegalStateException("invalid test thread id"); } } catch (InterruptedException ie) { throw new RuntimeException(ie); } } } }