├── .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 |
--------------------------------------------------------------------------------