From a3d6b02174ae8f8baf4714106a81f8a1867b4a7b Mon Sep 17 00:00:00 2001 From: John DeRegnaucourt Date: Sun, 23 Jun 2024 14:21:25 -0400 Subject: [PATCH] Adding two LRUCache-ing strategies - locking and threaded. --- .../java/com/cedarsoftware/util/LRUCache.java | 246 ++++--------- .../util/cache/LockingLRUCacheStrategy.java | 295 ++++++++++++++++ .../ThreadedLRUCacheStrategy.java} | 76 +++-- .../util/{ => cache}/LRUCacheTest.java | 322 ++++++++++-------- 4 files changed, 568 insertions(+), 371 deletions(-) create mode 100644 src/main/java/com/cedarsoftware/util/cache/LockingLRUCacheStrategy.java rename src/main/java/com/cedarsoftware/util/{LRUCache2.java => cache/ThreadedLRUCacheStrategy.java} (82%) rename src/test/java/com/cedarsoftware/util/{ => cache}/LRUCacheTest.java (56%) diff --git a/src/main/java/com/cedarsoftware/util/LRUCache.java b/src/main/java/com/cedarsoftware/util/LRUCache.java index 621f4e33..9e326d61 100644 --- a/src/main/java/com/cedarsoftware/util/LRUCache.java +++ b/src/main/java/com/cedarsoftware/util/LRUCache.java @@ -1,242 +1,118 @@ package com.cedarsoftware.util; -import java.util.AbstractMap; -import java.util.LinkedHashMap; +import java.util.Collection; import java.util.Map; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; - -/** - * This class provides a thread-safe Least Recently Used (LRU) cache API that will evict the least recently used items, - * once a threshold is met. It implements the Map interface for convenience. - *

- * LRUCache supports null for key or value. - *

- * @author John DeRegnaucourt (jdereg@gmail.com) - *
- * Copyright (c) Cedar Software LLC - *

- * 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 - *

- * License - *

- * 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. - */ -public class LRUCache extends AbstractMap implements Map { - private static final Object NULL_ITEM = new Object(); // Sentinel value for null keys and values - private final int capacity; - private final ConcurrentHashMap> cache; - private final Node head; - private final Node tail; - private final Lock lock = new ReentrantLock(); - - private static class Node { - K key; - V value; - Node prev; - Node next; - - Node(K key, V value) { - this.key = key; - this.value = value; - } - } +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.ScheduledExecutorService; - public LRUCache(int capacity) { - this.capacity = capacity; - this.cache = new ConcurrentHashMap<>(capacity); - this.head = new Node<>(null, null); - this.tail = new Node<>(null, null); - head.next = tail; - tail.prev = head; - } +import com.cedarsoftware.util.cache.LockingLRUCacheStrategy; +import com.cedarsoftware.util.cache.ThreadedLRUCacheStrategy; - private void moveToHead(Node node) { - removeNode(node); - addToHead(node); - } +public class LRUCache implements Map { + private final Map strategy; - private void addToHead(Node node) { - node.next = head.next; - node.next.prev = node; - head.next = node; - node.prev = head; + public enum StrategyType { + THREADED, + LOCKING } - private void removeNode(Node node) { - node.prev.next = node.next; - node.next.prev = node.prev; + public LRUCache(int capacity, StrategyType strategyType) { + this(capacity, strategyType, 10, null, null); } - private Node removeTail() { - Node node = tail.prev; - removeNode(node); - return node; + public LRUCache(int capacity, StrategyType strategyType, int cleanupDelayMillis, ScheduledExecutorService scheduler, ForkJoinPool cleanupPool) { + switch (strategyType) { + case THREADED: + this.strategy = new ThreadedLRUCacheStrategy<>(capacity, cleanupDelayMillis, scheduler, cleanupPool); + break; + case LOCKING: + this.strategy = new LockingLRUCacheStrategy<>(capacity); + break; + default: + throw new IllegalArgumentException("Unknown strategy type"); + } } @Override public V get(Object key) { - Object cacheKey = toCacheItem(key); - Node node = cache.get(cacheKey); - if (node == null) { - return null; - } - if (lock.tryLock()) { - try { - moveToHead(node); - } finally { - lock.unlock(); - } - } - return fromCacheItem(node.value); + return strategy.get(key); } - @SuppressWarnings("unchecked") @Override public V put(K key, V value) { - Object cacheKey = toCacheItem(key); - Object cacheValue = toCacheItem(value); - lock.lock(); - try { - Node node = cache.get(cacheKey); - if (node != null) { - node.value = (V)cacheValue; - moveToHead(node); - return fromCacheItem(node.value); - } else { - Node newNode = new Node<>(key, (V)cacheValue); - cache.put(cacheKey, newNode); - addToHead(newNode); - if (cache.size() > capacity) { - Node tail = removeTail(); - cache.remove(toCacheItem(tail.key)); - } - return null; - } - } finally { - lock.unlock(); - } + return strategy.put(key, value); + } + + @Override + public void putAll(Map m) { + strategy.putAll(m); } @Override public V remove(Object key) { - Object cacheKey = toCacheItem(key); - lock.lock(); - try { - Node node = cache.remove(cacheKey); - if (node != null) { - removeNode(node); - return fromCacheItem(node.value); - } - return null; - } finally { - lock.unlock(); - } + return strategy.remove((K)key); } @Override public void clear() { - lock.lock(); - try { - head.next = tail; - tail.prev = head; - cache.clear(); - } finally { - lock.unlock(); - } + strategy.clear(); } @Override public int size() { - return cache.size(); + return strategy.size(); + } + + @Override + public boolean isEmpty() { + return strategy.isEmpty(); } @Override public boolean containsKey(Object key) { - return cache.containsKey(toCacheItem(key)); + return strategy.containsKey((K)key); } @Override public boolean containsValue(Object value) { - Object cacheValue = toCacheItem(value); - lock.lock(); - try { - for (Node node = head.next; node != tail; node = node.next) { - if (node.value.equals(cacheValue)) { - return true; - } - } - return false; - } finally { - lock.unlock(); - } + return strategy.containsValue((V)value); } @Override public Set> entrySet() { - lock.lock(); - try { - Map map = new LinkedHashMap<>(); - for (Node node = head.next; node != tail; node = node.next) { - map.put(node.key, fromCacheItem(node.value)); - } - return map.entrySet(); - } finally { - lock.unlock(); - } + return strategy.entrySet(); + } + + @Override + public Set keySet() { + return strategy.keySet(); + } + + @Override + public Collection values() { + return strategy.values(); } - @SuppressWarnings("unchecked") @Override public String toString() { - lock.lock(); - try { - StringBuilder sb = new StringBuilder(); - sb.append("{"); - for (Node node = head.next; node != tail; node = node.next) { - sb.append((K) fromCacheItem(node.key)).append("=").append((V) fromCacheItem(node.value)).append(", "); - } - if (sb.length() > 1) { - sb.setLength(sb.length() - 2); // Remove trailing comma and space - } - sb.append("}"); - return sb.toString(); - } finally { - lock.unlock(); - } + return strategy.toString(); } @Override public int hashCode() { - lock.lock(); - try { - int hashCode = 1; - for (Node node = head.next; node != tail; node = node.next) { - Object key = fromCacheItem(node.key); - Object value = fromCacheItem(node.value); - hashCode = 31 * hashCode + (key == null ? 0 : key.hashCode()); - hashCode = 31 * hashCode + (value == null ? 0 : value.hashCode()); - } - return hashCode; - } finally { - lock.unlock(); - } + return strategy.hashCode(); } - private Object toCacheItem(Object item) { - return item == null ? NULL_ITEM : item; + @Override + public boolean equals(Object obj) { + return strategy.equals(obj); } - @SuppressWarnings("unchecked") - private T fromCacheItem(Object cacheItem) { - return cacheItem == NULL_ITEM ? null : (T) cacheItem; + public void shutdown() { + if (strategy instanceof ThreadedLRUCacheStrategy) { + ((ThreadedLRUCacheStrategy) strategy).shutdown(); + } } } diff --git a/src/main/java/com/cedarsoftware/util/cache/LockingLRUCacheStrategy.java b/src/main/java/com/cedarsoftware/util/cache/LockingLRUCacheStrategy.java new file mode 100644 index 00000000..8f61c30d --- /dev/null +++ b/src/main/java/com/cedarsoftware/util/cache/LockingLRUCacheStrategy.java @@ -0,0 +1,295 @@ +package com.cedarsoftware.util.cache; + +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * This class provides a thread-safe Least Recently Used (LRU) cache API that will evict the least recently used items, + * once a threshold is met. It implements the Map interface for convenience. + *

+ * LRUCache supports null for key or value. + *

+ * @author John DeRegnaucourt (jdereg@gmail.com) + *
+ * Copyright (c) Cedar Software LLC + *

+ * 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 + *

+ * License + *

+ * 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. + */ +public class LockingLRUCacheStrategy implements Map { + private static final Object NULL_ITEM = new Object(); // Sentinel value for null keys and values + private final int capacity; + private final ConcurrentHashMap> cache; + private final Node head; + private final Node tail; + private final Lock lock = new ReentrantLock(); + + private static class Node { + K key; + V value; + Node prev; + Node next; + + Node(K key, V value) { + this.key = key; + this.value = value; + } + } + + public LockingLRUCacheStrategy(int capacity) { + this.capacity = capacity; + this.cache = new ConcurrentHashMap<>(capacity); + this.head = new Node<>(null, null); + this.tail = new Node<>(null, null); + head.next = tail; + tail.prev = head; + } + + private void moveToHead(Node node) { + removeNode(node); + addToHead(node); + } + + private void addToHead(Node node) { + node.next = head.next; + node.next.prev = node; + head.next = node; + node.prev = head; + } + + private void removeNode(Node node) { + node.prev.next = node.next; + node.next.prev = node.prev; + } + + private Node removeTail() { + Node node = tail.prev; + removeNode(node); + return node; + } + + @Override + public V get(Object key) { + Object cacheKey = toCacheItem(key); + Node node = cache.get(cacheKey); + if (node == null) { + return null; + } + if (lock.tryLock()) { + try { + moveToHead(node); + } finally { + lock.unlock(); + } + } + return fromCacheItem(node.value); + } + + @SuppressWarnings("unchecked") + @Override + public V put(K key, V value) { + Object cacheKey = toCacheItem(key); + Object cacheValue = toCacheItem(value); + lock.lock(); + try { + Node node = cache.get(cacheKey); + if (node != null) { + node.value = (V) cacheValue; + moveToHead(node); + return fromCacheItem(node.value); + } else { + Node newNode = new Node<>(key, (V) cacheValue); + cache.put(cacheKey, newNode); + addToHead(newNode); + if (cache.size() > capacity) { + Node tail = removeTail(); + cache.remove(toCacheItem(tail.key)); + } + return null; + } + } finally { + lock.unlock(); + } + } + + @Override + public void putAll(Map m) { + lock.lock(); + try { + for (Map.Entry entry : m.entrySet()) { + put(entry.getKey(), entry.getValue()); + } + } finally { + lock.unlock(); + } + } + + @Override + public V remove(Object key) { + Object cacheKey = toCacheItem(key); + lock.lock(); + try { + Node node = cache.remove(cacheKey); + if (node != null) { + removeNode(node); + return fromCacheItem(node.value); + } + return null; + } finally { + lock.unlock(); + } + } + + @Override + public void clear() { + lock.lock(); + try { + head.next = tail; + tail.prev = head; + cache.clear(); + } finally { + lock.unlock(); + } + } + + @Override + public int size() { + return cache.size(); + } + + @Override + public boolean isEmpty() { + return size() == 0; + } + + @Override + public boolean containsKey(Object key) { + return cache.containsKey(toCacheItem(key)); + } + + @Override + public boolean containsValue(Object value) { + Object cacheValue = toCacheItem(value); + lock.lock(); + try { + for (Node node = head.next; node != tail; node = node.next) { + if (node.value.equals(cacheValue)) { + return true; + } + } + return false; + } finally { + lock.unlock(); + } + } + + @Override + public Set> entrySet() { + lock.lock(); + try { + Map map = new LinkedHashMap<>(); + for (Node node = head.next; node != tail; node = node.next) { + map.put(node.key, fromCacheItem(node.value)); + } + return map.entrySet(); + } finally { + lock.unlock(); + } + } + + @Override + public Set keySet() { + lock.lock(); + try { + Map map = new LinkedHashMap<>(); + for (Node node = head.next; node != tail; node = node.next) { + map.put(node.key, fromCacheItem(node.value)); + } + return map.keySet(); + } finally { + lock.unlock(); + } + } + + @Override + public Collection values() { + lock.lock(); + try { + Map map = new LinkedHashMap<>(); + for (Node node = head.next; node != tail; node = node.next) { + map.put(node.key, fromCacheItem(node.value)); + } + return map.values(); + } finally { + lock.unlock(); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Map)) return false; + Map other = (Map) o; + return entrySet().equals(other.entrySet()); + } + + @SuppressWarnings("unchecked") + @Override + public String toString() { + lock.lock(); + try { + StringBuilder sb = new StringBuilder(); + sb.append("{"); + for (Node node = head.next; node != tail; node = node.next) { + sb.append((K) fromCacheItem(node.key)).append("=").append((V) fromCacheItem(node.value)).append(", "); + } + if (sb.length() > 1) { + sb.setLength(sb.length() - 2); // Remove trailing comma and space + } + sb.append("}"); + return sb.toString(); + } finally { + lock.unlock(); + } + } + + @Override + public int hashCode() { + lock.lock(); + try { + int hashCode = 1; + for (Node node = head.next; node != tail; node = node.next) { + Object key = fromCacheItem(node.key); + Object value = fromCacheItem(node.value); + hashCode = 31 * hashCode + (key == null ? 0 : key.hashCode()); + hashCode = 31 * hashCode + (value == null ? 0 : value.hashCode()); + } + return hashCode; + } finally { + lock.unlock(); + } + } + + private Object toCacheItem(Object item) { + return item == null ? NULL_ITEM : item; + } + + @SuppressWarnings("unchecked") + private T fromCacheItem(Object cacheItem) { + return cacheItem == NULL_ITEM ? null : (T) cacheItem; + } +} \ No newline at end of file diff --git a/src/main/java/com/cedarsoftware/util/LRUCache2.java b/src/main/java/com/cedarsoftware/util/cache/ThreadedLRUCacheStrategy.java similarity index 82% rename from src/main/java/com/cedarsoftware/util/LRUCache2.java rename to src/main/java/com/cedarsoftware/util/cache/ThreadedLRUCacheStrategy.java index aae25336..0ceb8540 100644 --- a/src/main/java/com/cedarsoftware/util/LRUCache2.java +++ b/src/main/java/com/cedarsoftware/util/cache/ThreadedLRUCacheStrategy.java @@ -1,4 +1,4 @@ -package com.cedarsoftware.util; +package com.cedarsoftware.util.cache; import java.util.AbstractMap; import java.util.ArrayList; @@ -44,15 +44,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -public class LRUCache2 extends AbstractMap implements Map { - private static final ScheduledExecutorService defaultScheduler = Executors.newScheduledThreadPool(1); +public class ThreadedLRUCacheStrategy implements Map { private static final Object NULL_ITEM = new Object(); // Sentinel value for null keys and values private final long cleanupDelayMillis; private final int capacity; private final ConcurrentMap> cache; private final AtomicBoolean cleanupScheduled = new AtomicBoolean(false); private final ScheduledExecutorService scheduler; - private final ExecutorService cleanupExecutor; + private final ForkJoinPool cleanupPool; private boolean isDefaultScheduler; private static class Node { @@ -71,29 +70,6 @@ void updateTimestamp() { } } - /** - * Create a LRUCache with the maximum capacity of 'capacity.' Note, the LRUCache could temporarily exceed the - * capacity, however, it will quickly reduce to that amount. This time is configurable and defaults to 10ms. - * @param capacity int maximum size for the LRU cache. - */ - public LRUCache2(int capacity) { - this(capacity, 10, defaultScheduler, ForkJoinPool.commonPool()); - isDefaultScheduler = true; - } - - /** - * Create a LRUCache with the maximum capacity of 'capacity.' Note, the LRUCache could temporarily exceed the - * capacity, however, it will quickly reduce to that amount. This time is configurable via the cleanupDelay - * parameter. - * @param capacity int maximum size for the LRU cache. - * @param cleanupDelayMillis int milliseconds before scheduling a cleanup (reduction to capacity if the cache currently - * exceeds it). - */ - public LRUCache2(int capacity, int cleanupDelayMillis) { - this(capacity, cleanupDelayMillis, defaultScheduler, ForkJoinPool.commonPool()); - isDefaultScheduler = true; - } - /** * Create a LRUCache with the maximum capacity of 'capacity.' Note, the LRUCache could temporarily exceed the * capacity, however, it will quickly reduce to that amount. This time is configurable via the cleanupDelay @@ -102,15 +78,27 @@ public LRUCache2(int capacity, int cleanupDelayMillis) { * @param cleanupDelayMillis int milliseconds before scheduling a cleanup (reduction to capacity if the cache currently * exceeds it). * @param scheduler ScheduledExecutorService for scheduling cleanup tasks. - * @param cleanupExecutor ExecutorService for executing cleanup tasks. + * @param cleanupPool ForkJoinPool for executing cleanup tasks. */ - public LRUCache2(int capacity, int cleanupDelayMillis, ScheduledExecutorService scheduler, ExecutorService cleanupExecutor) { + public ThreadedLRUCacheStrategy(int capacity, int cleanupDelayMillis, ScheduledExecutorService scheduler, ForkJoinPool cleanupPool) { + isDefaultScheduler = false; + if (scheduler == null) { + this.scheduler = Executors.newScheduledThreadPool(1); + isDefaultScheduler = true; + } else { + this.scheduler = scheduler; + isDefaultScheduler = false; + } + + if (cleanupPool == null) { + this.cleanupPool = ForkJoinPool.commonPool(); + } else { + this.cleanupPool = cleanupPool; + } + this.capacity = capacity; this.cache = new ConcurrentHashMap<>(capacity); this.cleanupDelayMillis = cleanupDelayMillis; - this.scheduler = scheduler; - this.cleanupExecutor = cleanupExecutor; - isDefaultScheduler = false; } @SuppressWarnings("unchecked") @@ -158,6 +146,18 @@ public V put(K key, V value) { return null; } + @Override + public void putAll(Map m) { + for (Map.Entry entry : m.entrySet()) { + put(entry.getKey(), entry.getValue()); + } + } + + @Override + public boolean isEmpty() { + return cache.isEmpty(); + } + @Override public V remove(Object key) { Object cacheKey = toCacheItem(key); @@ -259,10 +259,11 @@ public String toString() { // Schedule a delayed cleanup private void scheduleCleanup() { if (cleanupScheduled.compareAndSet(false, true)) { - scheduler.schedule(() -> cleanupExecutor.execute(this::cleanup), cleanupDelayMillis, TimeUnit.MILLISECONDS); + scheduler.schedule(() -> ForkJoinPool.commonPool().execute(this::cleanup), cleanupDelayMillis, TimeUnit.MILLISECONDS); } } + // Converts a key or value to a cache-compatible item private Object toCacheItem(Object item) { return item == null ? NULL_ITEM : item; @@ -278,8 +279,13 @@ private T fromCacheItem(Object cacheItem) { * Shut down the scheduler if it is the default one. */ public void shutdown() { - if (isDefaultScheduler) { - scheduler.shutdown(); + scheduler.shutdown(); + try { + if (!scheduler.awaitTermination(1, TimeUnit.SECONDS)) { + scheduler.shutdownNow(); + } + } catch (InterruptedException e) { + scheduler.shutdownNow(); } } } \ No newline at end of file diff --git a/src/test/java/com/cedarsoftware/util/LRUCacheTest.java b/src/test/java/com/cedarsoftware/util/cache/LRUCacheTest.java similarity index 56% rename from src/test/java/com/cedarsoftware/util/LRUCacheTest.java rename to src/test/java/com/cedarsoftware/util/cache/LRUCacheTest.java index 7bd69081..d0f0a5be 100644 --- a/src/test/java/com/cedarsoftware/util/LRUCacheTest.java +++ b/src/test/java/com/cedarsoftware/util/cache/LRUCacheTest.java @@ -1,6 +1,8 @@ -package com.cedarsoftware.util; +package com.cedarsoftware.util.cache; import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Collection; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; @@ -9,8 +11,9 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; +import com.cedarsoftware.util.LRUCache; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -18,34 +21,25 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; -/** - * @author John DeRegnaucourt (jdereg@gmail.com) - *
- * Copyright (c) Cedar Software LLC - *

- * 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 - *

- * License - *

- * 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. - */ public class LRUCacheTest { private LRUCache lruCache; - @BeforeEach - void setUp() { - lruCache = new LRUCache<>(3); + static Collection strategies() { + return Arrays.asList( + LRUCache.StrategyType.LOCKING, + LRUCache.StrategyType.THREADED + ); } - @Test - void testPutAndGet() { + void setUp(LRUCache.StrategyType strategyType) { + lruCache = new LRUCache<>(3, strategyType); + } + + @ParameterizedTest + @MethodSource("strategies") + void testPutAndGet(LRUCache.StrategyType strategy) { + setUp(strategy); lruCache.put(1, "A"); lruCache.put(2, "B"); lruCache.put(3, "C"); @@ -55,43 +49,47 @@ void testPutAndGet() { assertEquals("C", lruCache.get(3)); } - @Test - void testEvictionPolicy() { + @ParameterizedTest + @MethodSource("strategies") + void testEvictionPolicy(LRUCache.StrategyType strategy) { + setUp(strategy); lruCache.put(1, "A"); lruCache.put(2, "B"); lruCache.put(3, "C"); lruCache.get(1); lruCache.put(4, "D"); - // Wait for the background cleanup thread to perform the eviction long startTime = System.currentTimeMillis(); - long timeout = 5000; // 5 seconds timeout + long timeout = 5000; while (System.currentTimeMillis() - startTime < timeout) { if (!lruCache.containsKey(2) && lruCache.containsKey(1) && lruCache.containsKey(4)) { break; } try { - Thread.sleep(100); // Check every 100ms + Thread.sleep(100); } catch (InterruptedException ignored) { } } - // Assert the expected cache state assertNull(lruCache.get(2), "Entry for key 2 should be evicted"); assertEquals("A", lruCache.get(1), "Entry for key 1 should still be present"); assertEquals("D", lruCache.get(4), "Entry for key 4 should be present"); } - - @Test - void testSize() { + + @ParameterizedTest + @MethodSource("strategies") + void testSize(LRUCache.StrategyType strategy) { + setUp(strategy); lruCache.put(1, "A"); lruCache.put(2, "B"); assertEquals(2, lruCache.size()); } - @Test - void testIsEmpty() { + @ParameterizedTest + @MethodSource("strategies") + void testIsEmpty(LRUCache.StrategyType strategy) { + setUp(strategy); assertTrue(lruCache.isEmpty()); lruCache.put(1, "A"); @@ -99,32 +97,40 @@ void testIsEmpty() { assertFalse(lruCache.isEmpty()); } - @Test - void testRemove() { + @ParameterizedTest + @MethodSource("strategies") + void testRemove(LRUCache.StrategyType strategy) { + setUp(strategy); lruCache.put(1, "A"); lruCache.remove(1); assertNull(lruCache.get(1)); } - @Test - void testContainsKey() { + @ParameterizedTest + @MethodSource("strategies") + void testContainsKey(LRUCache.StrategyType strategy) { + setUp(strategy); lruCache.put(1, "A"); assertTrue(lruCache.containsKey(1)); assertFalse(lruCache.containsKey(2)); } - @Test - void testContainsValue() { + @ParameterizedTest + @MethodSource("strategies") + void testContainsValue(LRUCache.StrategyType strategy) { + setUp(strategy); lruCache.put(1, "A"); assertTrue(lruCache.containsValue("A")); assertFalse(lruCache.containsValue("B")); } - @Test - void testKeySet() { + @ParameterizedTest + @MethodSource("strategies") + void testKeySet(LRUCache.StrategyType strategy) { + setUp(strategy); lruCache.put(1, "A"); lruCache.put(2, "B"); @@ -132,8 +138,10 @@ void testKeySet() { assertTrue(lruCache.keySet().contains(2)); } - @Test - void testValues() { + @ParameterizedTest + @MethodSource("strategies") + void testValues(LRUCache.StrategyType strategy) { + setUp(strategy); lruCache.put(1, "A"); lruCache.put(2, "B"); @@ -141,8 +149,10 @@ void testValues() { assertTrue(lruCache.values().contains("B")); } - @Test - void testClear() { + @ParameterizedTest + @MethodSource("strategies") + void testClear(LRUCache.StrategyType strategy) { + setUp(strategy); lruCache.put(1, "A"); lruCache.put(2, "B"); lruCache.clear(); @@ -150,8 +160,10 @@ void testClear() { assertTrue(lruCache.isEmpty()); } - @Test - void testPutAll() { + @ParameterizedTest + @MethodSource("strategies") + void testPutAll(LRUCache.StrategyType strategy) { + setUp(strategy); Map map = new LinkedHashMap<>(); map.put(1, "A"); map.put(2, "B"); @@ -161,28 +173,31 @@ void testPutAll() { assertEquals("B", lruCache.get(2)); } - @Test - void testEntrySet() { + @ParameterizedTest + @MethodSource("strategies") + void testEntrySet(LRUCache.StrategyType strategy) { + setUp(strategy); lruCache.put(1, "A"); lruCache.put(2, "B"); assertEquals(2, lruCache.entrySet().size()); } - @Test - void testPutIfAbsent() { + @ParameterizedTest + @MethodSource("strategies") + void testPutIfAbsent(LRUCache.StrategyType strategy) { + setUp(strategy); lruCache.putIfAbsent(1, "A"); lruCache.putIfAbsent(1, "B"); assertEquals("A", lruCache.get(1)); } - @Test - void testSmallSizes() - { - // Testing with different sizes + @ParameterizedTest + @MethodSource("strategies") + void testSmallSizes(LRUCache.StrategyType strategy) { for (int capacity : new int[]{1, 3, 5, 10}) { - LRUCache cache = new LRUCache<>(capacity); + LRUCache cache = new LRUCache<>(capacity, strategy); for (int i = 0; i < capacity; i++) { cache.put(i, "Value" + i); } @@ -193,17 +208,18 @@ void testSmallSizes() cache.remove(i); } - assert cache.isEmpty(); + assertTrue(cache.isEmpty()); cache.clear(); } } - - @Test - void testConcurrency() throws InterruptedException { + + @ParameterizedTest + @MethodSource("strategies") + void testConcurrency(LRUCache.StrategyType strategy) throws InterruptedException { + setUp(strategy); ExecutorService service = Executors.newFixedThreadPool(3); - lruCache = new LRUCache<>(10000); + lruCache = new LRUCache<>(10000, strategy); - // Perform a mix of put and get operations from multiple threads int max = 10000; int attempts = 0; Random random = new SecureRandom(); @@ -214,15 +230,11 @@ void testConcurrency() throws InterruptedException { service.submit(() -> lruCache.put(key, value)); service.submit(() -> lruCache.get(key)); service.submit(() -> lruCache.size()); - service.submit(() -> { - lruCache.keySet().remove(random.nextInt(max)); - }); - service.submit(() -> { - lruCache.values().remove("V" + random.nextInt(max)); - }); + service.submit(() -> lruCache.keySet().remove(random.nextInt(max))); + service.submit(() -> lruCache.values().remove("V" + random.nextInt(max))); final int attemptsCopy = attempts; service.submit(() -> { - Iterator i = lruCache.entrySet().iterator(); + Iterator> i = lruCache.entrySet().iterator(); int walk = random.nextInt(attemptsCopy); while (i.hasNext() && walk-- > 0) { i.next(); @@ -240,45 +252,45 @@ void testConcurrency() throws InterruptedException { assertTrue(service.awaitTermination(1, TimeUnit.MINUTES)); } - @Test - public void testConcurrency2() throws InterruptedException { + @ParameterizedTest + @MethodSource("strategies") + void testConcurrency2(LRUCache.StrategyType strategy) throws InterruptedException { + setUp(strategy); int initialEntries = 100; - lruCache = new LRUCache<>(initialEntries); + lruCache = new LRUCache<>(initialEntries, strategy); ExecutorService executor = Executors.newFixedThreadPool(10); - // Add initial entries for (int i = 0; i < initialEntries; i++) { lruCache.put(i, "true"); } SecureRandom random = new SecureRandom(); - // Perform concurrent operations for (int i = 0; i < 100000; i++) { final int key = random.nextInt(100); executor.submit(() -> { - lruCache.put(key, "true"); // Add - lruCache.remove(key); // Remove - lruCache.put(key, "false"); // Update + lruCache.put(key, "true"); + lruCache.remove(key); + lruCache.put(key, "false"); }); } executor.shutdown(); assertTrue(executor.awaitTermination(1, TimeUnit.MINUTES)); - // Check some values to ensure correctness for (int i = 0; i < initialEntries; i++) { final int key = i; assertTrue(lruCache.containsKey(key)); } - assert lruCache.size() == 100; assertEquals(initialEntries, lruCache.size()); } - @Test - void testEquals() { - LRUCache cache1 = new LRUCache<>(3); - LRUCache cache2 = new LRUCache<>(3); + @ParameterizedTest + @MethodSource("strategies") + void testEquals(LRUCache.StrategyType strategy) { + setUp(strategy); + LRUCache cache1 = new LRUCache<>(3, strategy); + LRUCache cache2 = new LRUCache<>(3, strategy); cache1.put(1, "A"); cache1.put(2, "B"); @@ -300,10 +312,12 @@ void testEquals() { assertTrue(cache1.equals(cache1)); } - @Test - void testHashCode() { - LRUCache cache1 = new LRUCache<>(3); - LRUCache cache2 = new LRUCache<>(3); + @ParameterizedTest + @MethodSource("strategies") + void testHashCode(LRUCache.StrategyType strategy) { + setUp(strategy); + LRUCache cache1 = new LRUCache<>(3, strategy); + LRUCache cache2 = new LRUCache<>(3, strategy); cache1.put(1, "A"); cache1.put(2, "B"); @@ -319,23 +333,27 @@ void testHashCode() { assertNotEquals(cache1.hashCode(), cache2.hashCode()); } - @Test - void testToString() { + @ParameterizedTest + @MethodSource("strategies") + void testToString(LRUCache.StrategyType strategy) { + setUp(strategy); lruCache.put(1, "A"); lruCache.put(2, "B"); lruCache.put(3, "C"); - assert lruCache.toString().contains("1=A"); - assert lruCache.toString().contains("2=B"); - assert lruCache.toString().contains("3=C"); + assertTrue(lruCache.toString().contains("1=A")); + assertTrue(lruCache.toString().contains("2=B")); + assertTrue(lruCache.toString().contains("3=C")); - Map cache = new LRUCache(100); - assert cache.toString().equals("{}"); - assert cache.size() == 0; + Map cache = new LRUCache<>(100, strategy); + assertEquals("{}", cache.toString()); + assertEquals(0, cache.size()); } - @Test - void testFullCycle() { + @ParameterizedTest + @MethodSource("strategies") + void testFullCycle(LRUCache.StrategyType strategy) { + setUp(strategy); lruCache.put(1, "A"); lruCache.put(2, "B"); lruCache.put(3, "C"); @@ -344,7 +362,7 @@ void testFullCycle() { lruCache.put(6, "F"); long startTime = System.currentTimeMillis(); - long timeout = 5000; // 5 seconds timeout + long timeout = 5000; while (System.currentTimeMillis() - startTime < timeout) { if (lruCache.size() == 3 && lruCache.containsKey(4) && @@ -356,7 +374,7 @@ void testFullCycle() { break; } try { - Thread.sleep(100); // Check every 100ms + Thread.sleep(100); } catch (InterruptedException ignored) { } } @@ -374,45 +392,43 @@ void testFullCycle() { lruCache.remove(4); assertEquals(0, lruCache.size(), "Cache should be empty after removing all elements"); } - - @Test - void testCacheWhenEmpty() { - // The cache is initially empty, so any get operation should return null + + @ParameterizedTest + @MethodSource("strategies") + void testCacheWhenEmpty(LRUCache.StrategyType strategy) { + setUp(strategy); assertNull(lruCache.get(1)); } - @Test - void testCacheClear() { - // Add elements to the cache + @ParameterizedTest + @MethodSource("strategies") + void testCacheClear(LRUCache.StrategyType strategy) { + setUp(strategy); lruCache.put(1, "A"); lruCache.put(2, "B"); - - // Clear the cache lruCache.clear(); - // The cache should be empty, so any get operation should return null assertNull(lruCache.get(1)); assertNull(lruCache.get(2)); } - @Test - void testCacheBlast() { - // Jam 10M items to the cache - lruCache = new LRUCache<>(1000); + @ParameterizedTest + @MethodSource("strategies") + void testCacheBlast(LRUCache.StrategyType strategy) { + lruCache = new LRUCache<>(1000, strategy); for (int i = 0; i < 10000000; i++) { lruCache.put(i, "" + i); } - // Wait until the cache size stabilizes to 1000 int expectedSize = 1000; long startTime = System.currentTimeMillis(); - long timeout = 10000; // wait up to 10 seconds (will never take this long) + long timeout = 10000; while (System.currentTimeMillis() - startTime < timeout) { if (lruCache.size() <= expectedSize) { break; } try { - Thread.sleep(100); // Check every 100ms + Thread.sleep(100); System.out.println("Cache size: " + lruCache.size()); } catch (InterruptedException ignored) { } @@ -421,51 +437,55 @@ void testCacheBlast() { assertEquals(1000, lruCache.size()); } - @Test - void testNullValue() - { - lruCache = new LRUCache<>(100, 1); + @ParameterizedTest + @MethodSource("strategies") + void testNullValue(LRUCache.StrategyType strategy) { + setUp(strategy); + lruCache = new LRUCache<>(100, strategy); lruCache.put(1, null); - assert lruCache.containsKey(1); - assert lruCache.containsValue(null); - assert lruCache.toString().contains("1=null"); - assert lruCache.hashCode() != 0; + assertTrue(lruCache.containsKey(1)); + assertTrue(lruCache.containsValue(null)); + assertTrue(lruCache.toString().contains("1=null")); + assertNotEquals(0, lruCache.hashCode()); } - @Test - void testNullKey() - { - lruCache = new LRUCache<>(100, 1); + @ParameterizedTest + @MethodSource("strategies") + void testNullKey(LRUCache.StrategyType strategy) { + setUp(strategy); + lruCache = new LRUCache<>(100, strategy); lruCache.put(null, "true"); - assert lruCache.containsKey(null); - assert lruCache.containsValue("true"); - assert lruCache.toString().contains("null=true"); - assert lruCache.hashCode() != 0; + assertTrue(lruCache.containsKey(null)); + assertTrue(lruCache.containsValue("true")); + assertTrue(lruCache.toString().contains("null=true")); + assertNotEquals(0, lruCache.hashCode()); } - @Test - void testNullKeyValue() - { - lruCache = new LRUCache<>(100, 1); + @ParameterizedTest + @MethodSource("strategies") + void testNullKeyValue(LRUCache.StrategyType strategy) { + setUp(strategy); + lruCache = new LRUCache<>(100, strategy); lruCache.put(null, null); - assert lruCache.containsKey(null); - assert lruCache.containsValue(null); - assert lruCache.toString().contains("null=null"); - assert lruCache.hashCode() != 0; + assertTrue(lruCache.containsKey(null)); + assertTrue(lruCache.containsValue(null)); + assertTrue(lruCache.toString().contains("null=null")); + assertNotEquals(0, lruCache.hashCode()); - LRUCache cache1 = new LRUCache<>(3); + LRUCache cache1 = new LRUCache<>(3, strategy); cache1.put(null, null); - LRUCache cache2 = new LRUCache<>(3); + LRUCache cache2 = new LRUCache<>(3, strategy); cache2.put(null, null); - assert cache1.equals(cache2); + assertTrue(cache1.equals(cache2)); } - @Test - void testSpeed() - { + @ParameterizedTest + @MethodSource("strategies") + void testSpeed(LRUCache.StrategyType strategy) { + setUp(strategy); long startTime = System.currentTimeMillis(); - LRUCache cache = new LRUCache<>(30000000); - for (int i = 0; i < 30000000; i++) { + LRUCache cache = new LRUCache<>(10000000, strategy); + for (int i = 0; i < 10000000; i++) { cache.put(i, true); } long endTime = System.currentTimeMillis();