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.

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

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

16
distributed-event-bus/.idea/compiler.xml generated Normal file
View File

@ -0,0 +1,16 @@
<?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="distributed-event-bus" />
</profile>
</annotationProcessing>
<bytecodeTargetLevel>
<module name="distributed-event-bus" target="11" />
</bytecodeTargetLevel>
</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>

View File

@ -0,0 +1,13 @@
<component name="libraryTable">
<library name="Maven: aopalliance:aopalliance:1.0">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/aopalliance/aopalliance/1.0/aopalliance-1.0.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/aopalliance/aopalliance/1.0/aopalliance-1.0-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/aopalliance/aopalliance/1.0/aopalliance-1.0-sources.jar!/" />
</SOURCES>
</library>
</component>

View File

@ -0,0 +1,13 @@
<component name="libraryTable">
<library name="Maven: com.google.code.gson:gson:2.8.6">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/com/google/code/gson/gson/2.8.6/gson-2.8.6.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/com/google/code/gson/gson/2.8.6/gson-2.8.6-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/com/google/code/gson/gson/2.8.6/gson-2.8.6-sources.jar!/" />
</SOURCES>
</library>
</component>

View File

@ -0,0 +1,13 @@
<component name="libraryTable">
<library name="Maven: com.google.guava:guava:16.0.1">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/com/google/guava/guava/16.0.1/guava-16.0.1.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/com/google/guava/guava/16.0.1/guava-16.0.1-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/com/google/guava/guava/16.0.1/guava-16.0.1-sources.jar!/" />
</SOURCES>
</library>
</component>

View File

@ -0,0 +1,13 @@
<component name="libraryTable">
<library name="Maven: com.google.inject:guice:4.0">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/com/google/inject/guice/4.0/guice-4.0.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/com/google/inject/guice/4.0/guice-4.0-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/com/google/inject/guice/4.0/guice-4.0-sources.jar!/" />
</SOURCES>
</library>
</component>

View File

@ -0,0 +1,13 @@
<component name="libraryTable">
<library name="Maven: javax.inject:javax.inject:1">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/javax/inject/javax.inject/1/javax.inject-1.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/javax/inject/javax.inject/1/javax.inject-1-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/javax/inject/javax.inject/1/javax.inject-1-sources.jar!/" />
</SOURCES>
</library>
</component>

View File

@ -0,0 +1,13 @@
<component name="libraryTable">
<library name="Maven: junit:junit:4.13">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/junit/junit/4.13/junit-4.13.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/junit/junit/4.13/junit-4.13-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/junit/junit/4.13/junit-4.13-sources.jar!/" />
</SOURCES>
</library>
</component>

View File

@ -0,0 +1,13 @@
<component name="libraryTable">
<library name="Maven: org.hamcrest:hamcrest-core:1.3">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3-sources.jar!/" />
</SOURCES>
</library>
</component>

13
distributed-event-bus/.idea/misc.xml generated Normal file
View File

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

8
distributed-event-bus/.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/distributed-event-bus.iml" filepath="$PROJECT_DIR$/distributed-event-bus.iml" />
</modules>
</component>
</project>

124
distributed-event-bus/.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-event-bus/.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,8 @@
1) Multiple publishers and subscribers (Register from any class to eventbus)
2) Causal ordering of topics
3) Supports configurable retry attempts.
4) Have a dead letter queue.
5) Idempotency on event receiving
6) Allow both pull and push models
7) Allow subscribing from a timestamp or offset
8) Allow preconditions for event subscription

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<module org.jetbrains.idea.maven.project.MavenProjectsManager.isMavenModule="true" type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_11">
<output url="file://$MODULE_DIR$/target/classes" />
<output-test url="file://$MODULE_DIR$/target/test-classes" />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" scope="TEST" name="Maven: junit:junit:4.13" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.hamcrest:hamcrest-core:1.3" level="project" />
<orderEntry type="library" name="Maven: com.google.inject:guice:4.0" level="project" />
<orderEntry type="library" name="Maven: javax.inject:javax.inject:1" level="project" />
<orderEntry type="library" name="Maven: aopalliance:aopalliance:1.0" level="project" />
<orderEntry type="library" name="Maven: com.google.guava:guava:16.0.1" level="project" />
<orderEntry type="library" name="Maven: com.google.code.gson:gson:2.8.6" level="project" />
</component>
</module>

View File

@ -0,0 +1,42 @@
<?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-event-bus</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>
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
<version>4.0</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.6</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,203 @@
import com.google.inject.Inject;
import com.google.inject.Singleton;
import exceptions.RetryLimitExceededException;
import exceptions.UnsubscribedPollException;
import lib.KeyedExecutor;
import models.Event;
import models.FailureEvent;
import models.Subscription;
import util.Timer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.*;
import java.util.function.Function;
import java.util.function.Predicate;
@Singleton
public class EventBus {
private final Map<String, List<Event>> topics;
private final Map<String, Map<String, Integer>> eventIndexes;
private final Map<String, ConcurrentSkipListMap<Long, String>> eventTimestamps;
private final Map<String, Map<String, Subscription>> pullSubscriptions;
private final Map<String, Map<String, Subscription>> pushSubscriptions;
private final KeyedExecutor<String> eventExecutor;
private final KeyedExecutor<String> broadcastExecutor;
private EventBus deadLetterQueue;
private final Timer timer;
@Inject
public EventBus(final KeyedExecutor<String> eventExecutor, final KeyedExecutor<String> broadcastExecutor, final Timer timer) {
this.topics = new ConcurrentHashMap<>();
this.eventIndexes = new ConcurrentHashMap<>();
this.eventTimestamps = new ConcurrentHashMap<>();
this.pullSubscriptions = new ConcurrentHashMap<>();
this.pushSubscriptions = new ConcurrentHashMap<>();
this.eventExecutor = eventExecutor;
this.broadcastExecutor = broadcastExecutor;
this.timer = timer;
}
public void setDeadLetterQueue(final EventBus deadLetterQueue) {
this.deadLetterQueue = deadLetterQueue;
}
public CompletionStage<Void> publish(final String topic, final Event event) {
return eventExecutor.getThreadFor(topic, publishToBus(topic, event));
}
private CompletionStage<Void> publishToBus(final String topic, final Event event) {
if (eventIndexes.containsKey(topic) && eventIndexes.get(topic).containsKey(event.getId())) {
return null;
}
topics.putIfAbsent(topic, new CopyOnWriteArrayList<>());
eventIndexes.putIfAbsent(topic, new ConcurrentHashMap<>());
eventIndexes.get(topic).put(event.getId(), topics.get(topic).size());
eventTimestamps.putIfAbsent(topic, new ConcurrentSkipListMap<>());
eventTimestamps.get(topic).put(timer.getCurrentTime(), event.getId());
topics.get(topic).add(event);
return notifyPushSubscribers(topic, event);
}
private CompletionStage<Void> notifyPushSubscribers(String topic, Event event) {
if (!pushSubscriptions.containsKey(topic)) {
return CompletableFuture.completedStage(null);
}
final var subscribersForTopic = pushSubscriptions.get(topic);
final var notifications = subscribersForTopic.values()
.stream()
.filter(subscription -> subscription.getPrecondition().test(event))
.map(subscription -> executeEventHandler(event, subscription))
.toArray(CompletableFuture[]::new);
return CompletableFuture.allOf(notifications);
}
private CompletionStage<Void> executeEventHandler(final Event event, Subscription subscription) {
return broadcastExecutor.getThreadFor(subscription.getTopic() + subscription.getSubscriber(),
doWithRetry(event, subscription.getEventHandler(),
1, subscription.getNumberOfRetries())
.exceptionally(throwable -> {
if (deadLetterQueue != null) {
deadLetterQueue.publish(subscription.getTopic(), new FailureEvent(event, throwable, timer.getCurrentTime()));
}
return null;
}));
}
private CompletionStage<Void> doWithRetry(final Event event,
final Function<Event, CompletionStage<Void>> task,
final int coolDownIntervalInMillis,
final int remainingTries) {
return task.apply(event).handle((__, throwable) -> {
if (throwable != null) {
if (remainingTries == 1) {
throw new RetryLimitExceededException(throwable);
}
try {
Thread.sleep(coolDownIntervalInMillis);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return doWithRetry(event, task, Math.max(coolDownIntervalInMillis * 2, 10), remainingTries - 1);
} else {
return CompletableFuture.completedFuture((Void) null);
}
}).thenCompose(Function.identity());
}
public CompletionStage<Event> poll(final String topic, final String subscriber) {
return eventExecutor.getThreadFor(topic + subscriber, () -> pollBus(topic, subscriber));
}
private Event pollBus(final String topic, final String subscriber) {
var subscription = pullSubscriptions.getOrDefault(topic, new HashMap<>()).get(subscriber);
if (subscription == null) {
throw new UnsubscribedPollException();
}
for (var index = subscription.getCurrentIndex(); index.intValue() < topics.get(topic).size(); index.increment()) {
var event = topics.get(topic).get(index.intValue());
if (subscription.getPrecondition().test(event)) {
index.increment();
return event;
}
}
return null;
}
public CompletionStage<Void> subscribeToEventsAfter(final String topic, final String subscriber, final long timeStamp) {
return eventExecutor.getThreadFor(topic + subscriber, () -> moveIndexAtTimestamp(topic, subscriber, timeStamp));
}
private void moveIndexAtTimestamp(final String topic, final String subscriber, final long timeStamp) {
final var closestEventAfter = eventTimestamps.get(topic).higherEntry(timeStamp);
if (closestEventAfter == null) {
pullSubscriptions.get(topic).get(subscriber).setCurrentIndex(eventIndexes.get(topic).size());
} else {
final var eventIndex = eventIndexes.get(topic).get(closestEventAfter.getValue());
pullSubscriptions.get(topic).get(subscriber).setCurrentIndex(eventIndex);
}
}
public CompletionStage<Void> subscribeToEventsAfter(final String topic, final String subscriber, final String eventId) {
return eventExecutor.getThreadFor(topic + subscriber, () -> moveIndexAfterEvent(topic, subscriber, eventId));
}
private void moveIndexAfterEvent(final String topic, final String subscriber, final String eventId) {
if (eventId == null) {
pullSubscriptions.get(topic).get(subscriber).setCurrentIndex(0);
} else {
final var eventIndex = eventIndexes.get(topic).get(eventId) + 1;
pullSubscriptions.get(topic).get(subscriber).setCurrentIndex(eventIndex);
}
}
public CompletionStage<Void> subscribeForPush(final String topic,
final String subscriber,
final Predicate<Event> precondition,
final Function<Event, CompletionStage<Void>> handler,
final int numberOfRetries) {
return eventExecutor.getThreadFor(topic + subscriber,
() -> subscribeForPushEvents(topic, subscriber, precondition, handler, numberOfRetries));
}
private void subscribeForPushEvents(final String topic,
final String subscriber,
final Predicate<Event> precondition,
final Function<Event, CompletionStage<Void>> handler,
final int numberOfRetries) {
addSubscriber(pushSubscriptions, subscriber, precondition, topic, handler, numberOfRetries);
}
private void addSubscriber(final Map<String, Map<String, Subscription>> pullSubscriptions,
final String subscriber,
final Predicate<Event> precondition,
final String topic,
final Function<Event, CompletionStage<Void>> handler,
final int numberOfRetries) {
pullSubscriptions.putIfAbsent(topic, new ConcurrentHashMap<>());
final var subscription = new Subscription(topic, subscriber, precondition, handler, numberOfRetries);
subscription.setCurrentIndex(topics.getOrDefault(topic, new ArrayList<>()).size());
pullSubscriptions.get(topic).put(subscriber, subscription);
}
public CompletionStage<Void> subscribeForPull(final String topic, final String subscriber, final Predicate<Event> precondition) {
return eventExecutor.getThreadFor(topic + subscriber, () -> subscribeForPullEvents(topic, subscriber, precondition));
}
private void subscribeForPullEvents(final String topic, final String subscriber, final Predicate<Event> precondition) {
addSubscriber(pullSubscriptions, subscriber, precondition, topic, null, 0);
}
public CompletionStage<Void> unsubscribe(final String topic, final String subscriber) {
return eventExecutor.getThreadFor(topic + subscriber, () -> unsubscribeFromTopic(topic, subscriber));
}
private void unsubscribeFromTopic(final String topic, final String subscriber) {
pushSubscriptions.getOrDefault(topic, new HashMap<>()).remove(subscriber);
pullSubscriptions.getOrDefault(topic, new HashMap<>()).remove(subscriber);
}
}

View File

@ -0,0 +1,7 @@
package exceptions;
public class RetryLimitExceededException extends RuntimeException {
public RetryLimitExceededException(Throwable cause) {
super(cause);
}
}

View File

@ -0,0 +1,4 @@
package exceptions;
public class UnsubscribedPollException extends RuntimeException {
}

View File

@ -0,0 +1,31 @@
package lib;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.function.Function;
import java.util.function.Supplier;
public class KeyedExecutor<KEY> {
private final Executor[] executorPool;
public KeyedExecutor(final int poolSize) {
this.executorPool = new Executor[poolSize];
for (int i = 0; i < poolSize; i++) {
executorPool[i] = Executors.newSingleThreadExecutor();
}
}
public CompletionStage<Void> getThreadFor(KEY key, Runnable task) {
return CompletableFuture.runAsync(task, executorPool[Math.abs(key.hashCode() % executorPool.length)]);
}
public <U> CompletionStage<U> getThreadFor(KEY key, Supplier<U> task) {
return CompletableFuture.supplyAsync(task, executorPool[Math.abs(key.hashCode() % executorPool.length)]);
}
public <U> CompletionStage<U> getThreadFor(KEY key, CompletionStage<U> task) {
return CompletableFuture.supplyAsync(() -> task, executorPool[Math.abs(key.hashCode() % executorPool.length)]).thenCompose(Function.identity());
}
}

View File

@ -0,0 +1,43 @@
package models;
import java.util.Objects;
import java.util.UUID;
public class Event {
private final String id;
private final String publisher;
private final EventType eventType;
private final String description;
private final long creationTime;
public Event(final String publisher,
final EventType eventType,
final String description,
final long creationTime) {
this.description = description;
this.id = UUID.randomUUID().toString();
this.publisher = publisher;
this.eventType = eventType;
this.creationTime = creationTime;
}
public String getId() {
return id;
}
public String getPublisher() {
return publisher;
}
public EventType getEventType() {
return eventType;
}
public long getCreationTime() {
return creationTime;
}
public String getDescription() {
return description;
}
}

View File

@ -0,0 +1,5 @@
package models;
public enum EventType {
PRIORITY, LOGGING, ERROR
}

View File

@ -0,0 +1,20 @@
package models;
public class FailureEvent extends Event {
private final Event event;
private final Throwable throwable;
public FailureEvent(Event event, Throwable throwable, long failureTimestamp) {
super("dead-letter-queue", EventType.ERROR, throwable.getMessage(), failureTimestamp);
this.event = event;
this.throwable = throwable;
}
public Event getEvent() {
return event;
}
public Throwable getThrowable() {
return throwable;
}
}

View File

@ -0,0 +1,57 @@
package models;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.atomic.LongAdder;
import java.util.function.Function;
import java.util.function.Predicate;
public class Subscription {
private final String topic;
private final String subscriber;
private final Predicate<Event> precondition;
private final Function<Event, CompletionStage<Void>> eventHandler;
private final int numberOfRetries;
private final LongAdder currentIndex;
public Subscription(final String topic,
final String subscriber,
final Predicate<Event> precondition,
final Function<Event, CompletionStage<Void>> eventHandler,
final int numberOfRetries) {
this.topic = topic;
this.subscriber = subscriber;
this.precondition = precondition;
this.eventHandler = eventHandler;
this.currentIndex = new LongAdder();
this.numberOfRetries = numberOfRetries;
}
public String getTopic() {
return topic;
}
public String getSubscriber() {
return subscriber;
}
public Predicate<Event> getPrecondition() {
return precondition;
}
public Function<Event, CompletionStage<Void>> getEventHandler() {
return eventHandler;
}
public LongAdder getCurrentIndex() {
return currentIndex;
}
public void setCurrentIndex(final int offset) {
currentIndex.reset();
currentIndex.add(offset);
}
public int getNumberOfRetries() {
return numberOfRetries;
}
}

View File

@ -0,0 +1,10 @@
package util;
import com.google.inject.Singleton;
@Singleton
public class Timer {
public long getCurrentTime() {
return System.nanoTime();
}
}

View File

@ -0,0 +1,340 @@
import com.google.gson.Gson;
import exceptions.RetryLimitExceededException;
import exceptions.UnsubscribedPollException;
import lib.KeyedExecutor;
import models.Event;
import models.EventType;
import models.FailureEvent;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import util.Timer;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicLong;
// Causal ordering of topics
public class EventBusTest {
public static final String TOPIC_1 = "topic-1";
public static final String TOPIC_2 = "topic-2";
public static final String PUBLISHER_1 = "publisher-1";
public static final String SUBSCRIBER_1 = "subscriber-1";
public static final String SUBSCRIBER_2 = "subscriber-2";
private Timer timer;
private KeyedExecutor<String> keyedExecutor;
private KeyedExecutor<String> broadcastExecutor;
@Before
public void setUp() {
keyedExecutor = new KeyedExecutor<>(16);
broadcastExecutor = new KeyedExecutor<>(16);
timer = new Timer();
}
private Event constructEvent(EventType priority, String description) {
return new Event(PUBLISHER_1, priority, description, timer.getCurrentTime());
}
@Test
public void defaultBehavior() {
final EventBus eventBus = new EventBus(keyedExecutor, broadcastExecutor, timer);
eventBus.publish(TOPIC_1, constructEvent(EventType.LOGGING, "first event"));
eventBus.subscribeForPull(TOPIC_1, SUBSCRIBER_1, (event) -> true).toCompletableFuture().join();
Assert.assertNull(eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join());
eventBus.publish(TOPIC_1, constructEvent(EventType.PRIORITY, "second event"));
final Event secondEvent = eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join();
Assert.assertEquals(EventType.PRIORITY, secondEvent.getEventType());
Assert.assertEquals("second event", secondEvent.getDescription());
eventBus.subscribeToEventsAfter(TOPIC_1, SUBSCRIBER_1, null).toCompletableFuture().join();
final Event firstEvent = eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join();
Assert.assertEquals(EventType.LOGGING, firstEvent.getEventType());
Assert.assertEquals("first event", firstEvent.getDescription());
Assert.assertEquals(PUBLISHER_1, firstEvent.getPublisher());
final List<Event> eventCollector = new ArrayList<>();
eventBus.subscribeForPush(TOPIC_1,
SUBSCRIBER_2,
(event) -> true,
(event) -> CompletableFuture.runAsync(() -> eventCollector.add(event)),
0).toCompletableFuture().join();
eventBus.publish(TOPIC_1, constructEvent(EventType.ERROR, "third event")).toCompletableFuture().join();
Assert.assertEquals(EventType.ERROR, eventCollector.get(0).getEventType());
Assert.assertEquals("third event", eventCollector.get(0).getDescription());
eventBus.unsubscribe(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join();
eventBus.publish(TOPIC_1, constructEvent(EventType.LOGGING, "fourth event")).toCompletableFuture().join();
Assert.assertTrue(eventBus.poll(TOPIC_1, SUBSCRIBER_1)
.handle((__, throwable) -> throwable.getCause() instanceof UnsubscribedPollException)
.toCompletableFuture().join());
eventCollector.clear();
eventBus.unsubscribe(TOPIC_1, SUBSCRIBER_2).toCompletableFuture().join();
eventBus.publish(TOPIC_1, constructEvent(EventType.LOGGING, "fifth event")).toCompletableFuture().join();
Assert.assertTrue(eventCollector.isEmpty());
}
@Test
public void indexMove() {
final EventBus eventBus = new EventBus(keyedExecutor, broadcastExecutor, timer);
eventBus.subscribeForPull(TOPIC_1, SUBSCRIBER_1, (event) -> true).toCompletableFuture().join();
final Event firstEvent = constructEvent(EventType.PRIORITY, "first event");
final Event secondEvent = constructEvent(EventType.PRIORITY, "second event");
final Event thirdEvent = constructEvent(EventType.PRIORITY, "third event");
eventBus.publish(TOPIC_1, firstEvent).toCompletableFuture().join();
eventBus.publish(TOPIC_1, secondEvent).toCompletableFuture().join();
eventBus.publish(TOPIC_1, thirdEvent).toCompletableFuture().join();
eventBus.subscribeToEventsAfter(TOPIC_1, SUBSCRIBER_1, secondEvent.getId()).toCompletableFuture().join();
final Event firstPoll = eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join();
Assert.assertEquals("third event", firstPoll.getDescription());
Assert.assertNull(eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join());
eventBus.subscribeToEventsAfter(TOPIC_1, SUBSCRIBER_1, null).toCompletableFuture().join();
final Event secondPoll = eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join();
Assert.assertEquals("first event", secondPoll.getDescription());
eventBus.subscribeToEventsAfter(TOPIC_1, SUBSCRIBER_1, firstEvent.getId()).toCompletableFuture().join();
final Event thirdPoll = eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join();
Assert.assertEquals("second event", thirdPoll.getDescription());
eventBus.subscribeToEventsAfter(TOPIC_1, SUBSCRIBER_1, thirdEvent.getId()).toCompletableFuture().join();
Assert.assertNull(eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join());
}
@Test
public void timestampMove() {
final TestTimer timer = new TestTimer();
final EventBus eventBus = new EventBus(keyedExecutor, broadcastExecutor, timer);
eventBus.subscribeForPull(TOPIC_1, SUBSCRIBER_1, (event) -> true).toCompletableFuture().join();
final Event firstEvent = new Event(PUBLISHER_1, EventType.PRIORITY, "first event", timer.getCurrentTime());
eventBus.publish(TOPIC_1, firstEvent).toCompletableFuture().join();
timer.setCurrentTime(timer.getCurrentTime() + Duration.ofSeconds(10).toNanos());
final Event secondEvent = new Event(PUBLISHER_1, EventType.PRIORITY, "second event", timer.getCurrentTime());
eventBus.publish(TOPIC_1, secondEvent).toCompletableFuture().join();
timer.setCurrentTime(timer.getCurrentTime() + Duration.ofSeconds(10).toNanos());
final Event thirdEvent = new Event(PUBLISHER_1, EventType.PRIORITY, "third event", timer.getCurrentTime());
eventBus.publish(TOPIC_1, thirdEvent).toCompletableFuture().join();
eventBus.subscribeToEventsAfter(TOPIC_1, SUBSCRIBER_1, secondEvent.getCreationTime() + Duration.ofSeconds(5).toNanos()).toCompletableFuture().join();
final Event firstPoll = eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join();
Assert.assertEquals("third event", firstPoll.getDescription());
Assert.assertNull(eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join());
eventBus.subscribeToEventsAfter(TOPIC_1, SUBSCRIBER_1, 0).toCompletableFuture().join();
final Event secondPoll = eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join();
Assert.assertEquals("first event", secondPoll.getDescription());
eventBus.subscribeToEventsAfter(TOPIC_1, SUBSCRIBER_1, firstEvent.getCreationTime() + Duration.ofSeconds(5).toNanos()).toCompletableFuture().join();
final Event thirdPoll = eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join();
Assert.assertEquals("second event", thirdPoll.getDescription());
eventBus.subscribeToEventsAfter(TOPIC_1, SUBSCRIBER_1, thirdEvent.getCreationTime() + Duration.ofNanos(1).toNanos()).toCompletableFuture().join();
Assert.assertNull(eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join());
}
@Test
public void idempotency() {
final EventBus eventBus = new EventBus(keyedExecutor, broadcastExecutor, timer);
eventBus.subscribeForPull(TOPIC_1, SUBSCRIBER_1, (event) -> true).toCompletableFuture().join();
Event event1 = new Gson().fromJson("{\n" +
" \"id\": \"event-5435\",\n" +
" \"publisher\": \"random-publisher-1\",\n" +
" \"eventType\": \"LOGGING\",\n" +
" \"description\": \"random-event-1\",\n" +
" \"creationTime\": 31884739810179363\n" +
"}", Event.class);
eventBus.publish(TOPIC_1, event1);
Event event2 = new Gson().fromJson("{\n" +
" \"id\": \"event-5435\",\n" +
" \"publisher\": \"random-publisher-2\",\n" +
" \"eventType\": \"PRIORITY\",\n" +
" \"description\": \"random-event-2\",\n" +
" \"creationTime\": 31824735510179363\n" +
"}", Event.class);
eventBus.publish(TOPIC_1, event2);
final Event firstEvent = eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join();
Assert.assertEquals(EventType.LOGGING, firstEvent.getEventType());
Assert.assertEquals("random-event-1", firstEvent.getDescription());
Assert.assertNull(eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join());
}
@Test
public void unsubscribePushEvents() {
final EventBus eventBus = new EventBus(keyedExecutor, broadcastExecutor, timer);
final List<Event> topic1 = new ArrayList<>(), topic2 = new ArrayList<>();
eventBus.subscribeForPush(TOPIC_1, SUBSCRIBER_1, event -> true, event -> {
topic1.add(event);
return CompletableFuture.completedStage(null);
}, 0).toCompletableFuture().join();
eventBus.subscribeForPush(TOPIC_2, SUBSCRIBER_1, event -> true, event -> {
topic2.add(event);
return CompletableFuture.completedStage(null);
}, 0).toCompletableFuture().join();
for (int i = 0; i < 3; i++) {
eventBus.publish(TOPIC_1, constructEvent(EventType.PRIORITY, UUID.randomUUID().toString())).toCompletableFuture().join();
}
eventBus.publish(TOPIC_2, constructEvent(EventType.PRIORITY, UUID.randomUUID().toString())).toCompletableFuture().join();
eventBus.unsubscribe(TOPIC_1, SUBSCRIBER_1);
Assert.assertEquals(3, topic1.size());
Assert.assertEquals(1, topic2.size());
for (int i = 0; i < 2; i++) {
eventBus.publish(TOPIC_2, constructEvent(EventType.PRIORITY, UUID.randomUUID().toString())).toCompletableFuture().join();
}
for (int i = 0; i < 3; i++) {
eventBus.publish(TOPIC_1, constructEvent(EventType.PRIORITY, UUID.randomUUID().toString())).toCompletableFuture().join();
}
Assert.assertEquals(3, topic1.size());
Assert.assertEquals(3, topic2.size());
eventBus.subscribeForPush(TOPIC_1, SUBSCRIBER_1, event -> true, event -> {
topic1.add(event);
return CompletableFuture.completedStage(null);
}, 0).toCompletableFuture().join();
for (int i = 0; i < 3; i++) {
eventBus.publish(TOPIC_1, constructEvent(EventType.PRIORITY, UUID.randomUUID().toString())).toCompletableFuture().join();
}
Assert.assertEquals(6, topic1.size());
Assert.assertEquals(3, topic2.size());
}
@Test
public void unsubscribePullEvents() {
final EventBus eventBus = new EventBus(keyedExecutor, broadcastExecutor, timer);
eventBus.subscribeForPull(TOPIC_1, SUBSCRIBER_1, event -> true).toCompletableFuture().join();
eventBus.subscribeForPull(TOPIC_2, SUBSCRIBER_1, event -> true).toCompletableFuture().join();
for (int i = 0; i < 3; i++) {
eventBus.publish(TOPIC_1, constructEvent(EventType.PRIORITY, UUID.randomUUID().toString())).toCompletableFuture().join();
}
eventBus.publish(TOPIC_2, constructEvent(EventType.PRIORITY, UUID.randomUUID().toString())).toCompletableFuture().join();
for (int i = 0; i < 3; i++) {
Assert.assertNotNull(eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join());
}
Assert.assertNotNull(eventBus.poll(TOPIC_2, SUBSCRIBER_1).toCompletableFuture().join());
eventBus.unsubscribe(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join();
for (int i = 0; i < 2; i++) {
eventBus.publish(TOPIC_2, constructEvent(EventType.PRIORITY, UUID.randomUUID().toString())).toCompletableFuture().join();
}
for (int i = 0; i < 3; i++) {
eventBus.publish(TOPIC_1, constructEvent(EventType.PRIORITY, UUID.randomUUID().toString())).toCompletableFuture().join();
}
Assert.assertTrue(eventBus.poll(TOPIC_1, SUBSCRIBER_1)
.handle((__, throwable) -> throwable.getCause() instanceof UnsubscribedPollException).toCompletableFuture().join());
for (int i = 0; i < 2; i++) {
Assert.assertNotNull(eventBus.poll(TOPIC_2, SUBSCRIBER_1).toCompletableFuture().join());
}
eventBus.subscribeForPull(TOPIC_1, SUBSCRIBER_1, event -> true).toCompletableFuture().join();
for (int i = 0; i < 3; i++) {
eventBus.publish(TOPIC_1, constructEvent(EventType.PRIORITY, UUID.randomUUID().toString())).toCompletableFuture().join();
}
for (int i = 0; i < 3; i++) {
Assert.assertNotNull(eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join());
}
Assert.assertNull(eventBus.poll(TOPIC_2, SUBSCRIBER_1).toCompletableFuture().join());
}
@Test
public void deadLetterQueue() {
final EventBus eventBus = new EventBus(keyedExecutor, broadcastExecutor, timer);
final EventBus dlq = new EventBus(new KeyedExecutor<>(3), new KeyedExecutor<>(3), new Timer());
eventBus.setDeadLetterQueue(dlq);
dlq.subscribeForPull(TOPIC_1, SUBSCRIBER_1, event -> event.getEventType().equals(EventType.ERROR));
final AtomicLong attempts = new AtomicLong();
final int maxTries = 5;
eventBus.subscribeForPush(TOPIC_1, SUBSCRIBER_1, event -> true, event -> {
attempts.incrementAndGet();
return CompletableFuture.failedStage(new RuntimeException());
}, maxTries).toCompletableFuture().join();
final Event event = new Event(PUBLISHER_1, EventType.LOGGING, "random", timer.getCurrentTime());
eventBus.publish(TOPIC_1, event).toCompletableFuture().join();
Assert.assertEquals(5, attempts.intValue());
final Event failureEvent = dlq.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join();
Assert.assertTrue(failureEvent instanceof FailureEvent);
Assert.assertEquals(event.getId(), ((FailureEvent) failureEvent).getEvent().getId());
Assert.assertEquals(EventType.ERROR, failureEvent.getEventType());
Assert.assertTrue(((FailureEvent) failureEvent).getThrowable().getCause() instanceof RetryLimitExceededException);
}
@Test
public void retrySuccess() {
final EventBus eventBus = new EventBus(keyedExecutor, broadcastExecutor, timer);
final AtomicLong attempts = new AtomicLong();
final int maxTries = 5;
final List<Event> events = new ArrayList<>();
eventBus.subscribeForPush(TOPIC_1, SUBSCRIBER_1, event -> true, event -> {
if (attempts.incrementAndGet() == maxTries) {
events.add(event);
return CompletableFuture.completedStage(null);
} else {
return CompletableFuture.failedStage(new RuntimeException("TRY no: " + attempts.intValue()));
}
}, maxTries).toCompletableFuture().join();
eventBus.publish(TOPIC_1, new Event(PUBLISHER_1, EventType.LOGGING, "random", timer.getCurrentTime())).toCompletableFuture().join();
Assert.assertEquals(EventType.LOGGING, events.get(0).getEventType());
Assert.assertEquals("random", events.get(0).getDescription());
Assert.assertEquals(5, attempts.intValue());
Assert.assertEquals(1, events.size());
}
@Test
public void preconditionCheckForPush() {
final EventBus eventBus = new EventBus(keyedExecutor, broadcastExecutor, timer);
final List<Event> events = new ArrayList<>();
eventBus.subscribeForPush(TOPIC_1, SUBSCRIBER_1, event -> event.getDescription().contains("-1"), event -> {
events.add(event);
return CompletableFuture.completedStage(null);
}, 0).toCompletableFuture().join();
eventBus.publish(TOPIC_1, new Event(PUBLISHER_1, EventType.LOGGING, "random-event-1", timer.getCurrentTime())).toCompletableFuture().join();
eventBus.publish(TOPIC_1, new Event(PUBLISHER_1, EventType.LOGGING, "random-event-2", timer.getCurrentTime())).toCompletableFuture().join();
eventBus.publish(TOPIC_1, new Event(PUBLISHER_1, EventType.LOGGING, "random-event-12", timer.getCurrentTime())).toCompletableFuture().join();
eventBus.publish(TOPIC_1, new Event(PUBLISHER_1, EventType.LOGGING, "random-event-21", timer.getCurrentTime())).toCompletableFuture().join();
Assert.assertEquals(events.size(), 2);
Assert.assertEquals(EventType.LOGGING, events.get(0).getEventType());
Assert.assertEquals("random-event-1", events.get(0).getDescription());
Assert.assertEquals(EventType.LOGGING, events.get(1).getEventType());
Assert.assertEquals("random-event-12", events.get(1).getDescription());
}
@Test
public void preconditionCheckForPull() {
final EventBus eventBus = new EventBus(keyedExecutor, broadcastExecutor, timer);
eventBus.subscribeForPull(TOPIC_1, SUBSCRIBER_1, event -> event.getDescription().contains("-1")).toCompletableFuture().join();
eventBus.publish(TOPIC_1, new Event(PUBLISHER_1, EventType.LOGGING, "random-event-1", timer.getCurrentTime())).toCompletableFuture().join();
eventBus.publish(TOPIC_1, new Event(PUBLISHER_1, EventType.LOGGING, "random-event-2", timer.getCurrentTime())).toCompletableFuture().join();
eventBus.publish(TOPIC_1, new Event(PUBLISHER_1, EventType.LOGGING, "random-event-12", timer.getCurrentTime())).toCompletableFuture().join();
eventBus.publish(TOPIC_1, new Event(PUBLISHER_1, EventType.LOGGING, "random-event-21", timer.getCurrentTime())).toCompletableFuture().join();
final Event event1 = eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join();
Assert.assertEquals(EventType.LOGGING, event1.getEventType());
Assert.assertEquals("random-event-1", event1.getDescription());
final Event event2 = eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join();
Assert.assertEquals(EventType.LOGGING, event2.getEventType());
Assert.assertEquals("random-event-12", event2.getDescription());
Assert.assertNull(eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join());
}
}

View File

@ -0,0 +1,18 @@
import util.Timer;
public class TestTimer extends Timer {
private long currentTime;
public TestTimer() {
this.currentTime = System.nanoTime();
}
@Override
public long getCurrentTime() {
return currentTime;
}
public void setCurrentTime(final long currentTime) {
this.currentTime = currentTime;
}
}

Binary file not shown.

Binary file not shown.

3
rate-limiter/.idea/.gitignore generated vendored Normal file
View File

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

16
rate-limiter/.idea/compiler.xml generated Normal file
View File

@ -0,0 +1,16 @@
<?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="rate-limiter" />
</profile>
</annotationProcessing>
<bytecodeTargetLevel>
<module name="rate-limiter" target="10" />
</bytecodeTargetLevel>
</component>
</project>

20
rate-limiter/.idea/jarRepositories.xml generated Normal file
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>

View File

@ -0,0 +1,13 @@
<component name="libraryTable">
<library name="Maven: junit:junit:4.13">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/junit/junit/4.13/junit-4.13.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/junit/junit/4.13/junit-4.13-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/junit/junit/4.13/junit-4.13-sources.jar!/" />
</SOURCES>
</library>
</component>

View File

@ -0,0 +1,13 @@
<component name="libraryTable">
<library name="Maven: org.hamcrest:hamcrest-core:1.3">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3-sources.jar!/" />
</SOURCES>
</library>
</component>

13
rate-limiter/.idea/misc.xml generated Normal file
View File

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

8
rate-limiter/.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/rate-limiter.iml" filepath="$PROJECT_DIR$/rate-limiter.iml" />
</modules>
</component>
</project>

124
rate-limiter/.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
rate-limiter/.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>

31
rate-limiter/pom.xml Normal file
View File

@ -0,0 +1,31 @@
<?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>rate-limiter</artifactId>
<version>1.0</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>10</source>
<target>10</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,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<module org.jetbrains.idea.maven.project.MavenProjectsManager.isMavenModule="true" type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_10">
<output url="file://$MODULE_DIR$/target/classes" />
<output-test url="file://$MODULE_DIR$/target/test-classes" />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" scope="TEST" name="Maven: junit:junit:4.13" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.hamcrest:hamcrest-core:1.3" level="project" />
</component>
</module>

View File

@ -0,0 +1,77 @@
import exceptions.RateLimitExceededException;
import models.Request;
import utils.Timer;
import java.util.Map;
import java.util.concurrent.*;
public class TimerWheel {
private final int timeOutPeriod;
private final int capacityPerSlot;
private final TimeUnit timeUnit;
private final ArrayBlockingQueue<Request>[] slots;
private final Map<String, Integer> reverseIndex;
private final Timer timer;
private final ExecutorService[] threads;
public TimerWheel(final TimeUnit timeUnit,
final int timeOutPeriod,
final int capacityPerSlot,
final Timer timer) {
this.timeUnit = timeUnit;
this.timeOutPeriod = timeOutPeriod;
this.capacityPerSlot = capacityPerSlot;
if (this.timeOutPeriod > 1000) {
throw new IllegalArgumentException();
}
this.slots = new ArrayBlockingQueue[this.timeOutPeriod];
this.threads = new ExecutorService[this.timeOutPeriod];
this.reverseIndex = new ConcurrentHashMap<>();
for (int i = 0; i < slots.length; i++) {
slots[i] = new ArrayBlockingQueue<>(capacityPerSlot);
threads[i] = Executors.newSingleThreadExecutor();
}
this.timer = timer;
final long timePerSlot = TimeUnit.MILLISECONDS.convert(1, timeUnit);
Executors.newSingleThreadScheduledExecutor()
.scheduleAtFixedRate(this::flushRequests,
timePerSlot - (this.timer.getCurrentTimeInMillis() % timePerSlot),
timePerSlot, TimeUnit.MILLISECONDS);
}
public Future<?> flushRequests() {
final int currentSlot = getCurrentSlot();
return threads[currentSlot].submit(() -> {
for (final Request request : slots[currentSlot]) {
if (timer.getCurrentTime(timeUnit) - request.getStartTime() >= timeOutPeriod) {
slots[currentSlot].remove(request);
reverseIndex.remove(request.getRequestId());
}
}
});
}
public Future<?> addRequest(final Request request) {
final int currentSlot = getCurrentSlot();
return threads[currentSlot].submit(() -> {
if (slots[currentSlot].size() >= capacityPerSlot) {
throw new RateLimitExceededException();
}
slots[currentSlot].add(request);
reverseIndex.put(request.getRequestId(), currentSlot);
});
}
public Future<?> evict(final String requestId) {
final int currentSlot = reverseIndex.get(requestId);
return threads[currentSlot].submit(() -> {
slots[currentSlot].remove(new Request(requestId, 0));
reverseIndex.remove(requestId);
});
}
private int getCurrentSlot() {
return (int) timer.getCurrentTime(timeUnit) % slots.length;
}
}

View File

@ -0,0 +1,7 @@
package exceptions;
public class RateLimitExceededException extends IllegalStateException {
public RateLimitExceededException() {
super("Rate limit exceeded");
}
}

View File

@ -0,0 +1,33 @@
package models;
import java.util.Objects;
public class Request {
private final String requestId;
private final long startTime;
public Request(String requestId, long startTime) {
this.requestId = requestId;
this.startTime = startTime;
}
public String getRequestId() {
return requestId;
}
public long getStartTime() {
return startTime;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
return requestId.equals(((Request) o).requestId);
}
@Override
public int hashCode() {
return requestId.hashCode();
}
}

View File

@ -0,0 +1,13 @@
package utils;
import java.util.concurrent.TimeUnit;
public class Timer {
public long getCurrentTime(final TimeUnit timeUnit) {
return timeUnit.convert(getCurrentTimeInMillis(), TimeUnit.MILLISECONDS);
}
public long getCurrentTimeInMillis() {
return System.currentTimeMillis();
}
}

View File

@ -0,0 +1,74 @@
import models.Request;
import org.junit.Assert;
import org.junit.Test;
import java.util.concurrent.TimeUnit;
public class RateLimitTest {
@Test
public void testDefaultBehaviour() throws Exception {
final TimeUnit timeUnit = TimeUnit.SECONDS;
final TestTimer timer = new TestTimer();
final TimerWheel timerWheel = new TimerWheel(timeUnit, 6, 3, timer);
timerWheel.addRequest(new Request("1", timer.getCurrentTime(timeUnit))).get();
timerWheel.addRequest(new Request("2", timer.getCurrentTime(timeUnit))).get();
timerWheel.addRequest(new Request("3", timer.getCurrentTime(timeUnit))).get();
Throwable exception = null;
try {
timerWheel.addRequest(new Request("4", timer.getCurrentTime(timeUnit))).get();
} catch (Exception e) {
exception = e.getCause();
}
Assert.assertNotNull(exception);
Assert.assertEquals("Rate limit exceeded", exception.getMessage());
tick(timeUnit, timer, timerWheel);
timerWheel.addRequest(new Request("4", timer.getCurrentTime(timeUnit))).get();
timerWheel.addRequest(new Request("5", timer.getCurrentTime(timeUnit))).get();
timerWheel.evict("1").get();
timerWheel.evict("4").get();
timerWheel.addRequest(new Request("6", timer.getCurrentTime(timeUnit))).get();
timerWheel.addRequest(new Request("7", timer.getCurrentTime(timeUnit))).get();
}
@Test
public void testClearing() throws Exception {
final TimeUnit timeUnit = TimeUnit.SECONDS;
final TestTimer timer = new TestTimer();
final int timeOutPeriod = 6;
final TimerWheel timerWheel = new TimerWheel(timeUnit, timeOutPeriod, 3, timer);
timerWheel.addRequest(new Request("0", timer.getCurrentTime(timeUnit))).get();
timerWheel.addRequest(new Request("1", timer.getCurrentTime(timeUnit))).get();
timerWheel.addRequest(new Request("2", timer.getCurrentTime(timeUnit))).get();
Throwable exception = null;
try {
timerWheel.addRequest(new Request("3", timer.getCurrentTime(timeUnit))).get();
} catch (Exception e) {
exception = e.getCause();
}
Assert.assertNotNull(exception);
Assert.assertEquals("Rate limit exceeded", exception.getMessage());
for (int i = 0; i < timeOutPeriod; i++) {
tick(timeUnit, timer, timerWheel);
}
timerWheel.addRequest(new Request("4", timer.getCurrentTime(timeUnit))).get();
timerWheel.addRequest(new Request("5", timer.getCurrentTime(timeUnit))).get();
timerWheel.addRequest(new Request("6", timer.getCurrentTime(timeUnit))).get();
exception = null;
try {
timerWheel.addRequest(new Request("7", timer.getCurrentTime(timeUnit))).get();
} catch (Exception e) {
exception = e.getCause();
}
Assert.assertNotNull(exception);
Assert.assertEquals("Rate limit exceeded", exception.getMessage());
}
private void tick(TimeUnit timeUnit, TestTimer timer, TimerWheel timerWheel) throws Exception {
timer.setTime(timer.getCurrentTimeInMillis() + TimeUnit.MILLISECONDS.convert(1, timeUnit));
timerWheel.flushRequests().get();
}
}

Some files were not shown because too many files have changed in this diff Show More