├── .idea ├── .gitignore ├── misc.xml ├── modules.xml ├── uiDesigner.xml └── vcs.xml ├── ConcurrentTester.iml ├── README.md ├── out └── production │ └── ConcurrentTester │ ├── cache │ ├── Cache.class │ └── CacheInterface.class │ ├── database │ ├── DBCall.class │ ├── DBRType.class │ ├── Database.class │ ├── DatabaseInterface.class │ └── DatabaseRequest.class │ └── tester │ └── CacheTester.class └── src ├── cache ├── Cache.java ├── CacheException.java ├── CacheInterface.java └── implementations │ └── LRUCache.java ├── database ├── DBCall.java ├── DBFailure.java ├── DBRType.java ├── Database.java ├── DatabaseInterface.java └── DatabaseRequest.java ├── models ├── DoublyLinkedList.java └── Node.java └── tester ├── CacheTester.java ├── RequestGenerator.java ├── SingleCacheTester.java ├── models ├── RType.java ├── Request.java └── Response.java └── order ├── RandomOrganizer.java ├── RequestOrganiser.java ├── RotatingOrganizer.java └── SerialOrganizer.java /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/uiDesigner.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ConcurrentTester.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Concurrent Cache Testing 2 | 3 | This project introduces a comprehensive testing framework designed to evaluate the performance of different Least Recently Used (LRU) cache implementations integrated with a simulated database environment. 4 | 5 | The framework tests under various configurations and scenarios to benchmark the efficiency, reliability, and scalability of cache strategies within concurrent systems. 6 | 7 | A sample configuration of an LRU cache is available. It includes features such as request collapsing and various threading models to simulate different real-world usage scenarios. 8 | 9 | ### How It Works 10 | The testing framework operates by executing the following steps: 11 | 12 | 1. **Generate Requests**: Different patterns of requests are generated to simulate varied user interaction patterns with the cache. 13 | 2. **Organize Requests**: Requests are organized according to specified strategies to test the cache's response to different access patterns. 14 | 3. **Simulate Cache Operation**: Each cache configuration is tested with the organized requests. The cache interacts with the simulated database, handling GET and SET operations. 15 | 4. **Measure Performance**: After executing the tests, the system reports various metrics which detail the effectiveness and efficiency of the cache configuration under test. 16 | 17 | ### What You Get Out of It 18 | 19 | 1. Understand **performance impacts** of different cache configurations. 20 | 2. Diagnose potential scalability and **reliability issues** in cache implementations. 21 | 22 | ### How to Run 23 | 24 | Just run the main program in *CacheTester*. 25 | -------------------------------------------------------------------------------- /out/production/ConcurrentTester/cache/Cache.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InterviewReady/Concurrency-Tester/c28f709a82362827e627d40270ab2f742e8644d5/out/production/ConcurrentTester/cache/Cache.class -------------------------------------------------------------------------------- /out/production/ConcurrentTester/cache/CacheInterface.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InterviewReady/Concurrency-Tester/c28f709a82362827e627d40270ab2f742e8644d5/out/production/ConcurrentTester/cache/CacheInterface.class -------------------------------------------------------------------------------- /out/production/ConcurrentTester/database/DBCall.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InterviewReady/Concurrency-Tester/c28f709a82362827e627d40270ab2f742e8644d5/out/production/ConcurrentTester/database/DBCall.class -------------------------------------------------------------------------------- /out/production/ConcurrentTester/database/DBRType.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InterviewReady/Concurrency-Tester/c28f709a82362827e627d40270ab2f742e8644d5/out/production/ConcurrentTester/database/DBRType.class -------------------------------------------------------------------------------- /out/production/ConcurrentTester/database/Database.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InterviewReady/Concurrency-Tester/c28f709a82362827e627d40270ab2f742e8644d5/out/production/ConcurrentTester/database/Database.class -------------------------------------------------------------------------------- /out/production/ConcurrentTester/database/DatabaseInterface.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InterviewReady/Concurrency-Tester/c28f709a82362827e627d40270ab2f742e8644d5/out/production/ConcurrentTester/database/DatabaseInterface.class -------------------------------------------------------------------------------- /out/production/ConcurrentTester/database/DatabaseRequest.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InterviewReady/Concurrency-Tester/c28f709a82362827e627d40270ab2f742e8644d5/out/production/ConcurrentTester/database/DatabaseRequest.class -------------------------------------------------------------------------------- /out/production/ConcurrentTester/tester/CacheTester.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InterviewReady/Concurrency-Tester/c28f709a82362827e627d40270ab2f742e8644d5/out/production/ConcurrentTester/tester/CacheTester.class -------------------------------------------------------------------------------- /src/cache/Cache.java: -------------------------------------------------------------------------------- 1 | package cache; 2 | 3 | import database.Database; 4 | import database.DatabaseInterface; 5 | 6 | import java.util.concurrent.Future; 7 | 8 | public abstract class Cache implements CacheInterface { 9 | protected final DatabaseInterface database; 10 | 11 | protected Cache(final DatabaseInterface database) { 12 | this.database = database; 13 | } 14 | 15 | public Cache() { 16 | this.database = new Database(5, 0.01); 17 | } 18 | 19 | @Override 20 | public abstract Future get(String key); 21 | 22 | @Override 23 | public abstract Future put(String key, String value); 24 | } 25 | -------------------------------------------------------------------------------- /src/cache/CacheException.java: -------------------------------------------------------------------------------- 1 | package cache; 2 | 3 | public class CacheException extends RuntimeException { 4 | public CacheException() { 5 | super("Temporary error, please retry."); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/cache/CacheInterface.java: -------------------------------------------------------------------------------- 1 | package cache; 2 | 3 | import java.util.concurrent.Future; 4 | 5 | public interface CacheInterface { 6 | Future get(String key); 7 | Future put(String key, String value); 8 | } 9 | -------------------------------------------------------------------------------- /src/cache/implementations/LRUCache.java: -------------------------------------------------------------------------------- 1 | package cache.implementations; 2 | 3 | import cache.Cache; 4 | import cache.CacheException; 5 | import database.DBFailure; 6 | import database.DatabaseInterface; 7 | import models.DoublyLinkedList; 8 | import models.Node; 9 | 10 | import java.util.HashMap; 11 | import java.util.Map; 12 | import java.util.concurrent.*; 13 | import java.util.concurrent.atomic.LongAdder; 14 | import java.util.concurrent.locks.Lock; 15 | import java.util.concurrent.locks.ReentrantLock; 16 | 17 | public class LRUCache extends Cache { 18 | private final String name; 19 | private final int size; 20 | private final DoublyLinkedList doublyLinkedList = new DoublyLinkedList(); 21 | private final Map store = new HashMap<>(); 22 | private final Lock lock = new ReentrantLock(); 23 | private final ExecutorService[] dbQueryExecutors; 24 | private final LongAdder[] beingModified; 25 | private final boolean requestCollapsing; 26 | private final Statistics statistics; 27 | 28 | public LRUCache(String name, 29 | int size, 30 | int dbThreadPool, 31 | boolean requestCollapsing, 32 | DatabaseInterface database) { 33 | super(database); 34 | this.name = name; 35 | this.size = size; 36 | this.dbQueryExecutors = new ExecutorService[dbThreadPool]; 37 | this.beingModified = new LongAdder[dbThreadPool]; 38 | this.requestCollapsing = requestCollapsing; 39 | for (int i = 0; i < dbThreadPool; i++) { 40 | dbQueryExecutors[i] = Executors.newSingleThreadExecutor(); 41 | beingModified[i] = new LongAdder(); 42 | } 43 | statistics = new Statistics(); 44 | } 45 | 46 | @Override 47 | public Future get(String key) { 48 | if (requestCollapsing && beingModified[getHashIndex(key)].sum() == 0) { 49 | final Node node = store.get(key); 50 | if (node != null) { 51 | statistics.hits.increment(); 52 | if (!node.value.isDone()) { 53 | statistics.collapses.increment(); 54 | } 55 | return node.value; 56 | } else { 57 | statistics.misses.increment(); 58 | } 59 | } else { 60 | statistics.waitInQueue.increment(); 61 | } 62 | return CompletableFuture.supplyAsync(() -> { 63 | try { 64 | lock.lock(); 65 | if (store.containsKey(key)) { 66 | statistics.hitsAfterWait.increment(); 67 | return moveToHead(key); 68 | } 69 | statistics.missesAfterWait.increment(); 70 | evict(); 71 | return database.get(key); 72 | } finally { 73 | lock.unlock(); 74 | } 75 | }, getExecutor(key)).thenApply(future -> { 76 | try { 77 | String s = future.get(1, TimeUnit.SECONDS); 78 | lock.lock(); 79 | add(key, CompletableFuture.completedFuture(s)); 80 | lock.unlock(); 81 | return s; 82 | } catch (Exception e) { 83 | throw wrapAndHandleException(key, e); 84 | } 85 | }); 86 | } 87 | 88 | @Override 89 | public Future put(String key, String value) { 90 | beingModified[getHashIndex(key)].increment(); 91 | return CompletableFuture.supplyAsync(() -> { 92 | lock.lock(); 93 | remove(key); 94 | lock.unlock(); 95 | try { 96 | return database.set(key, value).get(1, TimeUnit.SECONDS); 97 | } catch (Exception e) { 98 | throw wrapAndHandleException(key, e); 99 | } 100 | }, getExecutor(key)).thenAccept(__ -> beingModified[getHashIndex(key)].decrement()); 101 | } 102 | 103 | private RuntimeException wrapAndHandleException(String key, Throwable e) { 104 | if (e.getCause() instanceof DBFailure) { 105 | lock.lock(); 106 | remove(key); 107 | lock.unlock(); 108 | return new CacheException(); 109 | } else { 110 | System.err.println("Failed to get key: " + key); 111 | e.printStackTrace(); 112 | return new IllegalStateException(e); 113 | } 114 | } 115 | 116 | private Future moveToHead(String key) { 117 | final Node node = store.get(key); 118 | doublyLinkedList.delete(node); 119 | doublyLinkedList.updateHead(node); 120 | return node.value; 121 | } 122 | 123 | private void evict() { 124 | while (store.size() == size) { 125 | final Node evicted = doublyLinkedList.evict(); 126 | store.remove(evicted.key); 127 | statistics.evictions.increment(); 128 | } 129 | } 130 | 131 | private void add(String key, Future result) { 132 | final Node node = new Node(key, result); 133 | doublyLinkedList.updateHead(node); 134 | store.put(key, node); 135 | } 136 | 137 | private void remove(String key) { 138 | final Node node = store.remove(key); 139 | if (node != null) { 140 | doublyLinkedList.delete(node); 141 | } 142 | } 143 | 144 | private ExecutorService getExecutor(String key) { 145 | return dbQueryExecutors[getHashIndex(key)]; 146 | } 147 | 148 | private int getHashIndex(String key) { 149 | return Math.abs(key.hashCode()) % dbQueryExecutors.length; 150 | } 151 | 152 | public String getName() { 153 | return name; 154 | } 155 | 156 | public String getStats() { 157 | return statistics.toString() + "\n" + database.getStats(); 158 | } 159 | } 160 | 161 | class Statistics { 162 | public LongAdder hits = new LongAdder(), 163 | hitsAfterWait = new LongAdder(), 164 | misses = new LongAdder(), 165 | missesAfterWait = new LongAdder(), 166 | evictions = new LongAdder(), 167 | collapses = new LongAdder(), 168 | waitInQueue = new LongAdder(); 169 | 170 | @Override 171 | public String toString() { 172 | return "Statistics{" + 173 | "hits=" + hits.sum() + 174 | ", misses=" + misses.sum() + 175 | ", collapses=" + collapses.sum() + 176 | ", waitInQueue=" + waitInQueue.sum() + 177 | ", hitsAfterWait=" + hitsAfterWait.sum() + 178 | ", missesAfterWait=" + missesAfterWait.sum() + 179 | ", evictions=" + evictions.sum() + 180 | '}'; 181 | } 182 | } -------------------------------------------------------------------------------- /src/database/DBCall.java: -------------------------------------------------------------------------------- 1 | package database; 2 | 3 | import java.util.concurrent.CompletableFuture; 4 | 5 | class DBCall { 6 | final DatabaseRequest request; 7 | final CompletableFuture response; 8 | final Long startTime; 9 | 10 | public DBCall(DatabaseRequest request, CompletableFuture response, Long startTime) { 11 | this.request = request; 12 | this.response = response; 13 | this.startTime = startTime; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/database/DBFailure.java: -------------------------------------------------------------------------------- 1 | package database; 2 | 3 | public class DBFailure extends RuntimeException { 4 | public DBFailure() { 5 | super("Mock failure, please retry."); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/database/DBRType.java: -------------------------------------------------------------------------------- 1 | package database; 2 | 3 | enum DBRType { 4 | GET, SET 5 | } 6 | -------------------------------------------------------------------------------- /src/database/Database.java: -------------------------------------------------------------------------------- 1 | package database; 2 | 3 | import java.util.*; 4 | import java.util.concurrent.*; 5 | import java.util.concurrent.atomic.LongAdder; 6 | import java.util.concurrent.locks.ReadWriteLock; 7 | import java.util.concurrent.locks.ReentrantReadWriteLock; 8 | 9 | /** 10 | * Represents a concurrent database with tunable failure rate and no ordering guarantees for responses. 11 | * This database handles GET and SET operations, processes requests concurrently, and tracks statistics. 12 | */ 13 | public class Database implements DatabaseInterface { 14 | 15 | // Configuration and state variables 16 | private final int batchRequestThreshold; 17 | private final Map db; 18 | private final Map requestCount; 19 | private final List pendingCalls; 20 | private final ReadWriteLock lock = new ReentrantReadWriteLock(); 21 | private final ScheduledExecutorService executorService; 22 | private final double failureRate; 23 | 24 | // Metrics counters 25 | private final LongAdder batchCompletion = new LongAdder(), 26 | clearance = new LongAdder(), 27 | concurrentRequests = new LongAdder(), 28 | failures = new LongAdder(), 29 | hits = new LongAdder(); 30 | 31 | /** 32 | * Constructs a Database instance with specified batch processing threshold and failure rate. 33 | * 34 | * @param batchThreshold The threshold for batch processing of requests. 35 | * @param failureRate The tunable failure rate for simulated failures. 36 | */ 37 | public Database(final int batchThreshold, double failureRate) { 38 | this.batchRequestThreshold = batchThreshold; 39 | this.failureRate = failureRate; 40 | db = new HashMap<>(); 41 | requestCount = new ConcurrentHashMap<>(batchRequestThreshold); 42 | pendingCalls = new ArrayList<>(); 43 | executorService = Executors.newSingleThreadScheduledExecutor(); 44 | // Schedule batch processing task to run periodically 45 | executorService.scheduleAtFixedRate(this::completePendingRequests, 0, 1, TimeUnit.MILLISECONDS); 46 | } 47 | 48 | /** 49 | * Retrieves the value associated with the specified key from the database. 50 | * 51 | * @param key The key for which to retrieve the value. 52 | * @return A Future representing the asynchronous result of the GET operation. 53 | */ 54 | public Future get(String key) { 55 | hits.increment(); // Increment the hits counter 56 | return addToRequestQueue(new DatabaseRequest(DBRType.GET, key)); 57 | } 58 | 59 | /** 60 | * Sets the value associated with the specified key in the database. 61 | * 62 | * @param key The key to set. 63 | * @param value The value to set. 64 | * @return A Future representing the asynchronous result of the SET operation. 65 | */ 66 | public Future set(String key, String value) { 67 | // Add the SET request to the queue and complete it with a null value 68 | return addToRequestQueue(new DatabaseRequest(DBRType.SET, key, value)) 69 | .thenAccept(__ -> { 70 | }); 71 | } 72 | 73 | /** 74 | * Adds a database request to the pending queue for processing. 75 | * 76 | * @param databaseRequest The database request to enqueue. 77 | * @return A CompletableFuture representing the asynchronous response to the request. 78 | */ 79 | private CompletableFuture addToRequestQueue(DatabaseRequest databaseRequest) { 80 | DBCall dbCall = new DBCall(databaseRequest, new CompletableFuture<>(), System.nanoTime()); 81 | lock.writeLock().lock(); 82 | requestCount.putIfAbsent(databaseRequest.key, new LongAdder()); 83 | LongAdder count = requestCount.get(databaseRequest.key); 84 | count.increment(); 85 | if (count.sum() > 1) { 86 | concurrentRequests.increment(); 87 | } 88 | pendingCalls.add(dbCall); 89 | lock.writeLock().unlock(); 90 | // Trigger batch processing if the threshold is reached 91 | if (pendingCalls.size() >= batchRequestThreshold) { 92 | executorService.execute(this::completePendingRequests); 93 | } 94 | return dbCall.response; 95 | } 96 | 97 | /** 98 | * Processes pending database requests, allowing for concurrent processing. 99 | */ 100 | private void completePendingRequests() { 101 | if (!pendingCalls.isEmpty()) { 102 | lock.writeLock().lock(); 103 | if (!pendingCalls.isEmpty()) { 104 | boolean clearAll = pendingCalls.size() >= batchRequestThreshold; 105 | if (clearAll) { 106 | batchCompletion.increment(); 107 | } 108 | List completedRequests = new ArrayList<>(); 109 | Collections.shuffle(pendingCalls); // Randomize order for no ordering guarantees 110 | for (final var call : pendingCalls) { 111 | final boolean oldEntry = System.nanoTime() - call.startTime > 1000000; 112 | final DatabaseRequest request = call.request; 113 | final CompletableFuture response = call.response; 114 | if (Math.random() < failureRate) { // Simulate a failure 115 | failures.increment(); // Increment failure counter 116 | call.response.completeExceptionally(new DBFailure()); 117 | completedRequests.add(call); 118 | } else if (clearAll || oldEntry) { // Process the request 119 | if (!clearAll) { 120 | clearance.increment(); // Increment clearance counter 121 | } 122 | if (request.type.equals(DBRType.GET)) { 123 | response.complete(getKey(request.key)); // Complete with retrieved value 124 | } else { 125 | setKey(request.key, request.value); // Set value in the database 126 | response.complete(null); // Complete with null since SET doesn't return a value 127 | } 128 | completedRequests.add(call); 129 | } 130 | } 131 | // Remove completed requests from the pendingCalls list and decrement request count 132 | completedRequests.forEach(dbCall -> { 133 | pendingCalls.remove(dbCall); 134 | requestCount.get(dbCall.request.key).decrement(); 135 | }); 136 | } 137 | lock.writeLock().unlock(); 138 | } 139 | } 140 | 141 | /** 142 | * Retrieves the value associated with the specified key from the database. 143 | * 144 | * @param key The key for which to retrieve the value. 145 | * @return The value associated with the key, or null if the key is not found. 146 | */ 147 | private String getKey(String key) { 148 | return db.get(key); 149 | } 150 | 151 | /** 152 | * Sets the value associated with the specified key in the database. 153 | * 154 | * @param key The key to set. 155 | * @param value The value to set. 156 | */ 157 | private void setKey(String key, String value) { 158 | db.put(key, value); 159 | } 160 | 161 | /** 162 | * Returns statistics about the database's performance and operation. 163 | * 164 | * @return A formatted string containing various statistics. 165 | */ 166 | @Override 167 | public String getStats() { 168 | // Return a formatted string containing various statistics 169 | return "clearances: " + clearance.sum() 170 | + " batchCompletions: " + batchCompletion.sum() 171 | + " concurrentRequests: " + concurrentRequests.sum() 172 | + " failures: " + failures.sum() 173 | + " hits: " + hits.sum(); 174 | } 175 | } -------------------------------------------------------------------------------- /src/database/DatabaseInterface.java: -------------------------------------------------------------------------------- 1 | package database; 2 | 3 | import java.util.concurrent.Future; 4 | 5 | public interface DatabaseInterface { 6 | Future get(String key); 7 | Future set(String key, String value); 8 | String getStats(); 9 | } 10 | -------------------------------------------------------------------------------- /src/database/DatabaseRequest.java: -------------------------------------------------------------------------------- 1 | package database; 2 | 3 | class DatabaseRequest { 4 | final DBRType type; 5 | final String key; 6 | final String value; 7 | 8 | public DatabaseRequest(DBRType type, String key, String value) { 9 | this.type = type; 10 | this.key = key; 11 | this.value = value; 12 | } 13 | 14 | public DatabaseRequest(DBRType type, String key) { 15 | if (type.equals(DBRType.SET)) { 16 | throw new IllegalArgumentException("Cannot create a DB set request without value"); 17 | } 18 | this.type = type; 19 | this.key = key; 20 | this.value = null; 21 | } 22 | 23 | @Override 24 | public String toString() { 25 | return "{" + 26 | "type=" + type + 27 | ", key='" + key + '\'' + 28 | (type.equals(DBRType.SET) ? ", value='" + value + '\'' : "") + 29 | '}'; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/models/DoublyLinkedList.java: -------------------------------------------------------------------------------- 1 | package models; 2 | 3 | public class DoublyLinkedList { 4 | Node head, tail; 5 | public void updateHead(Node node) { 6 | node.next = head; 7 | node.prev = null; 8 | if (head != null) { 9 | head.prev = node; 10 | } 11 | head = node; 12 | if (tail == null) { 13 | tail = node; 14 | } 15 | } 16 | 17 | public Node evict() { 18 | final Node deleted = tail; 19 | tail = tail.prev; 20 | if (tail == null) { 21 | System.err.println("HEAD when tail is null: " + head); 22 | throw new IllegalStateException(); 23 | } 24 | tail.next = null; 25 | return deleted; 26 | } 27 | 28 | public void delete(Node node) { 29 | if (head == node) 30 | head = node.next; 31 | if (tail == node) 32 | tail = node.prev; 33 | if (node.next != null) 34 | node.next.prev = node.prev; 35 | if (node.prev != null) 36 | node.prev.next = node.next; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/models/Node.java: -------------------------------------------------------------------------------- 1 | package models; 2 | 3 | import java.util.concurrent.Future; 4 | 5 | public class Node { 6 | public final String key; 7 | public final Future value; 8 | public Node next; 9 | public Node prev; 10 | 11 | public Node(String key, Future value) { 12 | this.key = key; 13 | this.value = value; 14 | } 15 | 16 | public String toString() { 17 | Node current = this; 18 | StringBuilder s = new StringBuilder(); 19 | int count = 0; 20 | while (current != null) { 21 | s.append(current.key).append(", "); 22 | current = current.next; 23 | count++; 24 | if (count > 100) { 25 | throw new IllegalStateException("Infinite Linked list? " + s); 26 | } 27 | } 28 | return s.toString(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/tester/CacheTester.java: -------------------------------------------------------------------------------- 1 | package tester; 2 | 3 | import cache.CacheException; 4 | import cache.implementations.LRUCache; 5 | import database.Database; 6 | import tester.models.RType; 7 | import tester.models.Request; 8 | import tester.order.RandomOrganizer; 9 | import tester.order.RequestOrganiser; 10 | import tester.order.RotatingOrganizer; 11 | import tester.order.SerialOrganizer; 12 | 13 | import java.util.*; 14 | import java.util.concurrent.CompletableFuture; 15 | import java.util.concurrent.ExecutorService; 16 | import java.util.concurrent.Executors; 17 | import java.util.concurrent.TimeUnit; 18 | import java.util.stream.Collectors; 19 | 20 | public class CacheTester { 21 | public static void main(String[] args) { 22 | final List organizers = Arrays.asList( 23 | new RandomOrganizer(), 24 | new SerialOrganizer(), 25 | new RotatingOrganizer()); 26 | final List generators = Arrays.asList( 27 | new RequestGenerator(0.1), 28 | new RequestGenerator(0.5), 29 | new RequestGenerator(0.01) 30 | ); 31 | final int keySpace = 30, requestsPerKey = 40; 32 | for (final RequestGenerator generator : generators) { 33 | final var requestMap = generator.setupRequests(keySpace, requestsPerKey); 34 | for (final RequestOrganiser organizer : organizers) { 35 | final var requests = organizer.setOrder(keySpace, requestsPerKey, requestMap); 36 | for (int factor = 2; factor <= 6; factor = factor + 2) { 37 | for (int batchThreshold = 5; batchThreshold <= keySpace; batchThreshold += keySpace / 3) { 38 | for (double failureRate = 0; failureRate < 0.03; failureRate += 0.01) { 39 | final int cacheSize = keySpace / factor; 40 | final List cacheInterfaces = Arrays.asList( 41 | new LRUCache("Blocking", cacheSize, 1, false, new Database(batchThreshold, failureRate)), 42 | new LRUCache("Blocking Request Collapsing", cacheSize, 1, true, new Database(batchThreshold, failureRate)), 43 | new LRUCache("Concurrent", cacheSize, cacheSize, false, new Database(batchThreshold, failureRate)), 44 | new LRUCache("Concurrent Request Collapsing", cacheSize, cacheSize, true, new Database(batchThreshold, failureRate)) 45 | ); 46 | for (final LRUCache cache : cacheInterfaces) { 47 | System.out.println("Configuration: " + cache.getName() 48 | + " + " + organizer.getClass().getSimpleName() 49 | + " + writeProbability: " + generator.getWriteProbability() 50 | + " + batchThreshold: " + batchThreshold 51 | + " + failureRate: " + failureRate 52 | + " + cacheSize: " + (100.0 / factor)); 53 | testCache(cache, requests); 54 | } 55 | } 56 | } 57 | } 58 | } 59 | } 60 | System.exit(0); 61 | } 62 | 63 | private static void testCache(LRUCache cache, List requests) { 64 | final long startTime = System.nanoTime() / 1000000000; 65 | final List> tasks = new ArrayList<>(); 66 | final ExecutorService[] executorService = new ExecutorService[3]; 67 | for (int i = 0; i < executorService.length; i++) { 68 | executorService[i] = Executors.newSingleThreadExecutor(); 69 | } 70 | for (final Request request : requests) { 71 | final String key = request.getKey(); 72 | tasks.add(CompletableFuture.runAsync(() -> { 73 | if (request.getType().equals(RType.GET)) { 74 | request.setResponse(cache.get(key)); 75 | } else { 76 | request.setResponse(cache.put(key, request.getValue())); 77 | } 78 | }, executorService[Math.abs(key.hashCode()) % executorService.length])); 79 | try { 80 | Thread.sleep(1); 81 | } catch (InterruptedException e) { 82 | System.err.println("Thread sleep issues for request: " + request); 83 | throw new RuntimeException(e); 84 | } 85 | } 86 | try { 87 | CompletableFuture.allOf(tasks.toArray(new CompletableFuture[tasks.size()])).get(60, TimeUnit.SECONDS); 88 | } catch (Exception e) { 89 | e.printStackTrace(); 90 | System.err.println("Problem when completing tasks"); 91 | System.exit(0); 92 | } 93 | int cacheFailures = 0; 94 | final Map currentValue = new HashMap<>(); 95 | for (final Request request : requests) { 96 | Object result = null; 97 | boolean cacheFailure = false; 98 | try { 99 | result = request.getResponse().get(10, TimeUnit.SECONDS); 100 | } catch (Exception e) { 101 | if (e.getCause() instanceof CacheException) { 102 | cacheFailure = true; 103 | cacheFailures++; 104 | } else { 105 | System.err.println("Failed to " + request.getType() + " key: " + request.getKey() + " time: " + System.nanoTime() / 1000000000); 106 | e.printStackTrace(); 107 | printTraceAndExit(requests, request); 108 | } 109 | } 110 | if (!cacheFailure) { 111 | if (request.getType().equals(RType.GET)) { 112 | if (!Objects.equals(currentValue.get(request.getKey()), result)) { 113 | System.err.println("Mismatch in response state: " + result + " and expected value:" + currentValue.get(request.getKey()) + " for key: " + request.getKey()); 114 | printTraceAndExit(requests, request); 115 | } 116 | } else { 117 | currentValue.put(request.getKey(), request.getValue()); 118 | } 119 | } 120 | } 121 | System.out.println("PASSED IN " + (System.nanoTime() / 1000000000d - startTime) + " SECONDS"); 122 | System.out.println("CacheFailures: " + cacheFailures + " " + cache.getStats()); 123 | } 124 | 125 | private static void printTraceAndExit(List requests, Request request) { 126 | System.err.println(requests.stream().filter(r -> r.getKey().equals(request.getKey())).map(r -> { 127 | try { 128 | return r.getResponse().get(); 129 | } catch (Exception e) { 130 | throw new IllegalStateException(); 131 | } 132 | }).collect(Collectors.toList())); 133 | System.exit(0); 134 | } 135 | } -------------------------------------------------------------------------------- /src/tester/RequestGenerator.java: -------------------------------------------------------------------------------- 1 | package tester; 2 | 3 | import tester.models.RType; 4 | import tester.models.Request; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import java.util.UUID; 9 | 10 | public class RequestGenerator { 11 | private final double writeProbability; 12 | 13 | public RequestGenerator(double writeProbability) { 14 | this.writeProbability = writeProbability; 15 | } 16 | 17 | public List[] setupRequests(int keySpace, int requestsPerKey) { 18 | List[] requestMap = new List[keySpace]; 19 | for (int i = 0; i < requestMap.length; i++) { 20 | requestMap[i] = new ArrayList<>(); 21 | final String key = UUID.randomUUID().toString(); 22 | requestMap[i].add(new Request(RType.PUT, key, UUID.randomUUID().toString())); 23 | for (int j = 1; j < requestsPerKey; j++) { 24 | requestMap[i].add(generateRequest(key, writeProbability)); 25 | } 26 | } 27 | return requestMap; 28 | } 29 | 30 | private Request generateRequest(String key, double writeProbability) { 31 | if (Math.random() < writeProbability) { 32 | return new Request(RType.PUT, key, UUID.randomUUID().toString()); 33 | } else { 34 | return new Request(RType.GET, key); 35 | } 36 | } 37 | 38 | public double getWriteProbability() { 39 | return writeProbability; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/tester/SingleCacheTester.java: -------------------------------------------------------------------------------- 1 | package tester; 2 | 3 | import cache.Cache; 4 | import cache.CacheException; 5 | import cache.implementations.LRUCache; 6 | import database.Database; 7 | import tester.models.RType; 8 | import tester.models.Request; 9 | import tester.order.RequestOrganiser; 10 | import tester.order.RotatingOrganizer; 11 | import tester.order.SerialOrganizer; 12 | 13 | import java.util.*; 14 | import java.util.concurrent.CompletableFuture; 15 | import java.util.concurrent.ExecutorService; 16 | import java.util.concurrent.Executors; 17 | import java.util.concurrent.TimeUnit; 18 | import java.util.stream.Collectors; 19 | 20 | public class SingleCacheTester { 21 | public static void main(String[] args) { 22 | final List organizers = Arrays.asList( 23 | new SerialOrganizer(), 24 | new RotatingOrganizer()); 25 | final int keySpace = 40, requestsPerKey = 100; 26 | final var generator = new RequestGenerator(0.01); 27 | final var requestMap = generator.setupRequests(keySpace, requestsPerKey); 28 | for (final RequestOrganiser organizer : organizers) { 29 | final var requests = organizer.setOrder(keySpace, requestsPerKey, requestMap); 30 | for (int factor = 1; factor <= 2; factor++) { 31 | final int cacheSize = keySpace / factor; 32 | Database database = new Database(5, 0.01); 33 | Cache cache = new LRUCache("", cacheSize, 1, false, database); 34 | System.out.println("Configuration: " + organizer.getClass().getSimpleName() 35 | + " + cacheSize: " + (100.0 / factor)); 36 | testCache(cache, requests); 37 | System.out.println(database.getStats()); 38 | } 39 | } 40 | System.exit(0); 41 | } 42 | 43 | private static void testCache(Cache cache, List requests) { 44 | final long startTime = System.nanoTime() / 1000000000; 45 | final List> tasks = new ArrayList<>(); 46 | final ExecutorService[] executorService = new ExecutorService[3]; 47 | for (int i = 0; i < executorService.length; i++) { 48 | executorService[i] = Executors.newSingleThreadExecutor(); 49 | } 50 | for (final Request request : requests) { 51 | final String key = request.getKey(); 52 | tasks.add(CompletableFuture.runAsync(() -> { 53 | if (request.getType().equals(RType.GET)) { 54 | request.setResponse(cache.get(key)); 55 | } else { 56 | request.setResponse(cache.put(key, request.getValue())); 57 | } 58 | }, executorService[Math.abs(key.hashCode()) % executorService.length])); 59 | try { 60 | Thread.sleep(1); 61 | } catch (InterruptedException e) { 62 | System.err.println("Thread sleep issues for request: " + request); 63 | throw new RuntimeException(e); 64 | } 65 | } 66 | try { 67 | CompletableFuture.allOf(tasks.toArray(new CompletableFuture[tasks.size()])).get(60, TimeUnit.SECONDS); 68 | } catch (Exception e) { 69 | e.printStackTrace(); 70 | System.err.println("Problem when completing tasks"); 71 | System.exit(0); 72 | } 73 | int cacheFailures = 0; 74 | final Map currentValue = new HashMap<>(); 75 | for (final Request request : requests) { 76 | Object result = null; 77 | boolean cacheFailure = false; 78 | try { 79 | result = request.getResponse().get(10, TimeUnit.SECONDS); 80 | } catch (Exception e) { 81 | if (e.getCause() instanceof CacheException) { 82 | cacheFailure = true; 83 | cacheFailures++; 84 | } else { 85 | System.err.println("Failed to " + request.getType() + " key: " + request.getKey() + " time: " + System.nanoTime() / 1000000000); 86 | e.printStackTrace(); 87 | printTraceAndExit(requests, request); 88 | } 89 | } 90 | if (!cacheFailure) { 91 | if (request.getType().equals(RType.GET)) { 92 | if (!Objects.equals(currentValue.get(request.getKey()), result)) { 93 | System.err.println("Mismatch in response state: " + result + " and expected value:" + currentValue.get(request.getKey()) + " for key: " + request.getKey()); 94 | printTraceAndExit(requests, request); 95 | } 96 | } else { 97 | currentValue.put(request.getKey(), request.getValue()); 98 | } 99 | } 100 | } 101 | System.out.println("PASSED IN " + (System.nanoTime() / 1000000000d - startTime) + " SECONDS"); 102 | } 103 | 104 | private static void printTraceAndExit(List requests, Request request) { 105 | System.err.println(requests.stream().filter(r -> r.getKey().equals(request.getKey())).map(r -> { 106 | try { 107 | return r.getResponse().get(); 108 | } catch (Exception e) { 109 | throw new IllegalStateException(); 110 | } 111 | }).collect(Collectors.toList())); 112 | System.exit(0); 113 | } 114 | } -------------------------------------------------------------------------------- /src/tester/models/RType.java: -------------------------------------------------------------------------------- 1 | package tester.models; 2 | 3 | public enum RType { 4 | GET, PUT 5 | } 6 | -------------------------------------------------------------------------------- /src/tester/models/Request.java: -------------------------------------------------------------------------------- 1 | package tester.models; 2 | 3 | import java.util.UUID; 4 | import java.util.concurrent.Future; 5 | 6 | public class Request { 7 | final RType type; 8 | final String key; 9 | String value; 10 | Response response; 11 | String id; 12 | 13 | public Request(RType type, String key, String value) { 14 | this.type = type; 15 | this.key = key; 16 | this.value = value; 17 | this.id = UUID.randomUUID().toString(); 18 | response = new Response(); 19 | } 20 | 21 | public Request(RType type, String key) { 22 | this(type, key, null); 23 | } 24 | 25 | @Override 26 | public String toString() { 27 | return "{" + 28 | "type=" + type + 29 | ", key='" + key + '\'' + 30 | ", value='" + value + '\'' + 31 | ", id='" + id + '\'' + 32 | ", response='" + response.getResult() + '\'' + 33 | '}'; 34 | } 35 | 36 | public RType getType() { 37 | return type; 38 | } 39 | 40 | public String getKey() { 41 | return key; 42 | } 43 | 44 | public String getValue() { 45 | return value; 46 | } 47 | 48 | public Future getResponse() { 49 | return response.getResult(); 50 | } 51 | 52 | public void setResponse(Future future) { 53 | response.setResult(future); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/tester/models/Response.java: -------------------------------------------------------------------------------- 1 | package tester.models; 2 | 3 | import java.util.concurrent.Future; 4 | 5 | public class Response { 6 | Future result; 7 | 8 | public Future getResult() { 9 | return result; 10 | } 11 | 12 | public void setResult(Future future) { 13 | result = future; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/tester/order/RandomOrganizer.java: -------------------------------------------------------------------------------- 1 | package tester.order; 2 | 3 | import tester.models.Request; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.Random; 8 | 9 | public class RandomOrganizer implements RequestOrganiser { 10 | public List setOrder(int keySpace, int requestsPerKey, List[] requestMap) { 11 | List requests = new ArrayList<>(); 12 | final int[] currentPointer = new int[keySpace]; 13 | int currentSpace = keySpace; 14 | final var random = new Random(); 15 | for (int i = 0; i < keySpace * requestsPerKey; i++) { 16 | final int index = random.nextInt(currentSpace); 17 | final Request request = (Request) requestMap[index].get(currentPointer[index]); 18 | // System.out.println(i + " time: " + System.nanoTime() / 1000000000 + " request: " + request); 19 | requests.add(request); 20 | currentPointer[index]++; 21 | if (currentPointer[index] == requestMap[index].size()) { 22 | currentSpace--; 23 | currentPointer[index] = currentPointer[currentSpace]; 24 | var temp = requestMap[index]; 25 | requestMap[index] = requestMap[currentSpace]; 26 | requestMap[currentSpace] = temp; 27 | } 28 | } 29 | return requests; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/tester/order/RequestOrganiser.java: -------------------------------------------------------------------------------- 1 | package tester.order; 2 | 3 | 4 | import tester.models.Request; 5 | 6 | import java.util.List; 7 | 8 | public interface RequestOrganiser { 9 | List setOrder(int keySpace, int requestsPerKey, List[] requestMap); 10 | } 11 | -------------------------------------------------------------------------------- /src/tester/order/RotatingOrganizer.java: -------------------------------------------------------------------------------- 1 | package tester.order; 2 | 3 | import tester.models.Request; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | public class RotatingOrganizer implements RequestOrganiser { 9 | 10 | @Override 11 | public List setOrder(int keySpace, int requestsPerKey, List[] requestMap) { 12 | List requests = new ArrayList<>(); 13 | for (int i = 0; i < keySpace * requestsPerKey; i++) { 14 | final int index = i % keySpace; 15 | final Request request = (Request) requestMap[index].get(i / keySpace); 16 | // System.out.println(i + " time: " + System.nanoTime() / 1000000000 + " request: " + request); 17 | requests.add(request); 18 | } 19 | return requests; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/tester/order/SerialOrganizer.java: -------------------------------------------------------------------------------- 1 | package tester.order; 2 | 3 | import tester.models.Request; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | public class SerialOrganizer implements RequestOrganiser { 9 | @Override 10 | public List setOrder(int keySpace, int requestsPerKey, List[] requestMap) { 11 | List requests = new ArrayList<>(); 12 | for (int i = 0; i < keySpace * requestsPerKey; i++) { 13 | final int index = i / requestsPerKey; 14 | final Request request = (Request) requestMap[index].get(i % requestsPerKey); 15 | // System.out.println(i + " time: " + System.nanoTime() / 1000000000 + " request: " + request); 16 | requests.add(request); 17 | } 18 | return requests; 19 | } 20 | } 21 | --------------------------------------------------------------------------------