Added LLD projects

This commit is contained in:
Your Name
2020-07-15 18:02:38 +05:30
commit 0d2fabc962
135 changed files with 3319 additions and 0 deletions

3
distributed-cache/.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

13
distributed-cache/.idea/compiler.xml generated Normal file
View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<annotationProcessing>
<profile name="Maven default annotation processors profile" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<module name="DistributedCache" />
</profile>
</annotationProcessing>
</component>
</project>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://repo.maven.apache.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
</component>
</project>

14
distributed-cache/.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" project-jdk-name="11" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

124
distributed-cache/.idea/uiDesigner.xml generated Normal file
View File

@@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Palette2">
<group name="Swing">
<item class="com.intellij.uiDesigner.HSpacer" tooltip-text="Horizontal Spacer" icon="/com/intellij/uiDesigner/icons/hspacer.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="1" hsize-policy="6" anchor="0" fill="1" />
</item>
<item class="com.intellij.uiDesigner.VSpacer" tooltip-text="Vertical Spacer" icon="/com/intellij/uiDesigner/icons/vspacer.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="1" anchor="0" fill="2" />
</item>
<item class="javax.swing.JPanel" icon="/com/intellij/uiDesigner/icons/panel.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3" />
</item>
<item class="javax.swing.JScrollPane" icon="/com/intellij/uiDesigner/icons/scrollPane.png" removable="false" auto-create-binding="false" can-attach-label="true">
<default-constraints vsize-policy="7" hsize-policy="7" anchor="0" fill="3" />
</item>
<item class="javax.swing.JButton" icon="/com/intellij/uiDesigner/icons/button.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="0" fill="1" />
<initial-values>
<property name="text" value="Button" />
</initial-values>
</item>
<item class="javax.swing.JRadioButton" icon="/com/intellij/uiDesigner/icons/radioButton.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
<initial-values>
<property name="text" value="RadioButton" />
</initial-values>
</item>
<item class="javax.swing.JCheckBox" icon="/com/intellij/uiDesigner/icons/checkBox.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
<initial-values>
<property name="text" value="CheckBox" />
</initial-values>
</item>
<item class="javax.swing.JLabel" icon="/com/intellij/uiDesigner/icons/label.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="0" anchor="8" fill="0" />
<initial-values>
<property name="text" value="Label" />
</initial-values>
</item>
<item class="javax.swing.JTextField" icon="/com/intellij/uiDesigner/icons/textField.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JPasswordField" icon="/com/intellij/uiDesigner/icons/passwordField.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JFormattedTextField" icon="/com/intellij/uiDesigner/icons/formattedTextField.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JTextArea" icon="/com/intellij/uiDesigner/icons/textArea.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTextPane" icon="/com/intellij/uiDesigner/icons/textPane.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JEditorPane" icon="/com/intellij/uiDesigner/icons/editorPane.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JComboBox" icon="/com/intellij/uiDesigner/icons/comboBox.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="2" anchor="8" fill="1" />
</item>
<item class="javax.swing.JTable" icon="/com/intellij/uiDesigner/icons/table.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JList" icon="/com/intellij/uiDesigner/icons/list.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="2" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTree" icon="/com/intellij/uiDesigner/icons/tree.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTabbedPane" icon="/com/intellij/uiDesigner/icons/tabbedPane.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
<preferred-size width="200" height="200" />
</default-constraints>
</item>
<item class="javax.swing.JSplitPane" icon="/com/intellij/uiDesigner/icons/splitPane.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
<preferred-size width="200" height="200" />
</default-constraints>
</item>
<item class="javax.swing.JSpinner" icon="/com/intellij/uiDesigner/icons/spinner.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
</item>
<item class="javax.swing.JSlider" icon="/com/intellij/uiDesigner/icons/slider.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
</item>
<item class="javax.swing.JSeparator" icon="/com/intellij/uiDesigner/icons/separator.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3" />
</item>
<item class="javax.swing.JProgressBar" icon="/com/intellij/uiDesigner/icons/progressbar.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1" />
</item>
<item class="javax.swing.JToolBar" icon="/com/intellij/uiDesigner/icons/toolbar.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1">
<preferred-size width="-1" height="20" />
</default-constraints>
</item>
<item class="javax.swing.JToolBar$Separator" icon="/com/intellij/uiDesigner/icons/toolbarSeparator.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="0" anchor="0" fill="1" />
</item>
<item class="javax.swing.JScrollBar" icon="/com/intellij/uiDesigner/icons/scrollbar.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="0" anchor="0" fill="2" />
</item>
</group>
</component>
</project>

6
distributed-cache/.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4" />

View File

@@ -0,0 +1,9 @@
Should allow:
1) listeners on load and evict
2) hot loading elements on startup
3) multiple eviction algorithms like LRU and LFU
4) expiration time
5) multiple fetch algorithms like write back and write through
6) return futures
7) request collapsing
8) Avoid thrashing with rate limiting

32
distributed-cache/pom.xml Normal file
View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>interviewready.io</groupId>
<artifactId>distributed-cache</artifactId>
<version>1.0</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>11</source>
<target>11</target>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,179 @@
import events.*;
import models.*;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.*;
import java.util.function.Function;
public class Cache<KEY, VALUE> {
private final int maximumSize;
private final FetchAlgorithm fetchAlgorithm;
private final Duration expiryTime;
private final Map<KEY, CompletionStage<Record<KEY, VALUE>>> cache;
private final ConcurrentSkipListMap<AccessDetails, List<KEY>> priorityQueue;
private final ConcurrentSkipListMap<Long, List<KEY>> expiryQueue;
private final DataSource<KEY, VALUE> dataSource;
private final List<Event<KEY, VALUE>> eventQueue;
private final ExecutorService[] executorPool;
private final Timer timer;
protected Cache(final int maximumSize,
final Duration expiryTime,
final FetchAlgorithm fetchAlgorithm,
final EvictionAlgorithm evictionAlgorithm,
final DataSource<KEY, VALUE> dataSource,
final Set<KEY> keysToEagerlyLoad,
final Timer timer,
final int poolSize) {
this.expiryTime = expiryTime;
this.maximumSize = maximumSize;
this.fetchAlgorithm = fetchAlgorithm;
this.timer = timer;
this.cache = new ConcurrentHashMap<>();
this.eventQueue = new CopyOnWriteArrayList<>();
this.dataSource = dataSource;
this.executorPool = new ExecutorService[poolSize];
for (int i = 0; i < poolSize; i++) {
executorPool[i] = Executors.newSingleThreadExecutor();
}
priorityQueue = new ConcurrentSkipListMap<>((first, second) -> {
final var accessTimeDifference = (int) (first.getLastAccessTime() - second.getLastAccessTime());
if (evictionAlgorithm.equals(EvictionAlgorithm.LRU)) {
return accessTimeDifference;
} else {
final var accessCountDifference = first.getAccessCount() - second.getAccessCount();
return accessCountDifference != 0 ? accessCountDifference : accessTimeDifference;
}
});
expiryQueue = new ConcurrentSkipListMap<>();
final var eagerLoading = keysToEagerlyLoad.stream()
.map(key -> getThreadFor(key, addToCache(key, loadFromDB(dataSource, key))))
.toArray(CompletableFuture[]::new);
CompletableFuture.allOf(eagerLoading).join();
}
private <U> CompletionStage<U> getThreadFor(KEY key, CompletionStage<U> task) {
return CompletableFuture.supplyAsync(() -> task, executorPool[Math.abs(key.hashCode() % executorPool.length)]).thenCompose(Function.identity());
}
public CompletionStage<VALUE> get(KEY key) {
return getThreadFor(key, getFromCache(key));
}
public CompletionStage<Void> set(KEY key, VALUE value) {
return getThreadFor(key, setInCache(key, value));
}
private CompletionStage<VALUE> getFromCache(KEY key) {
final CompletionStage<Record<KEY, VALUE>> result;
if (!cache.containsKey(key)) {
result = addToCache(key, loadFromDB(dataSource, key));
} else {
result = cache.get(key).thenCompose(record -> {
if (hasExpired(record)) {
priorityQueue.get(record.getAccessDetails()).remove(key);
expiryQueue.get(record.getInsertionTime()).remove(key);
eventQueue.add(new Eviction<>(record, Eviction.Type.EXPIRY, timer.getCurrentTime()));
return addToCache(key, loadFromDB(dataSource, key));
} else {
return CompletableFuture.completedFuture(record);
}
});
}
return result.thenApply(record -> {
priorityQueue.get(record.getAccessDetails()).remove(key);
final AccessDetails updatedAccessDetails = record.getAccessDetails().update(timer.getCurrentTime());
priorityQueue.putIfAbsent(updatedAccessDetails, new CopyOnWriteArrayList<>());
priorityQueue.get(updatedAccessDetails).add(key);
record.setAccessDetails(updatedAccessDetails);
return record.getValue();
});
}
public CompletionStage<Void> setInCache(KEY key, VALUE value) {
CompletionStage<Void> result = CompletableFuture.completedFuture(null);
if (cache.containsKey(key)) {
result = cache.remove(key)
.thenAccept(oldRecord -> {
priorityQueue.get(oldRecord.getAccessDetails()).remove(key);
expiryQueue.get(oldRecord.getInsertionTime()).remove(key);
if (hasExpired(oldRecord)) {
eventQueue.add(new Eviction<>(oldRecord, Eviction.Type.EXPIRY, timer.getCurrentTime()));
} else {
eventQueue.add(new Update<>(new Record<>(key, value, timer.getCurrentTime()), oldRecord, timer.getCurrentTime()));
}
});
}
return result.thenCompose(__ -> addToCache(key, CompletableFuture.completedFuture(value))).thenCompose(record -> {
final CompletionStage<Void> writeOperation = persistRecord(record);
return fetchAlgorithm == FetchAlgorithm.WRITE_THROUGH ? writeOperation : CompletableFuture.completedFuture(null);
});
}
private CompletionStage<Record<KEY, VALUE>> addToCache(final KEY key, final CompletionStage<VALUE> valueFuture) {
manageEntries();
final var recordFuture = valueFuture.thenApply(value -> {
final Record<KEY, VALUE> record = new Record<>(key, value, timer.getCurrentTime());
expiryQueue.putIfAbsent(record.getInsertionTime(), new CopyOnWriteArrayList<>());
expiryQueue.get(record.getInsertionTime()).add(key);
priorityQueue.putIfAbsent(record.getAccessDetails(), new CopyOnWriteArrayList<>());
priorityQueue.get(record.getAccessDetails()).add(key);
return record;
});
cache.put(key, recordFuture);
return recordFuture;
}
private synchronized void manageEntries() {
if (cache.size() >= maximumSize) {
while (!expiryQueue.isEmpty() && hasExpired(expiryQueue.firstKey())) {
final List<KEY> keys = expiryQueue.pollFirstEntry().getValue();
for (final KEY key : keys) {
final Record<KEY, VALUE> expiredRecord = cache.remove(key).toCompletableFuture().join();
priorityQueue.remove(expiredRecord.getAccessDetails());
eventQueue.add(new Eviction<>(expiredRecord, Eviction.Type.EXPIRY, timer.getCurrentTime()));
}
}
}
if (cache.size() >= maximumSize) {
List<KEY> keys = priorityQueue.pollFirstEntry().getValue();
while (keys.isEmpty()) {
keys = priorityQueue.pollFirstEntry().getValue();
}
for (final KEY key : keys) {
final Record<KEY, VALUE> lowestPriorityRecord = cache.remove(key).toCompletableFuture().join();
expiryQueue.get(lowestPriorityRecord.getInsertionTime()).remove(lowestPriorityRecord.getKey());
eventQueue.add(new Eviction<>(lowestPriorityRecord, Eviction.Type.REPLACEMENT, timer.getCurrentTime()));
}
}
}
private CompletionStage<Void> persistRecord(final Record<KEY, VALUE> record) {
return dataSource.persist(record.getKey(), record.getValue(), record.getInsertionTime())
.thenAccept(__ -> eventQueue.add(new Write<>(record, timer.getCurrentTime())));
}
private boolean hasExpired(final Record<KEY, VALUE> record) {
return hasExpired(record.getInsertionTime());
}
private boolean hasExpired(final Long time) {
return Duration.ofNanos(timer.getCurrentTime() - time).compareTo(expiryTime) > 0;
}
public List<Event<KEY, VALUE>> getEventQueue() {
return eventQueue;
}
private CompletionStage<VALUE> loadFromDB(final DataSource<KEY, VALUE> dataSource, KEY key) {
return dataSource.load(key).whenComplete((value, throwable) -> {
if (throwable == null) {
eventQueue.add(new Load<>(new Record<>(key, value, timer.getCurrentTime()), timer.getCurrentTime()));
}
});
}
}

View File

@@ -0,0 +1,75 @@
import models.EvictionAlgorithm;
import models.FetchAlgorithm;
import models.Timer;
import java.time.Duration;
import java.util.HashSet;
import java.util.Set;
public class CacheBuilder<KEY, VALUE> {
private int maximumSize;
private Duration expiryTime;
private final Set<KEY> onStartLoad;
private EvictionAlgorithm evictionAlgorithm;
private FetchAlgorithm fetchAlgorithm;
private DataSource<KEY, VALUE> dataSource;
private Timer timer;
private int poolSize;
public CacheBuilder() {
maximumSize = 1000;
expiryTime = Duration.ofDays(365);
fetchAlgorithm = FetchAlgorithm.WRITE_THROUGH;
evictionAlgorithm = EvictionAlgorithm.LRU;
onStartLoad = new HashSet<>();
poolSize = 1;
timer = new Timer();
}
public CacheBuilder<KEY, VALUE> maximumSize(final int maximumSize) {
this.maximumSize = maximumSize;
return this;
}
public CacheBuilder<KEY, VALUE> expiryTime(final Duration expiryTime) {
this.expiryTime = expiryTime;
return this;
}
public CacheBuilder<KEY, VALUE> loadKeysOnStart(final Set<KEY> keys) {
this.onStartLoad.addAll(keys);
return this;
}
public CacheBuilder<KEY, VALUE> evictionAlgorithm(final EvictionAlgorithm evictionAlgorithm) {
this.evictionAlgorithm = evictionAlgorithm;
return this;
}
public CacheBuilder<KEY, VALUE> fetchAlgorithm(final FetchAlgorithm fetchAlgorithm) {
this.fetchAlgorithm = fetchAlgorithm;
return this;
}
public CacheBuilder<KEY, VALUE> dataSource(final DataSource<KEY, VALUE> dataSource) {
this.dataSource = dataSource;
return this;
}
public CacheBuilder<KEY, VALUE> timer(final Timer timer) {
this.timer = timer;
return this;
}
public CacheBuilder<KEY, VALUE> poolSize(final int poolSize) {
this.poolSize = poolSize;
return this;
}
public Cache<KEY, VALUE> build() {
if (dataSource == null) {
throw new IllegalArgumentException("No datasource configured");
}
return new Cache<>(maximumSize, expiryTime, fetchAlgorithm, evictionAlgorithm, dataSource, onStartLoad, timer, poolSize);
}
}

View File

@@ -0,0 +1,8 @@
import java.util.concurrent.CompletionStage;
public interface DataSource<KEY, VALUE> {
CompletionStage<VALUE> load(KEY key);
CompletionStage<Void> persist(KEY key, VALUE value, long timestamp);
}

View File

@@ -0,0 +1,37 @@
package events;
import models.Record;
import java.util.UUID;
public abstract class Event<KEY, VALUE> {
private final String id;
private final Record<KEY, VALUE> element;
private final long timestamp;
public Event(Record<KEY, VALUE> element, long timestamp) {
this.element = element;
this.timestamp = timestamp;
id = UUID.randomUUID().toString();
}
public String getId() {
return id;
}
public Record<KEY, VALUE> getElement() {
return element;
}
public long getTimestamp() {
return timestamp;
}
@Override
public String toString() {
return this.getClass().getName() + "{" +
"element=" + element +
", timestamp=" + timestamp +
"}\n";
}
}

View File

@@ -0,0 +1,28 @@
package events;
import models.Record;
public class Eviction<K, V> extends Event<K, V> {
private final Type type;
public Eviction(Record<K, V> element, Type type, long timestamp) {
super(element, timestamp);
this.type = type;
}
public Type getType() {
return type;
}
public enum Type {
EXPIRY, REPLACEMENT
}
@Override
public String toString() {
return "Eviction{" +
"type=" + type +
", "+super.toString() +
"}\n";
}
}

View File

@@ -0,0 +1,10 @@
package events;
import models.Record;
public class Load<K, V> extends Event<K, V> {
public Load(Record<K, V> element, long timestamp) {
super(element, timestamp);
}
}

View File

@@ -0,0 +1,13 @@
package events;
import models.Record;
public class Update<K, V> extends Event<K, V> {
private final Record<K, V> previousValue;
public Update(Record<K, V> element, Record<K, V> previousValue, long timestamp) {
super(element, timestamp);
this.previousValue = previousValue;
}
}

View File

@@ -0,0 +1,10 @@
package events;
import models.Record;
public class Write<K, V> extends Event<K, V> {
public Write(Record<K, V> element, long timestamp) {
super(element, timestamp);
}
}

View File

@@ -0,0 +1,50 @@
package models;
import java.util.Objects;
import java.util.concurrent.atomic.LongAdder;
public class AccessDetails {
private final LongAdder accessCount;
private long lastAccessTime;
public AccessDetails(long lastAccessTime) {
accessCount = new LongAdder();
this.lastAccessTime = lastAccessTime;
}
public long getLastAccessTime() {
return lastAccessTime;
}
public int getAccessCount() {
return (int) accessCount.longValue();
}
public AccessDetails update(long lastAccessTime) {
final AccessDetails accessDetails = new AccessDetails(lastAccessTime);
accessDetails.accessCount.add(this.accessCount.longValue() + 1);
return accessDetails;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
AccessDetails that = (AccessDetails) o;
return lastAccessTime == that.lastAccessTime &&
this.getAccessCount() == that.getAccessCount();
}
@Override
public int hashCode() {
return Objects.hash(getAccessCount(), lastAccessTime);
}
@Override
public String toString() {
return "AccessDetails{" +
"accessCount=" + accessCount +
", lastAccessTime=" + lastAccessTime +
'}';
}
}

View File

@@ -0,0 +1,5 @@
package models;
public enum EvictionAlgorithm {
LRU, LFU
}

View File

@@ -0,0 +1,5 @@
package models;
public enum FetchAlgorithm {
WRITE_THROUGH, WRITE_BACK
}

View File

@@ -0,0 +1,46 @@
package models;
public class Record<KEY, VALUE> {
private final KEY key;
private final VALUE value;
private final long insertionTime;
private AccessDetails accessDetails;
public Record(KEY key, VALUE value, long insertionTime) {
this.key = key;
this.value = value;
this.insertionTime = insertionTime;
this.accessDetails = new AccessDetails(insertionTime);
}
public KEY getKey() {
return key;
}
public VALUE getValue() {
return value;
}
public long getInsertionTime() {
return insertionTime;
}
public AccessDetails getAccessDetails() {
return accessDetails;
}
public void setAccessDetails(final AccessDetails accessDetails) {
this.accessDetails = accessDetails;
}
@Override
public String toString() {
return "Record{" +
"key=" + key +
", value=" + value +
", insertionTime=" + insertionTime +
", accessDetails=" + accessDetails +
'}';
}
}

View File

@@ -0,0 +1,7 @@
package models;
public class Timer {
public long getCurrentTime() {
return System.nanoTime();
}
}

View File

@@ -0,0 +1,366 @@
import events.Eviction;
import events.Load;
import events.Update;
import events.Write;
import models.EvictionAlgorithm;
import models.FetchAlgorithm;
import models.SettableTimer;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
public class TestCache {
private static final String PROFILE_MUMBAI_ENGINEER = "profile_mumbai_engineer", PROFILE_HYDERABAD_ENGINEER = "profile_hyderabad_engineer";
private final Map<String, String> dataMap = new ConcurrentHashMap<>();
private DataSource<String, String> dataSource;
private final Queue<CompletableFuture<Void>> writeOperations = new LinkedList<>();
private DataSource<String, String> writeBackDataSource;
@Before
public void setUp() {
dataMap.clear();
writeOperations.clear();
dataMap.put(PROFILE_MUMBAI_ENGINEER, "violet");
dataMap.put(PROFILE_HYDERABAD_ENGINEER, "blue");
dataSource = new DataSource<>() {
@Override
public CompletionStage<String> load(String key) {
if (dataMap.containsKey(key)) {
return CompletableFuture.completedFuture(dataMap.get(key));
} else {
return CompletableFuture.failedStage(new NullPointerException());
}
}
@Override
public CompletionStage<Void> persist(String key, String value, long timestamp) {
dataMap.put(key, value);
return CompletableFuture.completedFuture(null);
}
};
writeBackDataSource = new DataSource<>() {
@Override
public CompletionStage<String> load(String key) {
if (dataMap.containsKey(key)) {
return CompletableFuture.completedFuture(dataMap.get(key));
} else {
return CompletableFuture.failedStage(new NullPointerException());
}
}
@Override
public CompletionStage<Void> persist(String key, String value, long timestamp) {
final CompletableFuture<Void> hold = new CompletableFuture<>();
writeOperations.add(hold);
return hold.thenAccept(__ -> dataMap.put(key, value));
}
};
}
private void acceptWrite() {
final CompletableFuture<Void> write = writeOperations.poll();
if (write != null) {
write.complete(null);
}
}
@Test(expected = IllegalArgumentException.class)
public void testCacheConstructionWithoutDataSourceFailure() {
new CacheBuilder<>().build();
}
@Test
public void testCacheDefaultBehavior() throws ExecutionException, InterruptedException {
final var cache = new CacheBuilder<String, String>().dataSource(dataSource).build();
Assert.assertNotNull(cache);
assert isEqualTo(cache.get(PROFILE_MUMBAI_ENGINEER), "violet");
assert cache.get("random")
.exceptionally(throwable -> Boolean.TRUE.toString())
.thenApply(Boolean::valueOf)
.toCompletableFuture()
.get();
assert isEqualTo(cache.set(PROFILE_MUMBAI_ENGINEER, "brown").thenCompose(__ -> cache.get(PROFILE_MUMBAI_ENGINEER)), "brown");
Assert.assertEquals(3, cache.getEventQueue().size());
assert cache.getEventQueue().get(0) instanceof Load;
assert cache.getEventQueue().get(1) instanceof Update;
assert cache.getEventQueue().get(2) instanceof Write;
}
@Test
public void Eviction_LRU() {
final var maximumSize = 2;
final var cache = new CacheBuilder<String, String>()
.maximumSize(maximumSize)
.evictionAlgorithm(EvictionAlgorithm.LRU)
.fetchAlgorithm(FetchAlgorithm.WRITE_BACK)
.dataSource(writeBackDataSource).build();
cache.get(PROFILE_MUMBAI_ENGINEER).toCompletableFuture().join();
for (int i = 0; i < maximumSize; i++) {
cache.set("key" + i, "value" + i).toCompletableFuture().join();
}
Assert.assertEquals(2, cache.getEventQueue().size());
assert cache.getEventQueue().get(0) instanceof Load;
assert cache.getEventQueue().get(1) instanceof Eviction;
final var evictionEvent = (Eviction<String, String>) cache.getEventQueue().get(1);
Assert.assertEquals(Eviction.Type.REPLACEMENT, evictionEvent.getType());
Assert.assertEquals(PROFILE_MUMBAI_ENGINEER, evictionEvent.getElement().getKey());
cache.getEventQueue().clear();
final var permutation = new ArrayList<Integer>();
for (int i = 0; i < maximumSize; i++) {
permutation.add(i);
}
Collections.shuffle(permutation);
for (final int index : permutation) {
cache.get("key" + index).toCompletableFuture().join();
}
for (int i = 0; i < maximumSize; i++) {
cache.set("random" + permutation.get(i), "random_value").toCompletableFuture().join();
assert cache.getEventQueue().get(i) instanceof Eviction;
final var eviction = (Eviction<String, String>) cache.getEventQueue().get(i);
Assert.assertEquals(Eviction.Type.REPLACEMENT, eviction.getType());
Assert.assertEquals("key" + permutation.get(i), eviction.getElement().getKey());
}
}
@Test
public void Eviction_LFU() {
final var maximumSize = 2;
final var cache = new CacheBuilder<String, String>()
.maximumSize(maximumSize)
.evictionAlgorithm(EvictionAlgorithm.LFU)
.fetchAlgorithm(FetchAlgorithm.WRITE_BACK)
.dataSource(writeBackDataSource)
.build();
cache.get(PROFILE_MUMBAI_ENGINEER).toCompletableFuture().join();
for (int i = 0; i < maximumSize; i++) {
cache.set("key" + i, "value" + i).toCompletableFuture().join();
}
Assert.assertEquals(2, cache.getEventQueue().size());
assert cache.getEventQueue().get(0) instanceof Load;
assert cache.getEventQueue().get(1) instanceof Eviction;
final var evictionEvent = (Eviction<String, String>) cache.getEventQueue().get(1);
Assert.assertEquals(Eviction.Type.REPLACEMENT, evictionEvent.getType());
Assert.assertEquals("key0", evictionEvent.getElement().getKey());
for (int i = 0; i < maximumSize; i++) {
acceptWrite();
}
final var permutation = new ArrayList<Integer>();
for (int i = 0; i < maximumSize; i++) {
permutation.add(i);
}
Collections.shuffle(permutation);
for (final int index : permutation) {
for (int i = 0; i <= index; i++) {
cache.get("key" + index).toCompletableFuture().join();
}
}
cache.getEventQueue().clear();
for (int i = 0; i < maximumSize; i++) {
cache.set("random" + i, "random_value").toCompletableFuture().join();
acceptWrite();
for (int j = 0; j <= maximumSize; j++) {
cache.get("random" + i).toCompletableFuture().join();
}
Assert.assertEquals(Eviction.class.getName(), cache.getEventQueue().get(i * 2).getClass().getName());
Assert.assertEquals(Write.class.getName(), cache.getEventQueue().get(i * 2 + 1).getClass().getName());
final var eviction = (Eviction<String, String>) cache.getEventQueue().get(i * 2);
System.out.println(cache.getEventQueue().get(i));
Assert.assertEquals(Eviction.Type.REPLACEMENT, eviction.getType());
Assert.assertEquals("key" + i, eviction.getElement().getKey());
}
}
@Test
public void ExpiryOnGet() {
final var timer = new SettableTimer();
final var startTime = System.nanoTime();
final var cache = new CacheBuilder<String, String>().timer(timer).dataSource(dataSource).expiryTime(Duration.ofSeconds(10)).build();
timer.setTime(startTime);
cache.get(PROFILE_MUMBAI_ENGINEER).toCompletableFuture().join();
Assert.assertEquals(1, cache.getEventQueue().size());
assert cache.getEventQueue().get(0) instanceof Load;
Assert.assertEquals(PROFILE_MUMBAI_ENGINEER, cache.getEventQueue().get(0).getElement().getKey());
timer.setTime(startTime + Duration.ofSeconds(10).toNanos() + 1);
cache.get(PROFILE_MUMBAI_ENGINEER).toCompletableFuture().join();
Assert.assertEquals(3, cache.getEventQueue().size());
assert cache.getEventQueue().get(1) instanceof Eviction;
assert cache.getEventQueue().get(2) instanceof Load;
final var eviction = (Eviction<String, String>) cache.getEventQueue().get(1);
Assert.assertEquals(Eviction.Type.EXPIRY, eviction.getType());
Assert.assertEquals(PROFILE_MUMBAI_ENGINEER, eviction.getElement().getKey());
}
@Test
public void ExpiryOnSet() {
final var timer = new SettableTimer();
final var startTime = System.nanoTime();
final var cache = new CacheBuilder<String, String>().timer(timer).dataSource(dataSource).expiryTime(Duration.ofSeconds(10)).build();
timer.setTime(startTime);
cache.get(PROFILE_MUMBAI_ENGINEER).toCompletableFuture().join();
Assert.assertEquals(1, cache.getEventQueue().size());
assert cache.getEventQueue().get(0) instanceof Load;
Assert.assertEquals(PROFILE_MUMBAI_ENGINEER, cache.getEventQueue().get(0).getElement().getKey());
timer.setTime(startTime + Duration.ofSeconds(10).toNanos() + 1);
cache.set(PROFILE_MUMBAI_ENGINEER, "blue").toCompletableFuture().join();
Assert.assertEquals(3, cache.getEventQueue().size());
assert cache.getEventQueue().get(1) instanceof Eviction;
assert cache.getEventQueue().get(2) instanceof Write;
final var eviction = (Eviction<String, String>) cache.getEventQueue().get(1);
Assert.assertEquals(Eviction.Type.EXPIRY, eviction.getType());
Assert.assertEquals(PROFILE_MUMBAI_ENGINEER, eviction.getElement().getKey());
}
@Test
public void ExpiryOnEviction() {
final var timer = new SettableTimer();
final var startTime = System.nanoTime();
final var cache = new CacheBuilder<String, String>().maximumSize(2).timer(timer).dataSource(dataSource).expiryTime(Duration.ofSeconds(10)).build();
timer.setTime(startTime);
cache.get(PROFILE_MUMBAI_ENGINEER).toCompletableFuture().join();
cache.get(PROFILE_HYDERABAD_ENGINEER).toCompletableFuture().join();
timer.setTime(startTime + Duration.ofSeconds(10).toNanos() + 1);
cache.set("randomKey", "randomValue").toCompletableFuture().join();
Assert.assertEquals(5, cache.getEventQueue().size());
assert cache.getEventQueue().get(2) instanceof Eviction;
assert cache.getEventQueue().get(3) instanceof Eviction;
assert cache.getEventQueue().get(4) instanceof Write;
final var eviction1 = (Eviction<String, String>) cache.getEventQueue().get(2);
Assert.assertEquals(Eviction.Type.EXPIRY, eviction1.getType());
Assert.assertEquals(PROFILE_MUMBAI_ENGINEER, eviction1.getElement().getKey());
final var eviction2 = (Eviction<String, String>) cache.getEventQueue().get(3);
Assert.assertEquals(Eviction.Type.EXPIRY, eviction2.getType());
Assert.assertEquals(PROFILE_HYDERABAD_ENGINEER, eviction2.getElement().getKey());
}
@Test
public void FetchingWriteBack() {
final var cache = new CacheBuilder<String, String>()
.maximumSize(1)
.dataSource(writeBackDataSource)
.fetchAlgorithm(FetchAlgorithm.WRITE_BACK)
.build();
cache.set("randomKey", "randomValue").toCompletableFuture().join();
Assert.assertEquals(0, cache.getEventQueue().size());
Assert.assertNull(dataMap.get("randomValue"));
acceptWrite();
}
@Test
public void FetchingWriteThrough() {
final var cache = new CacheBuilder<String, String>().dataSource(dataSource).fetchAlgorithm(FetchAlgorithm.WRITE_THROUGH).build();
cache.set("randomKey", "randomValue").toCompletableFuture().join();
Assert.assertEquals(1, cache.getEventQueue().size());
assert cache.getEventQueue().get(0) instanceof Write;
Assert.assertEquals("randomValue", dataMap.get("randomKey"));
}
@Test
public void EagerLoading() {
final var eagerlyLoad = new HashSet<String>();
eagerlyLoad.add(PROFILE_MUMBAI_ENGINEER);
eagerlyLoad.add(PROFILE_HYDERABAD_ENGINEER);
final var cache = new CacheBuilder<String, String>()
.loadKeysOnStart(eagerlyLoad)
.dataSource(dataSource)
.build();
Assert.assertEquals(2, cache.getEventQueue().size());
assert cache.getEventQueue().get(0) instanceof Load;
assert cache.getEventQueue().get(1) instanceof Load;
cache.getEventQueue().clear();
dataMap.clear();
isEqualTo(cache.get(PROFILE_MUMBAI_ENGINEER), "violet");
isEqualTo(cache.get(PROFILE_HYDERABAD_ENGINEER), "blue");
Assert.assertEquals(0, cache.getEventQueue().size());
}
@Test
public void RaceConditions() throws ExecutionException, InterruptedException {
final var cache = new CacheBuilder<String, String>()
.poolSize(8)
.dataSource(dataSource).build();
final var cacheEntries = new HashMap<String, List<String>>();
final var numberOfEntries = 100;
final var numberOfValues = 1000;
final String[] keyList = new String[numberOfEntries];
final Map<String, Integer> inverseMapping = new HashMap<>();
for (int entry = 0; entry < numberOfEntries; entry++) {
final var key = UUID.randomUUID().toString();
keyList[entry] = key;
inverseMapping.put(key, entry);
cacheEntries.put(key, new ArrayList<>());
final var firstValue = UUID.randomUUID().toString();
dataMap.put(key, firstValue);
cacheEntries.get(key).add(firstValue);
for (int value = 0; value < numberOfValues - 1; value++) {
cacheEntries.get(key).add(UUID.randomUUID().toString());
}
}
final Random random = new Random();
final List<CompletionStage<String>> futures = new ArrayList<>();
final List<String> queries = new ArrayList<>();
final int[] updates = new int[numberOfEntries];
for (int i = 0; i < 1000000; i++) {
final var index = random.nextInt(numberOfEntries);
final var key = keyList[index];
if (Math.random() <= 0.05) {
if (updates[index] - 1 < numberOfEntries) {
updates[index]++;
}
cache.set(key, cacheEntries.get(key).get(updates[index] + 1));
} else {
queries.add(key);
futures.add(cache.get(key));
}
}
final CompletionStage<List<String>> results = CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new))
.thenApply(__ -> futures.stream()
.map(CompletionStage::toCompletableFuture)
.map(CompletableFuture::join)
.collect(Collectors.toList()));
final int[] currentIndexes = new int[numberOfEntries];
final StringBuilder stringBuilder = new StringBuilder();
results.thenAccept(values -> {
for (int i = 0; i < values.size(); i++) {
final var key = queries.get(i);
final var possibleValuesForKey = cacheEntries.get(key);
final var currentValue = currentIndexes[inverseMapping.get(key)];
if (!possibleValuesForKey.get(currentValue).equals(values.get(i))) {
int offset = 1;
while (currentValue + offset < numberOfValues && !possibleValuesForKey.get(currentValue + offset).equals(values.get(i))) {
offset++;
}
if (currentValue + offset == numberOfValues) {
System.out.println(Arrays.stream(stringBuilder.toString().split("\n")).filter(line -> line.contains(key)).collect(Collectors.joining("\n")));
System.err.println(key);
System.err.println(possibleValuesForKey);
System.err.println(possibleValuesForKey.get(currentValue) + " index: " + currentIndexes[inverseMapping.get(key)]);
System.err.println(values.get(i));
throw new IllegalStateException();
}
currentIndexes[inverseMapping.get(key)] += offset;
stringBuilder.append(key).append(" index: ").append(currentIndexes[inverseMapping.get(key)]).append(" ").append(values.get(i)).append('\n');
}
}
}).toCompletableFuture().join();
}
private boolean isEqualTo(CompletionStage<String> future, String value) {
return future.thenApply(result -> {
if (result.equals(value)) {
return true;
} else {
throw new AssertionError();
}
}).toCompletableFuture().join();
}
}

View File

@@ -0,0 +1,14 @@
package models;
public class SettableTimer extends Timer {
private long time = -1;
@Override
public long getCurrentTime() {
return time == -1 ? System.nanoTime() : time;
}
public void setTime(long time) {
this.time = time;
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.