├── CONTRIBUTING.md
├── README.md
├── distributed-cache
├── .idea
│ ├── .gitignore
│ ├── compiler.xml
│ ├── jarRepositories.xml
│ ├── misc.xml
│ ├── uiDesigner.xml
│ └── vcs.xml
├── DistributedCache.iml
├── Requirements.ME
├── pom.xml
├── src
│ ├── main
│ │ └── java
│ │ │ ├── Cache.java
│ │ │ ├── CacheBuilder.java
│ │ │ ├── DataSource.java
│ │ │ ├── events
│ │ │ ├── Event.java
│ │ │ ├── Eviction.java
│ │ │ ├── Load.java
│ │ │ ├── Update.java
│ │ │ └── Write.java
│ │ │ └── models
│ │ │ ├── AccessDetails.java
│ │ │ ├── EvictionAlgorithm.java
│ │ │ ├── FetchAlgorithm.java
│ │ │ ├── Record.java
│ │ │ └── Timer.java
│ └── test
│ │ └── java
│ │ ├── TestCache.java
│ │ └── models
│ │ └── SettableTimer.java
└── target
│ ├── classes
│ ├── Cache.class
│ ├── CacheBuilder.class
│ ├── DataSource.class
│ ├── events
│ │ ├── Event.class
│ │ ├── Eviction$Type.class
│ │ ├── Eviction.class
│ │ ├── Load.class
│ │ ├── Update.class
│ │ └── Write.class
│ └── models
│ │ ├── AccessDetails.class
│ │ ├── EvictionAlgorithm.class
│ │ ├── FetchAlgorithm.class
│ │ ├── Record.class
│ │ └── Timer.class
│ └── test-classes
│ ├── TestCache$1.class
│ ├── TestCache$2.class
│ ├── TestCache.class
│ └── models
│ └── SettableTimer.class
├── distributed-event-bus
├── .idea
│ ├── .gitignore
│ ├── compiler.xml
│ ├── jarRepositories.xml
│ ├── libraries
│ │ ├── Maven__aopalliance_aopalliance_1_0.xml
│ │ ├── Maven__com_google_code_gson_gson_2_8_6.xml
│ │ ├── Maven__com_google_guava_guava_16_0_1.xml
│ │ ├── Maven__com_google_inject_guice_4_0.xml
│ │ ├── Maven__javax_inject_javax_inject_1.xml
│ │ ├── Maven__junit_junit_4_13.xml
│ │ └── Maven__org_hamcrest_hamcrest_core_1_3.xml
│ ├── misc.xml
│ ├── modules.xml
│ ├── uiDesigner.xml
│ └── vcs.xml
├── README.ME
├── distributed-event-bus.iml
├── pom.xml
├── src
│ ├── main
│ │ ├── java
│ │ │ ├── EventBus.java
│ │ │ ├── exceptions
│ │ │ │ ├── RetryLimitExceededException.java
│ │ │ │ └── UnsubscribedPollException.java
│ │ │ ├── lib
│ │ │ │ └── KeyedExecutor.java
│ │ │ ├── models
│ │ │ │ ├── Event.java
│ │ │ │ ├── EventType.java
│ │ │ │ ├── FailureEvent.java
│ │ │ │ └── Subscription.java
│ │ │ └── util
│ │ │ │ └── Timer.java
│ │ └── resources
│ │ │ └── application.properties
│ └── test
│ │ └── java
│ │ ├── EventBusTest.java
│ │ └── TestTimer.java
└── target
│ ├── classes
│ ├── EventBus.class
│ ├── META-INF
│ │ └── distributed-event-bus.kotlin_module
│ ├── application.properties
│ ├── exceptions
│ │ ├── RetryLimitExceededException.class
│ │ └── UnsubscribedPollException.class
│ ├── lib
│ │ └── KeyedExecutor.class
│ ├── models
│ │ ├── Event.class
│ │ ├── EventType.class
│ │ ├── FailureEvent.class
│ │ └── Subscription.class
│ └── util
│ │ └── Timer.class
│ └── test-classes
│ ├── EventBusTest.class
│ └── TestTimer.class
├── rate-limiter
├── .idea
│ ├── .gitignore
│ ├── compiler.xml
│ ├── jarRepositories.xml
│ ├── libraries
│ │ ├── Maven__junit_junit_4_13.xml
│ │ └── Maven__org_hamcrest_hamcrest_core_1_3.xml
│ ├── misc.xml
│ ├── modules.xml
│ ├── uiDesigner.xml
│ └── vcs.xml
├── pom.xml
├── rate-limiter.iml
├── src
│ ├── main
│ │ └── java
│ │ │ ├── TimerWheel.java
│ │ │ ├── exceptions
│ │ │ └── RateLimitExceededException.java
│ │ │ ├── models
│ │ │ └── Request.java
│ │ │ └── utils
│ │ │ └── Timer.java
│ └── test
│ │ └── java
│ │ ├── RateLimitTest.java
│ │ └── TestTimer.java
└── target
│ ├── classes
│ ├── TimerWheel.class
│ ├── exceptions
│ │ └── RateLimitExceededException.class
│ ├── models
│ │ └── Request.class
│ └── utils
│ │ └── Timer.class
│ └── test-classes
│ ├── RateLimitTest.class
│ └── TestTimer.class
└── service-orchestrator
├── .idea
├── .gitignore
├── compiler.xml
├── jarRepositories.xml
├── misc.xml
├── uiDesigner.xml
└── vcs.xml
├── pom.xml
├── service-orchestrator.iml
├── src
├── main
│ └── java
│ │ ├── LoadBalancer.java
│ │ ├── algorithms
│ │ ├── ConsistentHashing.java
│ │ ├── Router.java
│ │ └── WeightedRoundRobin.java
│ │ └── models
│ │ ├── Node.java
│ │ ├── Request.java
│ │ └── Service.java
└── test
│ └── java
│ ├── LBTester.java
│ └── RouterTester.java
└── target
├── classes
├── LoadBalancer.class
├── META-INF
│ └── service-orchestrator.kotlin_module
├── algorithms
│ ├── ConsistentHashing.class
│ ├── Router.class
│ └── WeightedRoundRobin.class
└── models
│ ├── Node.class
│ ├── Request.class
│ └── Service.class
└── test-classes
├── LBTester.class
├── META-INF
└── service-orchestrator.kotlin_module
└── RouterTester.class
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ## Guidelines:
2 |
3 | _Please do not post personal videos or podcast resources, unless relevant to system design._
4 |
5 | Please give a short description of the link(s) before raising a pull request to add them.
6 |
7 | Try to look for good video and blog sources 💪🙂
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Low Level System Design
2 |
3 | This project contains multiple LLD codes for system design interviews.
4 | Please raise issues and pull requests for fixes and updates.
5 |
6 | 1. Cache
7 | 2. Event Bus
8 | 3. Rate Limiter
9 | 4. Service Orchestrator
10 |
11 | The following resources are useful for learning low level design.
12 |
13 | ### Design Patterns
14 |
15 | [Refactoring Guru](https://refactoring.guru/)
16 |
17 | ### Memory Management
18 |
19 | [Texas University Memory Models](https://www.cs.utexas.edu/~bornholt/post/memory-models.html)
20 |
21 | [Slack reducing memory footprint](https://slack.engineering/reducing-slacks-memory-footprint)
22 |
23 | ### Rate Limiting
24 |
25 | [Apache Kafka Exactly Once processing](https://docs.google.com/document/d/11Jqy_GjUGtdXJK94XGsEIK7CP1SnQGdp2eF0wSw9ra8)
26 |
27 | [Uber Rate Limiter](https://github.com/uber-go/ratelimit/blob/master/ratelimit.go)
28 |
29 | [Martin Fowler Circuit Breaker](https://martinfowler.com/bliki/CircuitBreaker.html)
30 |
31 | [Netflix Hystrix](https://github.com/Netflix/Hystrix)
32 |
33 | [Amazon AWS Shuffle Sharding](https://github.com/awslabs/route53-infima)
34 |
35 | ### Course
36 |
37 | https://interviewready.io/
38 |
--------------------------------------------------------------------------------
/distributed-cache/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/distributed-cache/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/distributed-cache/.idea/jarRepositories.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/distributed-cache/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/distributed-cache/.idea/uiDesigner.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | -
6 |
7 |
8 | -
9 |
10 |
11 | -
12 |
13 |
14 | -
15 |
16 |
17 | -
18 |
19 |
20 |
21 |
22 |
23 | -
24 |
25 |
26 |
27 |
28 |
29 | -
30 |
31 |
32 |
33 |
34 |
35 | -
36 |
37 |
38 |
39 |
40 |
41 | -
42 |
43 |
44 |
45 |
46 | -
47 |
48 |
49 |
50 |
51 | -
52 |
53 |
54 |
55 |
56 | -
57 |
58 |
59 |
60 |
61 | -
62 |
63 |
64 |
65 |
66 | -
67 |
68 |
69 |
70 |
71 | -
72 |
73 |
74 | -
75 |
76 |
77 |
78 |
79 | -
80 |
81 |
82 |
83 |
84 | -
85 |
86 |
87 |
88 |
89 | -
90 |
91 |
92 |
93 |
94 | -
95 |
96 |
97 |
98 |
99 | -
100 |
101 |
102 | -
103 |
104 |
105 | -
106 |
107 |
108 | -
109 |
110 |
111 | -
112 |
113 |
114 |
115 |
116 | -
117 |
118 |
119 | -
120 |
121 |
122 |
123 |
124 |
--------------------------------------------------------------------------------
/distributed-cache/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/distributed-cache/DistributedCache.iml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/distributed-cache/Requirements.ME:
--------------------------------------------------------------------------------
1 | Should allow:
2 | 1) listeners on load and evict
3 | 2) hot loading elements on startup
4 | 3) multiple eviction algorithms like LRU and LFU
5 | 4) expiration time
6 | 5) multiple fetch algorithms like write back and write through
7 | 6) return futures
8 | 7) request collapsing
9 | 8) Avoid thrashing with rate limiting
--------------------------------------------------------------------------------
/distributed-cache/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | interviewready.io
8 | distributed-cache
9 | 1.0
10 |
11 |
12 |
13 | org.apache.maven.plugins
14 | maven-compiler-plugin
15 |
16 | 11
17 | 11
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | junit
26 | junit
27 | 4.13.1
28 | test
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/distributed-cache/src/main/java/Cache.java:
--------------------------------------------------------------------------------
1 | import events.*;
2 | import models.*;
3 |
4 | import java.time.Duration;
5 | import java.util.List;
6 | import java.util.Map;
7 | import java.util.Set;
8 | import java.util.concurrent.*;
9 | import java.util.function.Function;
10 |
11 | public class Cache {
12 | private final int maximumSize;
13 | private final FetchAlgorithm fetchAlgorithm;
14 | private final Duration expiryTime;
15 | private final Map>> cache;
16 | private final ConcurrentSkipListMap> priorityQueue;
17 | private final ConcurrentSkipListMap> expiryQueue;
18 | private final DataSource dataSource;
19 | private final List> eventQueue;
20 | private final ExecutorService[] executorPool;
21 | private final Timer timer;
22 |
23 | protected Cache(final int maximumSize,
24 | final Duration expiryTime,
25 | final FetchAlgorithm fetchAlgorithm,
26 | final EvictionAlgorithm evictionAlgorithm,
27 | final DataSource dataSource,
28 | final Set keysToEagerlyLoad,
29 | final Timer timer,
30 | final int poolSize) {
31 | this.expiryTime = expiryTime;
32 | this.maximumSize = maximumSize;
33 | this.fetchAlgorithm = fetchAlgorithm;
34 | this.timer = timer;
35 | this.cache = new ConcurrentHashMap<>();
36 | this.eventQueue = new CopyOnWriteArrayList<>();
37 | this.dataSource = dataSource;
38 | this.executorPool = new ExecutorService[poolSize];
39 | for (int i = 0; i < poolSize; i++) {
40 | executorPool[i] = Executors.newSingleThreadExecutor();
41 | }
42 | priorityQueue = new ConcurrentSkipListMap<>((first, second) -> {
43 | final var accessTimeDifference = (int) (first.getLastAccessTime() - second.getLastAccessTime());
44 | if (evictionAlgorithm.equals(EvictionAlgorithm.LRU)) {
45 | return accessTimeDifference;
46 | } else {
47 | final var accessCountDifference = first.getAccessCount() - second.getAccessCount();
48 | return accessCountDifference != 0 ? accessCountDifference : accessTimeDifference;
49 | }
50 | });
51 | expiryQueue = new ConcurrentSkipListMap<>();
52 | final var eagerLoading = keysToEagerlyLoad.stream()
53 | .map(key -> getThreadFor(key, addToCache(key, loadFromDB(dataSource, key))))
54 | .toArray(CompletableFuture[]::new);
55 | CompletableFuture.allOf(eagerLoading).join();
56 | }
57 |
58 | private CompletionStage getThreadFor(KEY key, CompletionStage task) {
59 | return CompletableFuture.supplyAsync(() -> task, executorPool[Math.abs(key.hashCode() % executorPool.length)]).thenCompose(Function.identity());
60 | }
61 |
62 | public CompletionStage get(KEY key) {
63 | return getThreadFor(key, getFromCache(key));
64 | }
65 |
66 | public CompletionStage set(KEY key, VALUE value) {
67 | return getThreadFor(key, setInCache(key, value));
68 | }
69 |
70 | private CompletionStage getFromCache(KEY key) {
71 | final CompletionStage> result;
72 | if (!cache.containsKey(key)) {
73 | result = addToCache(key, loadFromDB(dataSource, key));
74 | } else {
75 | result = cache.get(key).thenCompose(record -> {
76 | if (hasExpired(record)) {
77 | priorityQueue.get(record.getAccessDetails()).remove(key);
78 | expiryQueue.get(record.getInsertionTime()).remove(key);
79 | eventQueue.add(new Eviction<>(record, Eviction.Type.EXPIRY, timer.getCurrentTime()));
80 | return addToCache(key, loadFromDB(dataSource, key));
81 | } else {
82 | return CompletableFuture.completedFuture(record);
83 | }
84 | });
85 | }
86 | return result.thenApply(record -> {
87 | priorityQueue.get(record.getAccessDetails()).remove(key);
88 | final AccessDetails updatedAccessDetails = record.getAccessDetails().update(timer.getCurrentTime());
89 | priorityQueue.putIfAbsent(updatedAccessDetails, new CopyOnWriteArrayList<>());
90 | priorityQueue.get(updatedAccessDetails).add(key);
91 | record.setAccessDetails(updatedAccessDetails);
92 | return record.getValue();
93 | });
94 | }
95 |
96 | public CompletionStage setInCache(KEY key, VALUE value) {
97 | CompletionStage result = CompletableFuture.completedFuture(null);
98 | if (cache.containsKey(key)) {
99 | result = cache.remove(key)
100 | .thenAccept(oldRecord -> {
101 | priorityQueue.get(oldRecord.getAccessDetails()).remove(key);
102 | expiryQueue.get(oldRecord.getInsertionTime()).remove(key);
103 | if (hasExpired(oldRecord)) {
104 | eventQueue.add(new Eviction<>(oldRecord, Eviction.Type.EXPIRY, timer.getCurrentTime()));
105 | } else {
106 | eventQueue.add(new Update<>(new Record<>(key, value, timer.getCurrentTime()), oldRecord, timer.getCurrentTime()));
107 | }
108 | });
109 | }
110 | return result.thenCompose(__ -> addToCache(key, CompletableFuture.completedFuture(value))).thenCompose(record -> {
111 | final CompletionStage writeOperation = persistRecord(record);
112 | return fetchAlgorithm == FetchAlgorithm.WRITE_THROUGH ? writeOperation : CompletableFuture.completedFuture(null);
113 | });
114 | }
115 |
116 | private CompletionStage> addToCache(final KEY key, final CompletionStage valueFuture) {
117 | manageEntries();
118 | final var recordFuture = valueFuture.thenApply(value -> {
119 | final Record record = new Record<>(key, value, timer.getCurrentTime());
120 | expiryQueue.putIfAbsent(record.getInsertionTime(), new CopyOnWriteArrayList<>());
121 | expiryQueue.get(record.getInsertionTime()).add(key);
122 | priorityQueue.putIfAbsent(record.getAccessDetails(), new CopyOnWriteArrayList<>());
123 | priorityQueue.get(record.getAccessDetails()).add(key);
124 | return record;
125 | });
126 | cache.put(key, recordFuture);
127 | return recordFuture;
128 | }
129 |
130 | private synchronized void manageEntries() {
131 | if (cache.size() >= maximumSize) {
132 | while (!expiryQueue.isEmpty() && hasExpired(expiryQueue.firstKey())) {
133 | final List keys = expiryQueue.pollFirstEntry().getValue();
134 | for (final KEY key : keys) {
135 | final Record expiredRecord = cache.remove(key).toCompletableFuture().join();
136 | priorityQueue.remove(expiredRecord.getAccessDetails());
137 | eventQueue.add(new Eviction<>(expiredRecord, Eviction.Type.EXPIRY, timer.getCurrentTime()));
138 | }
139 | }
140 | }
141 | if (cache.size() >= maximumSize) {
142 | List keys = priorityQueue.pollFirstEntry().getValue();
143 | while (keys.isEmpty()) {
144 | keys = priorityQueue.pollFirstEntry().getValue();
145 | }
146 | for (final KEY key : keys) {
147 | final Record lowestPriorityRecord = cache.remove(key).toCompletableFuture().join();
148 | expiryQueue.get(lowestPriorityRecord.getInsertionTime()).remove(lowestPriorityRecord.getKey());
149 | eventQueue.add(new Eviction<>(lowestPriorityRecord, Eviction.Type.REPLACEMENT, timer.getCurrentTime()));
150 | }
151 | }
152 | }
153 |
154 | private CompletionStage persistRecord(final Record record) {
155 | return dataSource.persist(record.getKey(), record.getValue(), record.getInsertionTime())
156 | .thenAccept(__ -> eventQueue.add(new Write<>(record, timer.getCurrentTime())));
157 | }
158 |
159 | private boolean hasExpired(final Record record) {
160 | return hasExpired(record.getInsertionTime());
161 | }
162 |
163 | private boolean hasExpired(final Long time) {
164 | return Duration.ofNanos(timer.getCurrentTime() - time).compareTo(expiryTime) > 0;
165 | }
166 |
167 | public List> getEventQueue() {
168 | return eventQueue;
169 | }
170 |
171 | private CompletionStage loadFromDB(final DataSource dataSource, KEY key) {
172 | return dataSource.load(key).whenComplete((value, throwable) -> {
173 | if (throwable == null) {
174 | eventQueue.add(new Load<>(new Record<>(key, value, timer.getCurrentTime()), timer.getCurrentTime()));
175 | }
176 | });
177 | }
178 | }
179 |
180 |
--------------------------------------------------------------------------------
/distributed-cache/src/main/java/CacheBuilder.java:
--------------------------------------------------------------------------------
1 | import models.EvictionAlgorithm;
2 | import models.FetchAlgorithm;
3 | import models.Timer;
4 |
5 | import java.time.Duration;
6 | import java.util.HashSet;
7 | import java.util.Set;
8 |
9 | public class CacheBuilder {
10 | private int maximumSize;
11 | private Duration expiryTime;
12 | private final Set onStartLoad;
13 | private EvictionAlgorithm evictionAlgorithm;
14 | private FetchAlgorithm fetchAlgorithm;
15 | private DataSource dataSource;
16 | private Timer timer;
17 | private int poolSize;
18 |
19 | public CacheBuilder() {
20 | maximumSize = 1000;
21 | expiryTime = Duration.ofDays(365);
22 | fetchAlgorithm = FetchAlgorithm.WRITE_THROUGH;
23 | evictionAlgorithm = EvictionAlgorithm.LRU;
24 | onStartLoad = new HashSet<>();
25 | poolSize = 1;
26 | timer = new Timer();
27 | }
28 |
29 | public CacheBuilder maximumSize(final int maximumSize) {
30 | this.maximumSize = maximumSize;
31 | return this;
32 | }
33 |
34 | public CacheBuilder expiryTime(final Duration expiryTime) {
35 | this.expiryTime = expiryTime;
36 | return this;
37 | }
38 |
39 | public CacheBuilder loadKeysOnStart(final Set keys) {
40 | this.onStartLoad.addAll(keys);
41 | return this;
42 | }
43 |
44 | public CacheBuilder evictionAlgorithm(final EvictionAlgorithm evictionAlgorithm) {
45 | this.evictionAlgorithm = evictionAlgorithm;
46 | return this;
47 | }
48 |
49 | public CacheBuilder fetchAlgorithm(final FetchAlgorithm fetchAlgorithm) {
50 | this.fetchAlgorithm = fetchAlgorithm;
51 | return this;
52 | }
53 |
54 | public CacheBuilder dataSource(final DataSource dataSource) {
55 | this.dataSource = dataSource;
56 | return this;
57 | }
58 |
59 | public CacheBuilder timer(final Timer timer) {
60 | this.timer = timer;
61 | return this;
62 | }
63 |
64 | public CacheBuilder poolSize(final int poolSize) {
65 | this.poolSize = poolSize;
66 | return this;
67 | }
68 |
69 | public Cache build() {
70 | if (dataSource == null) {
71 | throw new IllegalArgumentException("No datasource configured");
72 | }
73 | return new Cache<>(maximumSize, expiryTime, fetchAlgorithm, evictionAlgorithm, dataSource, onStartLoad, timer, poolSize);
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/distributed-cache/src/main/java/DataSource.java:
--------------------------------------------------------------------------------
1 | import java.util.concurrent.CompletionStage;
2 |
3 | public interface DataSource {
4 |
5 | CompletionStage load(KEY key);
6 |
7 | CompletionStage persist(KEY key, VALUE value, long timestamp);
8 | }
9 |
--------------------------------------------------------------------------------
/distributed-cache/src/main/java/events/Event.java:
--------------------------------------------------------------------------------
1 | package events;
2 |
3 | import models.Record;
4 |
5 | import java.util.UUID;
6 |
7 | public abstract class Event {
8 | private final String id;
9 | private final Record element;
10 | private final long timestamp;
11 |
12 | public Event(Record element, long timestamp) {
13 | this.element = element;
14 | this.timestamp = timestamp;
15 | id = UUID.randomUUID().toString();
16 | }
17 |
18 | public String getId() {
19 | return id;
20 | }
21 |
22 | public Record getElement() {
23 | return element;
24 | }
25 |
26 | public long getTimestamp() {
27 | return timestamp;
28 | }
29 |
30 | @Override
31 | public String toString() {
32 | return this.getClass().getName() + "{" +
33 | "element=" + element +
34 | ", timestamp=" + timestamp +
35 | "}\n";
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/distributed-cache/src/main/java/events/Eviction.java:
--------------------------------------------------------------------------------
1 | package events;
2 |
3 | import models.Record;
4 |
5 | public class Eviction extends Event {
6 | private final Type type;
7 |
8 | public Eviction(Record element, Type type, long timestamp) {
9 | super(element, timestamp);
10 | this.type = type;
11 | }
12 |
13 | public Type getType() {
14 | return type;
15 | }
16 |
17 | public enum Type {
18 | EXPIRY, REPLACEMENT
19 | }
20 |
21 | @Override
22 | public String toString() {
23 | return "Eviction{" +
24 | "type=" + type +
25 | ", "+super.toString() +
26 | "}\n";
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/distributed-cache/src/main/java/events/Load.java:
--------------------------------------------------------------------------------
1 | package events;
2 |
3 | import models.Record;
4 |
5 | public class Load extends Event {
6 |
7 | public Load(Record element, long timestamp) {
8 | super(element, timestamp);
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/distributed-cache/src/main/java/events/Update.java:
--------------------------------------------------------------------------------
1 | package events;
2 |
3 | import models.Record;
4 |
5 | public class Update extends Event {
6 |
7 | private final Record previousValue;
8 |
9 | public Update(Record element, Record previousValue, long timestamp) {
10 | super(element, timestamp);
11 | this.previousValue = previousValue;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/distributed-cache/src/main/java/events/Write.java:
--------------------------------------------------------------------------------
1 | package events;
2 |
3 | import models.Record;
4 |
5 | public class Write extends Event {
6 |
7 | public Write(Record element, long timestamp) {
8 | super(element, timestamp);
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/distributed-cache/src/main/java/models/AccessDetails.java:
--------------------------------------------------------------------------------
1 | package models;
2 |
3 | import java.util.Objects;
4 | import java.util.concurrent.atomic.LongAdder;
5 |
6 | public class AccessDetails {
7 | private final LongAdder accessCount;
8 | private long lastAccessTime;
9 |
10 | public AccessDetails(long lastAccessTime) {
11 | accessCount = new LongAdder();
12 | this.lastAccessTime = lastAccessTime;
13 | }
14 |
15 | public long getLastAccessTime() {
16 | return lastAccessTime;
17 | }
18 |
19 | public int getAccessCount() {
20 | return (int) accessCount.longValue();
21 | }
22 |
23 | public AccessDetails update(long lastAccessTime) {
24 | final AccessDetails accessDetails = new AccessDetails(lastAccessTime);
25 | accessDetails.accessCount.add(this.accessCount.longValue() + 1);
26 | return accessDetails;
27 | }
28 |
29 | @Override
30 | public boolean equals(Object o) {
31 | if (this == o) return true;
32 | if (o == null || getClass() != o.getClass()) return false;
33 | AccessDetails that = (AccessDetails) o;
34 | return lastAccessTime == that.lastAccessTime &&
35 | this.getAccessCount() == that.getAccessCount();
36 | }
37 |
38 | @Override
39 | public int hashCode() {
40 | return Objects.hash(getAccessCount(), lastAccessTime);
41 | }
42 |
43 | @Override
44 | public String toString() {
45 | return "AccessDetails{" +
46 | "accessCount=" + accessCount +
47 | ", lastAccessTime=" + lastAccessTime +
48 | '}';
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/distributed-cache/src/main/java/models/EvictionAlgorithm.java:
--------------------------------------------------------------------------------
1 | package models;
2 |
3 | public enum EvictionAlgorithm {
4 | LRU, LFU
5 | }
6 |
--------------------------------------------------------------------------------
/distributed-cache/src/main/java/models/FetchAlgorithm.java:
--------------------------------------------------------------------------------
1 | package models;
2 |
3 | public enum FetchAlgorithm {
4 | WRITE_THROUGH, WRITE_BACK
5 | }
6 |
--------------------------------------------------------------------------------
/distributed-cache/src/main/java/models/Record.java:
--------------------------------------------------------------------------------
1 | package models;
2 |
3 | public class Record {
4 | private final KEY key;
5 | private final VALUE value;
6 | private final long insertionTime;
7 | private AccessDetails accessDetails;
8 |
9 | public Record(KEY key, VALUE value, long insertionTime) {
10 | this.key = key;
11 | this.value = value;
12 | this.insertionTime = insertionTime;
13 | this.accessDetails = new AccessDetails(insertionTime);
14 | }
15 |
16 | public KEY getKey() {
17 | return key;
18 | }
19 |
20 | public VALUE getValue() {
21 | return value;
22 | }
23 |
24 | public long getInsertionTime() {
25 | return insertionTime;
26 | }
27 |
28 | public AccessDetails getAccessDetails() {
29 | return accessDetails;
30 | }
31 |
32 | public void setAccessDetails(final AccessDetails accessDetails) {
33 | this.accessDetails = accessDetails;
34 | }
35 |
36 | @Override
37 | public String toString() {
38 | return "Record{" +
39 | "key=" + key +
40 | ", value=" + value +
41 | ", insertionTime=" + insertionTime +
42 | ", accessDetails=" + accessDetails +
43 | '}';
44 | }
45 | }
46 |
47 |
--------------------------------------------------------------------------------
/distributed-cache/src/main/java/models/Timer.java:
--------------------------------------------------------------------------------
1 | package models;
2 |
3 | public class Timer {
4 | public long getCurrentTime() {
5 | return System.nanoTime();
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/distributed-cache/src/test/java/TestCache.java:
--------------------------------------------------------------------------------
1 | import events.Eviction;
2 | import events.Load;
3 | import events.Update;
4 | import events.Write;
5 | import models.EvictionAlgorithm;
6 | import models.FetchAlgorithm;
7 | import models.SettableTimer;
8 | import org.junit.Assert;
9 | import org.junit.Before;
10 | import org.junit.Test;
11 |
12 | import java.time.Duration;
13 | import java.util.*;
14 | import java.util.concurrent.CompletableFuture;
15 | import java.util.concurrent.CompletionStage;
16 | import java.util.concurrent.ConcurrentHashMap;
17 | import java.util.concurrent.ExecutionException;
18 | import java.util.stream.Collectors;
19 |
20 | public class TestCache {
21 |
22 | private static final String PROFILE_MUMBAI_ENGINEER = "profile_mumbai_engineer", PROFILE_HYDERABAD_ENGINEER = "profile_hyderabad_engineer";
23 | private final Map dataMap = new ConcurrentHashMap<>();
24 | private DataSource dataSource;
25 | private final Queue> writeOperations = new LinkedList<>();
26 | private DataSource writeBackDataSource;
27 |
28 | @Before
29 | public void setUp() {
30 | dataMap.clear();
31 | writeOperations.clear();
32 | dataMap.put(PROFILE_MUMBAI_ENGINEER, "violet");
33 | dataMap.put(PROFILE_HYDERABAD_ENGINEER, "blue");
34 | dataSource = new DataSource<>() {
35 | @Override
36 | public CompletionStage load(String key) {
37 | if (dataMap.containsKey(key)) {
38 | return CompletableFuture.completedFuture(dataMap.get(key));
39 | } else {
40 | return CompletableFuture.failedStage(new NullPointerException());
41 | }
42 | }
43 |
44 | @Override
45 | public CompletionStage persist(String key, String value, long timestamp) {
46 | dataMap.put(key, value);
47 | return CompletableFuture.completedFuture(null);
48 | }
49 | };
50 |
51 | writeBackDataSource = new DataSource<>() {
52 | @Override
53 | public CompletionStage load(String key) {
54 | if (dataMap.containsKey(key)) {
55 | return CompletableFuture.completedFuture(dataMap.get(key));
56 | } else {
57 | return CompletableFuture.failedStage(new NullPointerException());
58 | }
59 | }
60 |
61 | @Override
62 | public CompletionStage persist(String key, String value, long timestamp) {
63 | final CompletableFuture hold = new CompletableFuture<>();
64 | writeOperations.add(hold);
65 | return hold.thenAccept(__ -> dataMap.put(key, value));
66 | }
67 | };
68 | }
69 |
70 | private void acceptWrite() {
71 | final CompletableFuture write = writeOperations.poll();
72 | if (write != null) {
73 | write.complete(null);
74 | }
75 | }
76 |
77 | @Test(expected = IllegalArgumentException.class)
78 | public void testCacheConstructionWithoutDataSourceFailure() {
79 | new CacheBuilder<>().build();
80 | }
81 |
82 | @Test
83 | public void testCacheDefaultBehavior() throws ExecutionException, InterruptedException {
84 | final var cache = new CacheBuilder().dataSource(dataSource).build();
85 | Assert.assertNotNull(cache);
86 | assert isEqualTo(cache.get(PROFILE_MUMBAI_ENGINEER), "violet");
87 | assert cache.get("random")
88 | .exceptionally(throwable -> Boolean.TRUE.toString())
89 | .thenApply(Boolean::valueOf)
90 | .toCompletableFuture()
91 | .get();
92 | assert isEqualTo(cache.set(PROFILE_MUMBAI_ENGINEER, "brown").thenCompose(__ -> cache.get(PROFILE_MUMBAI_ENGINEER)), "brown");
93 | Assert.assertEquals(3, cache.getEventQueue().size());
94 | assert cache.getEventQueue().get(0) instanceof Load;
95 | assert cache.getEventQueue().get(1) instanceof Update;
96 | assert cache.getEventQueue().get(2) instanceof Write;
97 | }
98 |
99 | @Test
100 | public void Eviction_LRU() {
101 | final var maximumSize = 2;
102 | final var cache = new CacheBuilder()
103 | .maximumSize(maximumSize)
104 | .evictionAlgorithm(EvictionAlgorithm.LRU)
105 | .fetchAlgorithm(FetchAlgorithm.WRITE_BACK)
106 | .dataSource(writeBackDataSource).build();
107 | cache.get(PROFILE_MUMBAI_ENGINEER).toCompletableFuture().join();
108 | for (int i = 0; i < maximumSize; i++) {
109 | cache.set("key" + i, "value" + i).toCompletableFuture().join();
110 | }
111 | Assert.assertEquals(2, cache.getEventQueue().size());
112 | assert cache.getEventQueue().get(0) instanceof Load;
113 | assert cache.getEventQueue().get(1) instanceof Eviction;
114 | final var evictionEvent = (Eviction) cache.getEventQueue().get(1);
115 | Assert.assertEquals(Eviction.Type.REPLACEMENT, evictionEvent.getType());
116 | Assert.assertEquals(PROFILE_MUMBAI_ENGINEER, evictionEvent.getElement().getKey());
117 | cache.getEventQueue().clear();
118 | final var permutation = new ArrayList();
119 | for (int i = 0; i < maximumSize; i++) {
120 | permutation.add(i);
121 | }
122 | Collections.shuffle(permutation);
123 | for (final int index : permutation) {
124 | cache.get("key" + index).toCompletableFuture().join();
125 | }
126 | for (int i = 0; i < maximumSize; i++) {
127 | cache.set("random" + permutation.get(i), "random_value").toCompletableFuture().join();
128 | assert cache.getEventQueue().get(i) instanceof Eviction;
129 | final var eviction = (Eviction) cache.getEventQueue().get(i);
130 | Assert.assertEquals(Eviction.Type.REPLACEMENT, eviction.getType());
131 | Assert.assertEquals("key" + permutation.get(i), eviction.getElement().getKey());
132 | }
133 | }
134 |
135 | @Test
136 | public void Eviction_LFU() {
137 | final var maximumSize = 2;
138 | final var cache = new CacheBuilder()
139 | .maximumSize(maximumSize)
140 | .evictionAlgorithm(EvictionAlgorithm.LFU)
141 | .fetchAlgorithm(FetchAlgorithm.WRITE_BACK)
142 | .dataSource(writeBackDataSource)
143 | .build();
144 | cache.get(PROFILE_MUMBAI_ENGINEER).toCompletableFuture().join();
145 | for (int i = 0; i < maximumSize; i++) {
146 | cache.set("key" + i, "value" + i).toCompletableFuture().join();
147 | }
148 | Assert.assertEquals(2, cache.getEventQueue().size());
149 | assert cache.getEventQueue().get(0) instanceof Load;
150 | assert cache.getEventQueue().get(1) instanceof Eviction;
151 | final var evictionEvent = (Eviction) cache.getEventQueue().get(1);
152 | Assert.assertEquals(Eviction.Type.REPLACEMENT, evictionEvent.getType());
153 | Assert.assertEquals("key0", evictionEvent.getElement().getKey());
154 | for (int i = 0; i < maximumSize; i++) {
155 | acceptWrite();
156 | }
157 | final var permutation = new ArrayList();
158 | for (int i = 0; i < maximumSize; i++) {
159 | permutation.add(i);
160 | }
161 | Collections.shuffle(permutation);
162 | for (final int index : permutation) {
163 | for (int i = 0; i <= index; i++) {
164 | cache.get("key" + index).toCompletableFuture().join();
165 | }
166 | }
167 | cache.getEventQueue().clear();
168 | for (int i = 0; i < maximumSize; i++) {
169 | cache.set("random" + i, "random_value").toCompletableFuture().join();
170 | acceptWrite();
171 | for (int j = 0; j <= maximumSize; j++) {
172 | cache.get("random" + i).toCompletableFuture().join();
173 | }
174 | Assert.assertEquals(Eviction.class.getName(), cache.getEventQueue().get(i * 2).getClass().getName());
175 | Assert.assertEquals(Write.class.getName(), cache.getEventQueue().get(i * 2 + 1).getClass().getName());
176 | final var eviction = (Eviction) cache.getEventQueue().get(i * 2);
177 | System.out.println(cache.getEventQueue().get(i));
178 | Assert.assertEquals(Eviction.Type.REPLACEMENT, eviction.getType());
179 | Assert.assertEquals("key" + i, eviction.getElement().getKey());
180 | }
181 | }
182 |
183 | @Test
184 | public void ExpiryOnGet() {
185 | final var timer = new SettableTimer();
186 | final var startTime = System.nanoTime();
187 | final var cache = new CacheBuilder().timer(timer).dataSource(dataSource).expiryTime(Duration.ofSeconds(10)).build();
188 | timer.setTime(startTime);
189 | cache.get(PROFILE_MUMBAI_ENGINEER).toCompletableFuture().join();
190 | Assert.assertEquals(1, cache.getEventQueue().size());
191 | assert cache.getEventQueue().get(0) instanceof Load;
192 | Assert.assertEquals(PROFILE_MUMBAI_ENGINEER, cache.getEventQueue().get(0).getElement().getKey());
193 | timer.setTime(startTime + Duration.ofSeconds(10).toNanos() + 1);
194 | cache.get(PROFILE_MUMBAI_ENGINEER).toCompletableFuture().join();
195 | Assert.assertEquals(3, cache.getEventQueue().size());
196 | assert cache.getEventQueue().get(1) instanceof Eviction;
197 | assert cache.getEventQueue().get(2) instanceof Load;
198 | final var eviction = (Eviction) cache.getEventQueue().get(1);
199 | Assert.assertEquals(Eviction.Type.EXPIRY, eviction.getType());
200 | Assert.assertEquals(PROFILE_MUMBAI_ENGINEER, eviction.getElement().getKey());
201 | }
202 |
203 | @Test
204 | public void ExpiryOnSet() {
205 | final var timer = new SettableTimer();
206 | final var startTime = System.nanoTime();
207 | final var cache = new CacheBuilder().timer(timer).dataSource(dataSource).expiryTime(Duration.ofSeconds(10)).build();
208 | timer.setTime(startTime);
209 | cache.get(PROFILE_MUMBAI_ENGINEER).toCompletableFuture().join();
210 | Assert.assertEquals(1, cache.getEventQueue().size());
211 | assert cache.getEventQueue().get(0) instanceof Load;
212 | Assert.assertEquals(PROFILE_MUMBAI_ENGINEER, cache.getEventQueue().get(0).getElement().getKey());
213 | timer.setTime(startTime + Duration.ofSeconds(10).toNanos() + 1);
214 | cache.set(PROFILE_MUMBAI_ENGINEER, "blue").toCompletableFuture().join();
215 | Assert.assertEquals(3, cache.getEventQueue().size());
216 | assert cache.getEventQueue().get(1) instanceof Eviction;
217 | assert cache.getEventQueue().get(2) instanceof Write;
218 | final var eviction = (Eviction) cache.getEventQueue().get(1);
219 | Assert.assertEquals(Eviction.Type.EXPIRY, eviction.getType());
220 | Assert.assertEquals(PROFILE_MUMBAI_ENGINEER, eviction.getElement().getKey());
221 | }
222 |
223 | @Test
224 | public void ExpiryOnEviction() {
225 | final var timer = new SettableTimer();
226 | final var startTime = System.nanoTime();
227 | final var cache = new CacheBuilder().maximumSize(2).timer(timer).dataSource(dataSource).expiryTime(Duration.ofSeconds(10)).build();
228 | timer.setTime(startTime);
229 | cache.get(PROFILE_MUMBAI_ENGINEER).toCompletableFuture().join();
230 | cache.get(PROFILE_HYDERABAD_ENGINEER).toCompletableFuture().join();
231 | timer.setTime(startTime + Duration.ofSeconds(10).toNanos() + 1);
232 | cache.set("randomKey", "randomValue").toCompletableFuture().join();
233 | Assert.assertEquals(5, cache.getEventQueue().size());
234 | assert cache.getEventQueue().get(2) instanceof Eviction;
235 | assert cache.getEventQueue().get(3) instanceof Eviction;
236 | assert cache.getEventQueue().get(4) instanceof Write;
237 | final var eviction1 = (Eviction) cache.getEventQueue().get(2);
238 | Assert.assertEquals(Eviction.Type.EXPIRY, eviction1.getType());
239 | Assert.assertEquals(PROFILE_MUMBAI_ENGINEER, eviction1.getElement().getKey());
240 | final var eviction2 = (Eviction) cache.getEventQueue().get(3);
241 | Assert.assertEquals(Eviction.Type.EXPIRY, eviction2.getType());
242 | Assert.assertEquals(PROFILE_HYDERABAD_ENGINEER, eviction2.getElement().getKey());
243 | }
244 |
245 | @Test
246 | public void FetchingWriteBack() {
247 | final var cache = new CacheBuilder()
248 | .maximumSize(1)
249 | .dataSource(writeBackDataSource)
250 | .fetchAlgorithm(FetchAlgorithm.WRITE_BACK)
251 | .build();
252 | cache.set("randomKey", "randomValue").toCompletableFuture().join();
253 | Assert.assertEquals(0, cache.getEventQueue().size());
254 | Assert.assertNull(dataMap.get("randomValue"));
255 | acceptWrite();
256 | }
257 |
258 | @Test
259 | public void FetchingWriteThrough() {
260 | final var cache = new CacheBuilder().dataSource(dataSource).fetchAlgorithm(FetchAlgorithm.WRITE_THROUGH).build();
261 | cache.set("randomKey", "randomValue").toCompletableFuture().join();
262 | Assert.assertEquals(1, cache.getEventQueue().size());
263 | assert cache.getEventQueue().get(0) instanceof Write;
264 | Assert.assertEquals("randomValue", dataMap.get("randomKey"));
265 | }
266 |
267 | @Test
268 | public void EagerLoading() {
269 | final var eagerlyLoad = new HashSet();
270 | eagerlyLoad.add(PROFILE_MUMBAI_ENGINEER);
271 | eagerlyLoad.add(PROFILE_HYDERABAD_ENGINEER);
272 | final var cache = new CacheBuilder()
273 | .loadKeysOnStart(eagerlyLoad)
274 | .dataSource(dataSource)
275 | .build();
276 | Assert.assertEquals(2, cache.getEventQueue().size());
277 | assert cache.getEventQueue().get(0) instanceof Load;
278 | assert cache.getEventQueue().get(1) instanceof Load;
279 | cache.getEventQueue().clear();
280 | dataMap.clear();
281 | isEqualTo(cache.get(PROFILE_MUMBAI_ENGINEER), "violet");
282 | isEqualTo(cache.get(PROFILE_HYDERABAD_ENGINEER), "blue");
283 | Assert.assertEquals(0, cache.getEventQueue().size());
284 | }
285 |
286 | @Test
287 | public void RaceConditions() throws ExecutionException, InterruptedException {
288 | final var cache = new CacheBuilder()
289 | .poolSize(8)
290 | .dataSource(dataSource).build();
291 | final var cacheEntries = new HashMap>();
292 | final var numberOfEntries = 100;
293 | final var numberOfValues = 1000;
294 | final String[] keyList = new String[numberOfEntries];
295 | final Map inverseMapping = new HashMap<>();
296 | for (int entry = 0; entry < numberOfEntries; entry++) {
297 | final var key = UUID.randomUUID().toString();
298 | keyList[entry] = key;
299 | inverseMapping.put(key, entry);
300 | cacheEntries.put(key, new ArrayList<>());
301 | final var firstValue = UUID.randomUUID().toString();
302 | dataMap.put(key, firstValue);
303 | cacheEntries.get(key).add(firstValue);
304 | for (int value = 0; value < numberOfValues - 1; value++) {
305 | cacheEntries.get(key).add(UUID.randomUUID().toString());
306 | }
307 | }
308 | final Random random = new Random();
309 | final List> futures = new ArrayList<>();
310 | final List queries = new ArrayList<>();
311 | final int[] updates = new int[numberOfEntries];
312 | for (int i = 0; i < 1000000; i++) {
313 | final var index = random.nextInt(numberOfEntries);
314 | final var key = keyList[index];
315 | if (Math.random() <= 0.05) {
316 | if (updates[index] - 1 < numberOfEntries) {
317 | updates[index]++;
318 | }
319 | cache.set(key, cacheEntries.get(key).get(updates[index] + 1));
320 | } else {
321 | queries.add(key);
322 | futures.add(cache.get(key));
323 | }
324 | }
325 | final CompletionStage> results = CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new))
326 | .thenApply(__ -> futures.stream()
327 | .map(CompletionStage::toCompletableFuture)
328 | .map(CompletableFuture::join)
329 | .collect(Collectors.toList()));
330 | final int[] currentIndexes = new int[numberOfEntries];
331 | final StringBuilder stringBuilder = new StringBuilder();
332 | results.thenAccept(values -> {
333 | for (int i = 0; i < values.size(); i++) {
334 | final var key = queries.get(i);
335 | final var possibleValuesForKey = cacheEntries.get(key);
336 | final var currentValue = currentIndexes[inverseMapping.get(key)];
337 | if (!possibleValuesForKey.get(currentValue).equals(values.get(i))) {
338 | int offset = 1;
339 | while (currentValue + offset < numberOfValues && !possibleValuesForKey.get(currentValue + offset).equals(values.get(i))) {
340 | offset++;
341 | }
342 | if (currentValue + offset == numberOfValues) {
343 | System.out.println(Arrays.stream(stringBuilder.toString().split("\n")).filter(line -> line.contains(key)).collect(Collectors.joining("\n")));
344 | System.err.println(key);
345 | System.err.println(possibleValuesForKey);
346 | System.err.println(possibleValuesForKey.get(currentValue) + " index: " + currentIndexes[inverseMapping.get(key)]);
347 | System.err.println(values.get(i));
348 | throw new IllegalStateException();
349 | }
350 | currentIndexes[inverseMapping.get(key)] += offset;
351 | stringBuilder.append(key).append(" index: ").append(currentIndexes[inverseMapping.get(key)]).append(" ").append(values.get(i)).append('\n');
352 | }
353 | }
354 | }).toCompletableFuture().join();
355 | }
356 |
357 | private boolean isEqualTo(CompletionStage future, String value) {
358 | return future.thenApply(result -> {
359 | if (result.equals(value)) {
360 | return true;
361 | } else {
362 | throw new AssertionError();
363 | }
364 | }).toCompletableFuture().join();
365 | }
366 | }
367 |
--------------------------------------------------------------------------------
/distributed-cache/src/test/java/models/SettableTimer.java:
--------------------------------------------------------------------------------
1 | package models;
2 |
3 | public class SettableTimer extends Timer {
4 | private long time = -1;
5 |
6 | @Override
7 | public long getCurrentTime() {
8 | return time == -1 ? System.nanoTime() : time;
9 | }
10 |
11 | public void setTime(long time) {
12 | this.time = time;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/distributed-cache/target/classes/Cache.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding-parrot/Low-Level-Design/b78735e2824db1e8b7a18699681a328f125edef6/distributed-cache/target/classes/Cache.class
--------------------------------------------------------------------------------
/distributed-cache/target/classes/CacheBuilder.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding-parrot/Low-Level-Design/b78735e2824db1e8b7a18699681a328f125edef6/distributed-cache/target/classes/CacheBuilder.class
--------------------------------------------------------------------------------
/distributed-cache/target/classes/DataSource.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding-parrot/Low-Level-Design/b78735e2824db1e8b7a18699681a328f125edef6/distributed-cache/target/classes/DataSource.class
--------------------------------------------------------------------------------
/distributed-cache/target/classes/events/Event.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding-parrot/Low-Level-Design/b78735e2824db1e8b7a18699681a328f125edef6/distributed-cache/target/classes/events/Event.class
--------------------------------------------------------------------------------
/distributed-cache/target/classes/events/Eviction$Type.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding-parrot/Low-Level-Design/b78735e2824db1e8b7a18699681a328f125edef6/distributed-cache/target/classes/events/Eviction$Type.class
--------------------------------------------------------------------------------
/distributed-cache/target/classes/events/Eviction.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding-parrot/Low-Level-Design/b78735e2824db1e8b7a18699681a328f125edef6/distributed-cache/target/classes/events/Eviction.class
--------------------------------------------------------------------------------
/distributed-cache/target/classes/events/Load.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding-parrot/Low-Level-Design/b78735e2824db1e8b7a18699681a328f125edef6/distributed-cache/target/classes/events/Load.class
--------------------------------------------------------------------------------
/distributed-cache/target/classes/events/Update.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding-parrot/Low-Level-Design/b78735e2824db1e8b7a18699681a328f125edef6/distributed-cache/target/classes/events/Update.class
--------------------------------------------------------------------------------
/distributed-cache/target/classes/events/Write.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding-parrot/Low-Level-Design/b78735e2824db1e8b7a18699681a328f125edef6/distributed-cache/target/classes/events/Write.class
--------------------------------------------------------------------------------
/distributed-cache/target/classes/models/AccessDetails.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding-parrot/Low-Level-Design/b78735e2824db1e8b7a18699681a328f125edef6/distributed-cache/target/classes/models/AccessDetails.class
--------------------------------------------------------------------------------
/distributed-cache/target/classes/models/EvictionAlgorithm.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding-parrot/Low-Level-Design/b78735e2824db1e8b7a18699681a328f125edef6/distributed-cache/target/classes/models/EvictionAlgorithm.class
--------------------------------------------------------------------------------
/distributed-cache/target/classes/models/FetchAlgorithm.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding-parrot/Low-Level-Design/b78735e2824db1e8b7a18699681a328f125edef6/distributed-cache/target/classes/models/FetchAlgorithm.class
--------------------------------------------------------------------------------
/distributed-cache/target/classes/models/Record.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding-parrot/Low-Level-Design/b78735e2824db1e8b7a18699681a328f125edef6/distributed-cache/target/classes/models/Record.class
--------------------------------------------------------------------------------
/distributed-cache/target/classes/models/Timer.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding-parrot/Low-Level-Design/b78735e2824db1e8b7a18699681a328f125edef6/distributed-cache/target/classes/models/Timer.class
--------------------------------------------------------------------------------
/distributed-cache/target/test-classes/TestCache$1.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding-parrot/Low-Level-Design/b78735e2824db1e8b7a18699681a328f125edef6/distributed-cache/target/test-classes/TestCache$1.class
--------------------------------------------------------------------------------
/distributed-cache/target/test-classes/TestCache$2.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding-parrot/Low-Level-Design/b78735e2824db1e8b7a18699681a328f125edef6/distributed-cache/target/test-classes/TestCache$2.class
--------------------------------------------------------------------------------
/distributed-cache/target/test-classes/TestCache.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding-parrot/Low-Level-Design/b78735e2824db1e8b7a18699681a328f125edef6/distributed-cache/target/test-classes/TestCache.class
--------------------------------------------------------------------------------
/distributed-cache/target/test-classes/models/SettableTimer.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding-parrot/Low-Level-Design/b78735e2824db1e8b7a18699681a328f125edef6/distributed-cache/target/test-classes/models/SettableTimer.class
--------------------------------------------------------------------------------
/distributed-event-bus/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/distributed-event-bus/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/distributed-event-bus/.idea/jarRepositories.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/distributed-event-bus/.idea/libraries/Maven__aopalliance_aopalliance_1_0.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/distributed-event-bus/.idea/libraries/Maven__com_google_code_gson_gson_2_8_6.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/distributed-event-bus/.idea/libraries/Maven__com_google_guava_guava_16_0_1.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/distributed-event-bus/.idea/libraries/Maven__com_google_inject_guice_4_0.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/distributed-event-bus/.idea/libraries/Maven__javax_inject_javax_inject_1.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/distributed-event-bus/.idea/libraries/Maven__junit_junit_4_13.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/distributed-event-bus/.idea/libraries/Maven__org_hamcrest_hamcrest_core_1_3.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/distributed-event-bus/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/distributed-event-bus/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/distributed-event-bus/.idea/uiDesigner.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | -
6 |
7 |
8 | -
9 |
10 |
11 | -
12 |
13 |
14 | -
15 |
16 |
17 | -
18 |
19 |
20 |
21 |
22 |
23 | -
24 |
25 |
26 |
27 |
28 |
29 | -
30 |
31 |
32 |
33 |
34 |
35 | -
36 |
37 |
38 |
39 |
40 |
41 | -
42 |
43 |
44 |
45 |
46 | -
47 |
48 |
49 |
50 |
51 | -
52 |
53 |
54 |
55 |
56 | -
57 |
58 |
59 |
60 |
61 | -
62 |
63 |
64 |
65 |
66 | -
67 |
68 |
69 |
70 |
71 | -
72 |
73 |
74 | -
75 |
76 |
77 |
78 |
79 | -
80 |
81 |
82 |
83 |
84 | -
85 |
86 |
87 |
88 |
89 | -
90 |
91 |
92 |
93 |
94 | -
95 |
96 |
97 |
98 |
99 | -
100 |
101 |
102 | -
103 |
104 |
105 | -
106 |
107 |
108 | -
109 |
110 |
111 | -
112 |
113 |
114 |
115 |
116 | -
117 |
118 |
119 | -
120 |
121 |
122 |
123 |
124 |
--------------------------------------------------------------------------------
/distributed-event-bus/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/distributed-event-bus/README.ME:
--------------------------------------------------------------------------------
1 | 1) Multiple publishers and subscribers (Register from any class to eventbus)
2 | 2) Causal ordering of topics
3 | 3) Supports configurable retry attempts.
4 | 4) Have a dead letter queue.
5 | 5) Idempotency on event receiving
6 | 6) Allow both pull and push models
7 | 7) Allow subscribing from a timestamp or offset
8 | 8) Allow preconditions for event subscription
9 |
10 | Optional features:
11 | 1) Add database support, with flushing on threshold.
12 | 2) Retrieve K elements together for subscriber.
13 |
--------------------------------------------------------------------------------
/distributed-event-bus/distributed-event-bus.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/distributed-event-bus/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | interviewready.io
8 | distributed-event-bus
9 | 1.0
10 |
11 |
12 |
13 | org.apache.maven.plugins
14 | maven-compiler-plugin
15 |
16 | 11
17 | 11
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | junit
26 | junit
27 | 4.13.1
28 | test
29 |
30 |
31 | com.google.inject
32 | guice
33 | 4.0
34 |
35 |
36 | com.google.code.gson
37 | gson
38 | 2.8.9
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/distributed-event-bus/src/main/java/EventBus.java:
--------------------------------------------------------------------------------
1 | import com.google.inject.Inject;
2 | import com.google.inject.Singleton;
3 | import exceptions.RetryLimitExceededException;
4 | import exceptions.UnsubscribedPollException;
5 | import lib.KeyedExecutor;
6 | import models.Event;
7 | import models.FailureEvent;
8 | import models.Subscription;
9 | import util.Timer;
10 |
11 | import java.util.ArrayList;
12 | import java.util.HashMap;
13 | import java.util.List;
14 | import java.util.Map;
15 | import java.util.concurrent.*;
16 | import java.util.function.Function;
17 | import java.util.function.Predicate;
18 |
19 | @Singleton
20 | public class EventBus {
21 | private final Map> topics;
22 | private final Map> eventIndexes;
23 | private final Map> eventTimestamps;
24 | private final Map> pullSubscriptions;
25 | private final Map> pushSubscriptions;
26 | private final KeyedExecutor eventExecutor;
27 | private final KeyedExecutor broadcastExecutor;
28 | private EventBus deadLetterQueue;
29 | private final Timer timer;
30 |
31 | @Inject
32 | public EventBus(final KeyedExecutor eventExecutor, final KeyedExecutor broadcastExecutor, final Timer timer) {
33 | this.topics = new ConcurrentHashMap<>();
34 | this.eventIndexes = new ConcurrentHashMap<>();
35 | this.eventTimestamps = new ConcurrentHashMap<>();
36 | this.pullSubscriptions = new ConcurrentHashMap<>();
37 | this.pushSubscriptions = new ConcurrentHashMap<>();
38 | this.eventExecutor = eventExecutor;
39 | this.broadcastExecutor = broadcastExecutor;
40 | this.timer = timer;
41 | }
42 |
43 | public void setDeadLetterQueue(final EventBus deadLetterQueue) {
44 | this.deadLetterQueue = deadLetterQueue;
45 | }
46 |
47 | public CompletionStage publish(final String topic, final Event event) {
48 | return eventExecutor.getThreadFor(topic, publishToBus(topic, event));
49 | }
50 |
51 | private CompletionStage publishToBus(final String topic, final Event event) {
52 | if (eventIndexes.containsKey(topic) && eventIndexes.get(topic).containsKey(event.getId())) {
53 | return null;
54 | }
55 | topics.putIfAbsent(topic, new CopyOnWriteArrayList<>());
56 | eventIndexes.putIfAbsent(topic, new ConcurrentHashMap<>());
57 | eventIndexes.get(topic).put(event.getId(), topics.get(topic).size());
58 | eventTimestamps.putIfAbsent(topic, new ConcurrentSkipListMap<>());
59 | eventTimestamps.get(topic).put(timer.getCurrentTime(), event.getId());
60 | topics.get(topic).add(event);
61 | return notifyPushSubscribers(topic, event);
62 | }
63 |
64 | private CompletionStage notifyPushSubscribers(String topic, Event event) {
65 | if (!pushSubscriptions.containsKey(topic)) {
66 | return CompletableFuture.completedStage(null);
67 | }
68 | final var subscribersForTopic = pushSubscriptions.get(topic);
69 | final var notifications = subscribersForTopic.values()
70 | .stream()
71 | .filter(subscription -> subscription.getPrecondition().test(event))
72 | .map(subscription -> executeEventHandler(event, subscription))
73 | .toArray(CompletableFuture[]::new);
74 | return CompletableFuture.allOf(notifications);
75 | }
76 |
77 | private CompletionStage executeEventHandler(final Event event, Subscription subscription) {
78 | return broadcastExecutor.getThreadFor(subscription.getTopic() + subscription.getSubscriber(),
79 | doWithRetry(event, subscription.getEventHandler(),
80 | 1, subscription.getNumberOfRetries())
81 | .exceptionally(throwable -> {
82 | if (deadLetterQueue != null) {
83 | deadLetterQueue.publish(subscription.getTopic(), new FailureEvent(event, throwable, timer.getCurrentTime()));
84 | }
85 | return null;
86 | }));
87 | }
88 |
89 | private CompletionStage doWithRetry(final Event event,
90 | final Function> task,
91 | final int coolDownIntervalInMillis,
92 | final int remainingTries) {
93 | return task.apply(event).handle((__, throwable) -> {
94 | if (throwable != null) {
95 | if (remainingTries == 1) {
96 | throw new RetryLimitExceededException(throwable);
97 | }
98 | try {
99 | Thread.sleep(coolDownIntervalInMillis);
100 | } catch (InterruptedException e) {
101 | throw new RuntimeException(e);
102 | }
103 | return doWithRetry(event, task, Math.max(coolDownIntervalInMillis * 2, 10), remainingTries - 1);
104 | } else {
105 | return CompletableFuture.completedFuture((Void) null);
106 | }
107 | }).thenCompose(Function.identity());
108 | }
109 |
110 |
111 | public CompletionStage poll(final String topic, final String subscriber) {
112 | return eventExecutor.getThreadFor(topic + subscriber, () -> pollBus(topic, subscriber));
113 | }
114 |
115 | private Event pollBus(final String topic, final String subscriber) {
116 | var subscription = pullSubscriptions.getOrDefault(topic, new HashMap<>()).get(subscriber);
117 | if (subscription == null) {
118 | throw new UnsubscribedPollException();
119 | }
120 | for (var index = subscription.getCurrentIndex(); index.intValue() < topics.get(topic).size(); index.increment()) {
121 | var event = topics.get(topic).get(index.intValue());
122 | if (subscription.getPrecondition().test(event)) {
123 | index.increment();
124 | return event;
125 | }
126 | }
127 | return null;
128 | }
129 |
130 | public CompletionStage subscribeToEventsAfter(final String topic, final String subscriber, final long timeStamp) {
131 | return eventExecutor.getThreadFor(topic + subscriber, () -> moveIndexAtTimestamp(topic, subscriber, timeStamp));
132 | }
133 |
134 | private void moveIndexAtTimestamp(final String topic, final String subscriber, final long timeStamp) {
135 | final var closestEventAfter = eventTimestamps.get(topic).higherEntry(timeStamp);
136 | if (closestEventAfter == null) {
137 | pullSubscriptions.get(topic).get(subscriber).setCurrentIndex(eventIndexes.get(topic).size());
138 | } else {
139 | final var eventIndex = eventIndexes.get(topic).get(closestEventAfter.getValue());
140 | pullSubscriptions.get(topic).get(subscriber).setCurrentIndex(eventIndex);
141 | }
142 | }
143 |
144 | public CompletionStage subscribeToEventsAfter(final String topic, final String subscriber, final String eventId) {
145 | return eventExecutor.getThreadFor(topic + subscriber, () -> moveIndexAfterEvent(topic, subscriber, eventId));
146 | }
147 |
148 | private void moveIndexAfterEvent(final String topic, final String subscriber, final String eventId) {
149 | if (eventId == null) {
150 | pullSubscriptions.get(topic).get(subscriber).setCurrentIndex(0);
151 | } else {
152 | final var eventIndex = eventIndexes.get(topic).get(eventId) + 1;
153 | pullSubscriptions.get(topic).get(subscriber).setCurrentIndex(eventIndex);
154 | }
155 | }
156 |
157 | public CompletionStage subscribeForPush(final String topic,
158 | final String subscriber,
159 | final Predicate precondition,
160 | final Function> handler,
161 | final int numberOfRetries) {
162 | return eventExecutor.getThreadFor(topic + subscriber,
163 | () -> subscribeForPushEvents(topic, subscriber, precondition, handler, numberOfRetries));
164 | }
165 |
166 | private void subscribeForPushEvents(final String topic,
167 | final String subscriber,
168 | final Predicate precondition,
169 | final Function> handler,
170 | final int numberOfRetries) {
171 | addSubscriber(pushSubscriptions, subscriber, precondition, topic, handler, numberOfRetries);
172 | }
173 |
174 | private void addSubscriber(final Map> pullSubscriptions,
175 | final String subscriber,
176 | final Predicate precondition,
177 | final String topic,
178 | final Function> handler,
179 | final int numberOfRetries) {
180 | pullSubscriptions.putIfAbsent(topic, new ConcurrentHashMap<>());
181 | final var subscription = new Subscription(topic, subscriber, precondition, handler, numberOfRetries);
182 | subscription.setCurrentIndex(topics.getOrDefault(topic, new ArrayList<>()).size());
183 | pullSubscriptions.get(topic).put(subscriber, subscription);
184 | }
185 |
186 | public CompletionStage subscribeForPull(final String topic, final String subscriber, final Predicate precondition) {
187 | return eventExecutor.getThreadFor(topic + subscriber, () -> subscribeForPullEvents(topic, subscriber, precondition));
188 | }
189 |
190 | private void subscribeForPullEvents(final String topic, final String subscriber, final Predicate precondition) {
191 | addSubscriber(pullSubscriptions, subscriber, precondition, topic, null, 0);
192 | }
193 |
194 | public CompletionStage unsubscribe(final String topic, final String subscriber) {
195 | return eventExecutor.getThreadFor(topic + subscriber, () -> unsubscribeFromTopic(topic, subscriber));
196 | }
197 |
198 | private void unsubscribeFromTopic(final String topic, final String subscriber) {
199 | pushSubscriptions.getOrDefault(topic, new HashMap<>()).remove(subscriber);
200 | pullSubscriptions.getOrDefault(topic, new HashMap<>()).remove(subscriber);
201 | }
202 | }
203 |
204 |
--------------------------------------------------------------------------------
/distributed-event-bus/src/main/java/exceptions/RetryLimitExceededException.java:
--------------------------------------------------------------------------------
1 | package exceptions;
2 |
3 | public class RetryLimitExceededException extends RuntimeException {
4 | public RetryLimitExceededException(Throwable cause) {
5 | super(cause);
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/distributed-event-bus/src/main/java/exceptions/UnsubscribedPollException.java:
--------------------------------------------------------------------------------
1 | package exceptions;
2 |
3 | public class UnsubscribedPollException extends RuntimeException {
4 | }
5 |
--------------------------------------------------------------------------------
/distributed-event-bus/src/main/java/lib/KeyedExecutor.java:
--------------------------------------------------------------------------------
1 | package lib;
2 |
3 | import java.util.concurrent.CompletableFuture;
4 | import java.util.concurrent.CompletionStage;
5 | import java.util.concurrent.Executor;
6 | import java.util.concurrent.Executors;
7 | import java.util.function.Function;
8 | import java.util.function.Supplier;
9 |
10 | public class KeyedExecutor {
11 | private final Executor[] executorPool;
12 |
13 | public KeyedExecutor(final int poolSize) {
14 | this.executorPool = new Executor[poolSize];
15 | for (int i = 0; i < poolSize; i++) {
16 | executorPool[i] = Executors.newSingleThreadExecutor();
17 | }
18 | }
19 |
20 | public CompletionStage getThreadFor(KEY key, Runnable task) {
21 | return CompletableFuture.runAsync(task, executorPool[Math.abs(key.hashCode() % executorPool.length)]);
22 | }
23 |
24 | public CompletionStage getThreadFor(KEY key, Supplier task) {
25 | return CompletableFuture.supplyAsync(task, executorPool[Math.abs(key.hashCode() % executorPool.length)]);
26 | }
27 |
28 | public CompletionStage getThreadFor(KEY key, CompletionStage task) {
29 | return CompletableFuture.supplyAsync(() -> task, executorPool[Math.abs(key.hashCode() % executorPool.length)]).thenCompose(Function.identity());
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/distributed-event-bus/src/main/java/models/Event.java:
--------------------------------------------------------------------------------
1 | package models;
2 |
3 | import java.util.Objects;
4 | import java.util.UUID;
5 |
6 | public class Event {
7 | private final String id;
8 | private final String publisher;
9 | private final EventType eventType;
10 | private final String description;
11 | private final long creationTime;
12 |
13 | public Event(final String publisher,
14 | final EventType eventType,
15 | final String description,
16 | final long creationTime) {
17 | this.description = description;
18 | this.id = UUID.randomUUID().toString();
19 | this.publisher = publisher;
20 | this.eventType = eventType;
21 | this.creationTime = creationTime;
22 | }
23 |
24 | public String getId() {
25 | return id;
26 | }
27 |
28 | public String getPublisher() {
29 | return publisher;
30 | }
31 |
32 | public EventType getEventType() {
33 | return eventType;
34 | }
35 |
36 | public long getCreationTime() {
37 | return creationTime;
38 | }
39 |
40 | public String getDescription() {
41 | return description;
42 | }
43 | }
--------------------------------------------------------------------------------
/distributed-event-bus/src/main/java/models/EventType.java:
--------------------------------------------------------------------------------
1 | package models;
2 |
3 | public enum EventType {
4 | PRIORITY, LOGGING, ERROR
5 | }
6 |
--------------------------------------------------------------------------------
/distributed-event-bus/src/main/java/models/FailureEvent.java:
--------------------------------------------------------------------------------
1 | package models;
2 |
3 | public class FailureEvent extends Event {
4 | private final Event event;
5 | private final Throwable throwable;
6 |
7 | public FailureEvent(Event event, Throwable throwable, long failureTimestamp) {
8 | super("dead-letter-queue", EventType.ERROR, throwable.getMessage(), failureTimestamp);
9 | this.event = event;
10 | this.throwable = throwable;
11 | }
12 |
13 | public Event getEvent() {
14 | return event;
15 | }
16 |
17 | public Throwable getThrowable() {
18 | return throwable;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/distributed-event-bus/src/main/java/models/Subscription.java:
--------------------------------------------------------------------------------
1 | package models;
2 |
3 | import java.util.concurrent.CompletionStage;
4 | import java.util.concurrent.atomic.LongAdder;
5 | import java.util.function.Function;
6 | import java.util.function.Predicate;
7 |
8 | public class Subscription {
9 | private final String topic;
10 | private final String subscriber;
11 | private final Predicate precondition;
12 | private final Function> eventHandler;
13 | private final int numberOfRetries;
14 | private final LongAdder currentIndex;
15 |
16 | public Subscription(final String topic,
17 | final String subscriber,
18 | final Predicate precondition,
19 | final Function> eventHandler,
20 | final int numberOfRetries) {
21 | this.topic = topic;
22 | this.subscriber = subscriber;
23 | this.precondition = precondition;
24 | this.eventHandler = eventHandler;
25 | this.currentIndex = new LongAdder();
26 | this.numberOfRetries = numberOfRetries;
27 | }
28 |
29 | public String getTopic() {
30 | return topic;
31 | }
32 |
33 | public String getSubscriber() {
34 | return subscriber;
35 | }
36 |
37 | public Predicate getPrecondition() {
38 | return precondition;
39 | }
40 |
41 | public Function> getEventHandler() {
42 | return eventHandler;
43 | }
44 |
45 | public LongAdder getCurrentIndex() {
46 | return currentIndex;
47 | }
48 |
49 | public void setCurrentIndex(final int offset) {
50 | currentIndex.reset();
51 | currentIndex.add(offset);
52 | }
53 |
54 | public int getNumberOfRetries() {
55 | return numberOfRetries;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/distributed-event-bus/src/main/java/util/Timer.java:
--------------------------------------------------------------------------------
1 | package util;
2 |
3 | import com.google.inject.Singleton;
4 |
5 | @Singleton
6 | public class Timer {
7 | public long getCurrentTime() {
8 | return System.nanoTime();
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/distributed-event-bus/src/main/resources/application.properties:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding-parrot/Low-Level-Design/b78735e2824db1e8b7a18699681a328f125edef6/distributed-event-bus/src/main/resources/application.properties
--------------------------------------------------------------------------------
/distributed-event-bus/src/test/java/EventBusTest.java:
--------------------------------------------------------------------------------
1 | import com.google.gson.Gson;
2 | import exceptions.RetryLimitExceededException;
3 | import exceptions.UnsubscribedPollException;
4 | import lib.KeyedExecutor;
5 | import models.Event;
6 | import models.EventType;
7 | import models.FailureEvent;
8 | import org.junit.Assert;
9 | import org.junit.Before;
10 | import org.junit.Test;
11 | import util.Timer;
12 |
13 | import java.time.Duration;
14 | import java.util.ArrayList;
15 | import java.util.List;
16 | import java.util.UUID;
17 | import java.util.concurrent.CompletableFuture;
18 | import java.util.concurrent.atomic.AtomicLong;
19 |
20 |
21 | // Causal ordering of topics
22 |
23 | public class EventBusTest {
24 | public static final String TOPIC_1 = "topic-1";
25 | public static final String TOPIC_2 = "topic-2";
26 | public static final String PUBLISHER_1 = "publisher-1";
27 | public static final String SUBSCRIBER_1 = "subscriber-1";
28 | public static final String SUBSCRIBER_2 = "subscriber-2";
29 | private Timer timer;
30 | private KeyedExecutor keyedExecutor;
31 | private KeyedExecutor broadcastExecutor;
32 |
33 | @Before
34 | public void setUp() {
35 | keyedExecutor = new KeyedExecutor<>(16);
36 | broadcastExecutor = new KeyedExecutor<>(16);
37 | timer = new Timer();
38 | }
39 |
40 | private Event constructEvent(EventType priority, String description) {
41 | return new Event(PUBLISHER_1, priority, description, timer.getCurrentTime());
42 | }
43 |
44 | @Test
45 | public void defaultBehavior() {
46 | final EventBus eventBus = new EventBus(keyedExecutor, broadcastExecutor, timer);
47 | eventBus.publish(TOPIC_1, constructEvent(EventType.LOGGING, "first event"));
48 | eventBus.subscribeForPull(TOPIC_1, SUBSCRIBER_1, (event) -> true).toCompletableFuture().join();
49 | Assert.assertNull(eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join());
50 |
51 | eventBus.publish(TOPIC_1, constructEvent(EventType.PRIORITY, "second event"));
52 | final Event secondEvent = eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join();
53 |
54 | Assert.assertEquals(EventType.PRIORITY, secondEvent.getEventType());
55 | Assert.assertEquals("second event", secondEvent.getDescription());
56 |
57 | eventBus.subscribeToEventsAfter(TOPIC_1, SUBSCRIBER_1, null).toCompletableFuture().join();
58 | final Event firstEvent = eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join();
59 |
60 | Assert.assertEquals(EventType.LOGGING, firstEvent.getEventType());
61 | Assert.assertEquals("first event", firstEvent.getDescription());
62 | Assert.assertEquals(PUBLISHER_1, firstEvent.getPublisher());
63 |
64 | final List eventCollector = new ArrayList<>();
65 | eventBus.subscribeForPush(TOPIC_1,
66 | SUBSCRIBER_2,
67 | (event) -> true,
68 | (event) -> CompletableFuture.runAsync(() -> eventCollector.add(event)),
69 | 0).toCompletableFuture().join();
70 | eventBus.publish(TOPIC_1, constructEvent(EventType.ERROR, "third event")).toCompletableFuture().join();
71 |
72 | Assert.assertEquals(EventType.ERROR, eventCollector.get(0).getEventType());
73 | Assert.assertEquals("third event", eventCollector.get(0).getDescription());
74 |
75 | eventBus.unsubscribe(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join();
76 | eventBus.publish(TOPIC_1, constructEvent(EventType.LOGGING, "fourth event")).toCompletableFuture().join();
77 | Assert.assertTrue(eventBus.poll(TOPIC_1, SUBSCRIBER_1)
78 | .handle((__, throwable) -> throwable.getCause() instanceof UnsubscribedPollException)
79 | .toCompletableFuture().join());
80 |
81 | eventCollector.clear();
82 | eventBus.unsubscribe(TOPIC_1, SUBSCRIBER_2).toCompletableFuture().join();
83 | eventBus.publish(TOPIC_1, constructEvent(EventType.LOGGING, "fifth event")).toCompletableFuture().join();
84 | Assert.assertTrue(eventCollector.isEmpty());
85 | }
86 |
87 | @Test
88 | public void indexMove() {
89 | final EventBus eventBus = new EventBus(keyedExecutor, broadcastExecutor, timer);
90 | eventBus.subscribeForPull(TOPIC_1, SUBSCRIBER_1, (event) -> true).toCompletableFuture().join();
91 | final Event firstEvent = constructEvent(EventType.PRIORITY, "first event");
92 | final Event secondEvent = constructEvent(EventType.PRIORITY, "second event");
93 | final Event thirdEvent = constructEvent(EventType.PRIORITY, "third event");
94 | eventBus.publish(TOPIC_1, firstEvent).toCompletableFuture().join();
95 | eventBus.publish(TOPIC_1, secondEvent).toCompletableFuture().join();
96 | eventBus.publish(TOPIC_1, thirdEvent).toCompletableFuture().join();
97 |
98 | eventBus.subscribeToEventsAfter(TOPIC_1, SUBSCRIBER_1, secondEvent.getId()).toCompletableFuture().join();
99 | final Event firstPoll = eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join();
100 | Assert.assertEquals("third event", firstPoll.getDescription());
101 | Assert.assertNull(eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join());
102 |
103 | eventBus.subscribeToEventsAfter(TOPIC_1, SUBSCRIBER_1, null).toCompletableFuture().join();
104 | final Event secondPoll = eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join();
105 | Assert.assertEquals("first event", secondPoll.getDescription());
106 |
107 | eventBus.subscribeToEventsAfter(TOPIC_1, SUBSCRIBER_1, firstEvent.getId()).toCompletableFuture().join();
108 | final Event thirdPoll = eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join();
109 | Assert.assertEquals("second event", thirdPoll.getDescription());
110 |
111 | eventBus.subscribeToEventsAfter(TOPIC_1, SUBSCRIBER_1, thirdEvent.getId()).toCompletableFuture().join();
112 | Assert.assertNull(eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join());
113 | }
114 |
115 | @Test
116 | public void timestampMove() {
117 | final TestTimer timer = new TestTimer();
118 | final EventBus eventBus = new EventBus(keyedExecutor, broadcastExecutor, timer);
119 | eventBus.subscribeForPull(TOPIC_1, SUBSCRIBER_1, (event) -> true).toCompletableFuture().join();
120 |
121 | final Event firstEvent = new Event(PUBLISHER_1, EventType.PRIORITY, "first event", timer.getCurrentTime());
122 | eventBus.publish(TOPIC_1, firstEvent).toCompletableFuture().join();
123 | timer.setCurrentTime(timer.getCurrentTime() + Duration.ofSeconds(10).toNanos());
124 |
125 | final Event secondEvent = new Event(PUBLISHER_1, EventType.PRIORITY, "second event", timer.getCurrentTime());
126 | eventBus.publish(TOPIC_1, secondEvent).toCompletableFuture().join();
127 | timer.setCurrentTime(timer.getCurrentTime() + Duration.ofSeconds(10).toNanos());
128 |
129 | final Event thirdEvent = new Event(PUBLISHER_1, EventType.PRIORITY, "third event", timer.getCurrentTime());
130 | eventBus.publish(TOPIC_1, thirdEvent).toCompletableFuture().join();
131 |
132 | eventBus.subscribeToEventsAfter(TOPIC_1, SUBSCRIBER_1, secondEvent.getCreationTime() + Duration.ofSeconds(5).toNanos()).toCompletableFuture().join();
133 | final Event firstPoll = eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join();
134 | Assert.assertEquals("third event", firstPoll.getDescription());
135 | Assert.assertNull(eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join());
136 |
137 | eventBus.subscribeToEventsAfter(TOPIC_1, SUBSCRIBER_1, 0).toCompletableFuture().join();
138 | final Event secondPoll = eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join();
139 | Assert.assertEquals("first event", secondPoll.getDescription());
140 |
141 | eventBus.subscribeToEventsAfter(TOPIC_1, SUBSCRIBER_1, firstEvent.getCreationTime() + Duration.ofSeconds(5).toNanos()).toCompletableFuture().join();
142 | final Event thirdPoll = eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join();
143 | Assert.assertEquals("second event", thirdPoll.getDescription());
144 |
145 | eventBus.subscribeToEventsAfter(TOPIC_1, SUBSCRIBER_1, thirdEvent.getCreationTime() + Duration.ofNanos(1).toNanos()).toCompletableFuture().join();
146 | Assert.assertNull(eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join());
147 | }
148 |
149 | @Test
150 | public void idempotency() {
151 | final EventBus eventBus = new EventBus(keyedExecutor, broadcastExecutor, timer);
152 | eventBus.subscribeForPull(TOPIC_1, SUBSCRIBER_1, (event) -> true).toCompletableFuture().join();
153 | Event event1 = new Gson().fromJson("{\n" +
154 | " \"id\": \"event-5435\",\n" +
155 | " \"publisher\": \"random-publisher-1\",\n" +
156 | " \"eventType\": \"LOGGING\",\n" +
157 | " \"description\": \"random-event-1\",\n" +
158 | " \"creationTime\": 31884739810179363\n" +
159 | "}", Event.class);
160 | eventBus.publish(TOPIC_1, event1);
161 |
162 | Event event2 = new Gson().fromJson("{\n" +
163 | " \"id\": \"event-5435\",\n" +
164 | " \"publisher\": \"random-publisher-2\",\n" +
165 | " \"eventType\": \"PRIORITY\",\n" +
166 | " \"description\": \"random-event-2\",\n" +
167 | " \"creationTime\": 31824735510179363\n" +
168 | "}", Event.class);
169 | eventBus.publish(TOPIC_1, event2);
170 |
171 |
172 | final Event firstEvent = eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join();
173 | Assert.assertEquals(EventType.LOGGING, firstEvent.getEventType());
174 | Assert.assertEquals("random-event-1", firstEvent.getDescription());
175 | Assert.assertNull(eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join());
176 | }
177 |
178 | @Test
179 | public void unsubscribePushEvents() {
180 | final EventBus eventBus = new EventBus(keyedExecutor, broadcastExecutor, timer);
181 | final List topic1 = new ArrayList<>(), topic2 = new ArrayList<>();
182 | eventBus.subscribeForPush(TOPIC_1, SUBSCRIBER_1, event -> true, event -> {
183 | topic1.add(event);
184 | return CompletableFuture.completedStage(null);
185 | }, 0).toCompletableFuture().join();
186 | eventBus.subscribeForPush(TOPIC_2, SUBSCRIBER_1, event -> true, event -> {
187 | topic2.add(event);
188 | return CompletableFuture.completedStage(null);
189 | }, 0).toCompletableFuture().join();
190 |
191 | for (int i = 0; i < 3; i++) {
192 | eventBus.publish(TOPIC_1, constructEvent(EventType.PRIORITY, UUID.randomUUID().toString())).toCompletableFuture().join();
193 | }
194 | eventBus.publish(TOPIC_2, constructEvent(EventType.PRIORITY, UUID.randomUUID().toString())).toCompletableFuture().join();
195 | eventBus.unsubscribe(TOPIC_1, SUBSCRIBER_1);
196 | Assert.assertEquals(3, topic1.size());
197 | Assert.assertEquals(1, topic2.size());
198 |
199 | for (int i = 0; i < 2; i++) {
200 | eventBus.publish(TOPIC_2, constructEvent(EventType.PRIORITY, UUID.randomUUID().toString())).toCompletableFuture().join();
201 | }
202 | for (int i = 0; i < 3; i++) {
203 | eventBus.publish(TOPIC_1, constructEvent(EventType.PRIORITY, UUID.randomUUID().toString())).toCompletableFuture().join();
204 | }
205 | Assert.assertEquals(3, topic1.size());
206 | Assert.assertEquals(3, topic2.size());
207 |
208 | eventBus.subscribeForPush(TOPIC_1, SUBSCRIBER_1, event -> true, event -> {
209 | topic1.add(event);
210 | return CompletableFuture.completedStage(null);
211 | }, 0).toCompletableFuture().join();
212 | for (int i = 0; i < 3; i++) {
213 | eventBus.publish(TOPIC_1, constructEvent(EventType.PRIORITY, UUID.randomUUID().toString())).toCompletableFuture().join();
214 | }
215 | Assert.assertEquals(6, topic1.size());
216 | Assert.assertEquals(3, topic2.size());
217 | }
218 |
219 | @Test
220 | public void unsubscribePullEvents() {
221 | final EventBus eventBus = new EventBus(keyedExecutor, broadcastExecutor, timer);
222 | eventBus.subscribeForPull(TOPIC_1, SUBSCRIBER_1, event -> true).toCompletableFuture().join();
223 | eventBus.subscribeForPull(TOPIC_2, SUBSCRIBER_1, event -> true).toCompletableFuture().join();
224 | for (int i = 0; i < 3; i++) {
225 | eventBus.publish(TOPIC_1, constructEvent(EventType.PRIORITY, UUID.randomUUID().toString())).toCompletableFuture().join();
226 | }
227 | eventBus.publish(TOPIC_2, constructEvent(EventType.PRIORITY, UUID.randomUUID().toString())).toCompletableFuture().join();
228 |
229 | for (int i = 0; i < 3; i++) {
230 | Assert.assertNotNull(eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join());
231 | }
232 | Assert.assertNotNull(eventBus.poll(TOPIC_2, SUBSCRIBER_1).toCompletableFuture().join());
233 |
234 | eventBus.unsubscribe(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join();
235 | for (int i = 0; i < 2; i++) {
236 | eventBus.publish(TOPIC_2, constructEvent(EventType.PRIORITY, UUID.randomUUID().toString())).toCompletableFuture().join();
237 | }
238 | for (int i = 0; i < 3; i++) {
239 | eventBus.publish(TOPIC_1, constructEvent(EventType.PRIORITY, UUID.randomUUID().toString())).toCompletableFuture().join();
240 | }
241 |
242 | Assert.assertTrue(eventBus.poll(TOPIC_1, SUBSCRIBER_1)
243 | .handle((__, throwable) -> throwable.getCause() instanceof UnsubscribedPollException).toCompletableFuture().join());
244 | for (int i = 0; i < 2; i++) {
245 | Assert.assertNotNull(eventBus.poll(TOPIC_2, SUBSCRIBER_1).toCompletableFuture().join());
246 | }
247 |
248 | eventBus.subscribeForPull(TOPIC_1, SUBSCRIBER_1, event -> true).toCompletableFuture().join();
249 | for (int i = 0; i < 3; i++) {
250 | eventBus.publish(TOPIC_1, constructEvent(EventType.PRIORITY, UUID.randomUUID().toString())).toCompletableFuture().join();
251 | }
252 |
253 | for (int i = 0; i < 3; i++) {
254 | Assert.assertNotNull(eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join());
255 | }
256 | Assert.assertNull(eventBus.poll(TOPIC_2, SUBSCRIBER_1).toCompletableFuture().join());
257 | }
258 |
259 | @Test
260 | public void deadLetterQueue() {
261 | final EventBus eventBus = new EventBus(keyedExecutor, broadcastExecutor, timer);
262 | final EventBus dlq = new EventBus(new KeyedExecutor<>(3), new KeyedExecutor<>(3), new Timer());
263 | eventBus.setDeadLetterQueue(dlq);
264 | dlq.subscribeForPull(TOPIC_1, SUBSCRIBER_1, event -> event.getEventType().equals(EventType.ERROR));
265 | final AtomicLong attempts = new AtomicLong();
266 | final int maxTries = 5;
267 | eventBus.subscribeForPush(TOPIC_1, SUBSCRIBER_1, event -> true, event -> {
268 | attempts.incrementAndGet();
269 | return CompletableFuture.failedStage(new RuntimeException());
270 | }, maxTries).toCompletableFuture().join();
271 | final Event event = new Event(PUBLISHER_1, EventType.LOGGING, "random", timer.getCurrentTime());
272 | eventBus.publish(TOPIC_1, event).toCompletableFuture().join();
273 | Assert.assertEquals(5, attempts.intValue());
274 | final Event failureEvent = dlq.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join();
275 | Assert.assertTrue(failureEvent instanceof FailureEvent);
276 | Assert.assertEquals(event.getId(), ((FailureEvent) failureEvent).getEvent().getId());
277 | Assert.assertEquals(EventType.ERROR, failureEvent.getEventType());
278 | Assert.assertTrue(((FailureEvent) failureEvent).getThrowable().getCause() instanceof RetryLimitExceededException);
279 | }
280 |
281 | @Test
282 | public void retrySuccess() {
283 | final EventBus eventBus = new EventBus(keyedExecutor, broadcastExecutor, timer);
284 | final AtomicLong attempts = new AtomicLong();
285 | final int maxTries = 5;
286 | final List events = new ArrayList<>();
287 | eventBus.subscribeForPush(TOPIC_1, SUBSCRIBER_1, event -> true, event -> {
288 | if (attempts.incrementAndGet() == maxTries) {
289 | events.add(event);
290 | return CompletableFuture.completedStage(null);
291 | } else {
292 | return CompletableFuture.failedStage(new RuntimeException("TRY no: " + attempts.intValue()));
293 | }
294 | }, maxTries).toCompletableFuture().join();
295 | eventBus.publish(TOPIC_1, new Event(PUBLISHER_1, EventType.LOGGING, "random", timer.getCurrentTime())).toCompletableFuture().join();
296 |
297 | Assert.assertEquals(EventType.LOGGING, events.get(0).getEventType());
298 | Assert.assertEquals("random", events.get(0).getDescription());
299 | Assert.assertEquals(5, attempts.intValue());
300 | Assert.assertEquals(1, events.size());
301 | }
302 |
303 | @Test
304 | public void preconditionCheckForPush() {
305 | final EventBus eventBus = new EventBus(keyedExecutor, broadcastExecutor, timer);
306 | final List events = new ArrayList<>();
307 | eventBus.subscribeForPush(TOPIC_1, SUBSCRIBER_1, event -> event.getDescription().contains("-1"), event -> {
308 | events.add(event);
309 | return CompletableFuture.completedStage(null);
310 | }, 0).toCompletableFuture().join();
311 | eventBus.publish(TOPIC_1, new Event(PUBLISHER_1, EventType.LOGGING, "random-event-1", timer.getCurrentTime())).toCompletableFuture().join();
312 | eventBus.publish(TOPIC_1, new Event(PUBLISHER_1, EventType.LOGGING, "random-event-2", timer.getCurrentTime())).toCompletableFuture().join();
313 | eventBus.publish(TOPIC_1, new Event(PUBLISHER_1, EventType.LOGGING, "random-event-12", timer.getCurrentTime())).toCompletableFuture().join();
314 | eventBus.publish(TOPIC_1, new Event(PUBLISHER_1, EventType.LOGGING, "random-event-21", timer.getCurrentTime())).toCompletableFuture().join();
315 |
316 | Assert.assertEquals(events.size(), 2);
317 | Assert.assertEquals(EventType.LOGGING, events.get(0).getEventType());
318 | Assert.assertEquals("random-event-1", events.get(0).getDescription());
319 | Assert.assertEquals(EventType.LOGGING, events.get(1).getEventType());
320 | Assert.assertEquals("random-event-12", events.get(1).getDescription());
321 | }
322 |
323 | @Test
324 | public void preconditionCheckForPull() {
325 | final EventBus eventBus = new EventBus(keyedExecutor, broadcastExecutor, timer);
326 | eventBus.subscribeForPull(TOPIC_1, SUBSCRIBER_1, event -> event.getDescription().contains("-1")).toCompletableFuture().join();
327 | eventBus.publish(TOPIC_1, new Event(PUBLISHER_1, EventType.LOGGING, "random-event-1", timer.getCurrentTime())).toCompletableFuture().join();
328 | eventBus.publish(TOPIC_1, new Event(PUBLISHER_1, EventType.LOGGING, "random-event-2", timer.getCurrentTime())).toCompletableFuture().join();
329 | eventBus.publish(TOPIC_1, new Event(PUBLISHER_1, EventType.LOGGING, "random-event-12", timer.getCurrentTime())).toCompletableFuture().join();
330 | eventBus.publish(TOPIC_1, new Event(PUBLISHER_1, EventType.LOGGING, "random-event-21", timer.getCurrentTime())).toCompletableFuture().join();
331 |
332 | final Event event1 = eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join();
333 | Assert.assertEquals(EventType.LOGGING, event1.getEventType());
334 | Assert.assertEquals("random-event-1", event1.getDescription());
335 | final Event event2 = eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join();
336 | Assert.assertEquals(EventType.LOGGING, event2.getEventType());
337 | Assert.assertEquals("random-event-12", event2.getDescription());
338 | Assert.assertNull(eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join());
339 | }
340 | }
341 |
--------------------------------------------------------------------------------
/distributed-event-bus/src/test/java/TestTimer.java:
--------------------------------------------------------------------------------
1 | import util.Timer;
2 |
3 | public class TestTimer extends Timer {
4 | private long currentTime;
5 |
6 | public TestTimer() {
7 | this.currentTime = System.nanoTime();
8 | }
9 |
10 | @Override
11 | public long getCurrentTime() {
12 | return currentTime;
13 | }
14 |
15 | public void setCurrentTime(final long currentTime) {
16 | this.currentTime = currentTime;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/distributed-event-bus/target/classes/EventBus.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding-parrot/Low-Level-Design/b78735e2824db1e8b7a18699681a328f125edef6/distributed-event-bus/target/classes/EventBus.class
--------------------------------------------------------------------------------
/distributed-event-bus/target/classes/META-INF/distributed-event-bus.kotlin_module:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/distributed-event-bus/target/classes/application.properties:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding-parrot/Low-Level-Design/b78735e2824db1e8b7a18699681a328f125edef6/distributed-event-bus/target/classes/application.properties
--------------------------------------------------------------------------------
/distributed-event-bus/target/classes/exceptions/RetryLimitExceededException.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding-parrot/Low-Level-Design/b78735e2824db1e8b7a18699681a328f125edef6/distributed-event-bus/target/classes/exceptions/RetryLimitExceededException.class
--------------------------------------------------------------------------------
/distributed-event-bus/target/classes/exceptions/UnsubscribedPollException.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding-parrot/Low-Level-Design/b78735e2824db1e8b7a18699681a328f125edef6/distributed-event-bus/target/classes/exceptions/UnsubscribedPollException.class
--------------------------------------------------------------------------------
/distributed-event-bus/target/classes/lib/KeyedExecutor.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding-parrot/Low-Level-Design/b78735e2824db1e8b7a18699681a328f125edef6/distributed-event-bus/target/classes/lib/KeyedExecutor.class
--------------------------------------------------------------------------------
/distributed-event-bus/target/classes/models/Event.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding-parrot/Low-Level-Design/b78735e2824db1e8b7a18699681a328f125edef6/distributed-event-bus/target/classes/models/Event.class
--------------------------------------------------------------------------------
/distributed-event-bus/target/classes/models/EventType.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding-parrot/Low-Level-Design/b78735e2824db1e8b7a18699681a328f125edef6/distributed-event-bus/target/classes/models/EventType.class
--------------------------------------------------------------------------------
/distributed-event-bus/target/classes/models/FailureEvent.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding-parrot/Low-Level-Design/b78735e2824db1e8b7a18699681a328f125edef6/distributed-event-bus/target/classes/models/FailureEvent.class
--------------------------------------------------------------------------------
/distributed-event-bus/target/classes/models/Subscription.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding-parrot/Low-Level-Design/b78735e2824db1e8b7a18699681a328f125edef6/distributed-event-bus/target/classes/models/Subscription.class
--------------------------------------------------------------------------------
/distributed-event-bus/target/classes/util/Timer.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding-parrot/Low-Level-Design/b78735e2824db1e8b7a18699681a328f125edef6/distributed-event-bus/target/classes/util/Timer.class
--------------------------------------------------------------------------------
/distributed-event-bus/target/test-classes/EventBusTest.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding-parrot/Low-Level-Design/b78735e2824db1e8b7a18699681a328f125edef6/distributed-event-bus/target/test-classes/EventBusTest.class
--------------------------------------------------------------------------------
/distributed-event-bus/target/test-classes/TestTimer.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding-parrot/Low-Level-Design/b78735e2824db1e8b7a18699681a328f125edef6/distributed-event-bus/target/test-classes/TestTimer.class
--------------------------------------------------------------------------------
/rate-limiter/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/rate-limiter/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/rate-limiter/.idea/jarRepositories.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/rate-limiter/.idea/libraries/Maven__junit_junit_4_13.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/rate-limiter/.idea/libraries/Maven__org_hamcrest_hamcrest_core_1_3.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/rate-limiter/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/rate-limiter/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/rate-limiter/.idea/uiDesigner.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | -
6 |
7 |
8 | -
9 |
10 |
11 | -
12 |
13 |
14 | -
15 |
16 |
17 | -
18 |
19 |
20 |
21 |
22 |
23 | -
24 |
25 |
26 |
27 |
28 |
29 | -
30 |
31 |
32 |
33 |
34 |
35 | -
36 |
37 |
38 |
39 |
40 |
41 | -
42 |
43 |
44 |
45 |
46 | -
47 |
48 |
49 |
50 |
51 | -
52 |
53 |
54 |
55 |
56 | -
57 |
58 |
59 |
60 |
61 | -
62 |
63 |
64 |
65 |
66 | -
67 |
68 |
69 |
70 |
71 | -
72 |
73 |
74 | -
75 |
76 |
77 |
78 |
79 | -
80 |
81 |
82 |
83 |
84 | -
85 |
86 |
87 |
88 |
89 | -
90 |
91 |
92 |
93 |
94 | -
95 |
96 |
97 |
98 |
99 | -
100 |
101 |
102 | -
103 |
104 |
105 | -
106 |
107 |
108 | -
109 |
110 |
111 | -
112 |
113 |
114 |
115 |
116 | -
117 |
118 |
119 | -
120 |
121 |
122 |
123 |
124 |
--------------------------------------------------------------------------------
/rate-limiter/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/rate-limiter/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | interviewready.io
8 | rate-limiter
9 | 1.0
10 |
11 |
12 |
13 | org.apache.maven.plugins
14 | maven-compiler-plugin
15 |
16 | 10
17 | 10
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | junit
26 | junit
27 | 4.13.1
28 | test
29 |
30 |
31 |
--------------------------------------------------------------------------------
/rate-limiter/rate-limiter.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/rate-limiter/src/main/java/TimerWheel.java:
--------------------------------------------------------------------------------
1 | import exceptions.RateLimitExceededException;
2 | import models.Request;
3 | import utils.Timer;
4 |
5 | import java.util.Map;
6 | import java.util.concurrent.*;
7 |
8 | public class TimerWheel {
9 | private final int timeOutPeriod;
10 | private final int capacityPerSlot;
11 | private final TimeUnit timeUnit;
12 | private final ArrayBlockingQueue[] slots;
13 | private final Map reverseIndex;
14 | private final Timer timer;
15 | private final ExecutorService[] threads;
16 |
17 | public TimerWheel(final TimeUnit timeUnit,
18 | final int timeOutPeriod,
19 | final int capacityPerSlot,
20 | final Timer timer) {
21 | this.timeUnit = timeUnit;
22 | this.timeOutPeriod = timeOutPeriod;
23 | this.capacityPerSlot = capacityPerSlot;
24 | if (this.timeOutPeriod > 1000) {
25 | throw new IllegalArgumentException();
26 | }
27 | this.slots = new ArrayBlockingQueue[this.timeOutPeriod];
28 | this.threads = new ExecutorService[this.timeOutPeriod];
29 | this.reverseIndex = new ConcurrentHashMap<>();
30 | for (int i = 0; i < slots.length; i++) {
31 | slots[i] = new ArrayBlockingQueue<>(capacityPerSlot);
32 | threads[i] = Executors.newSingleThreadExecutor();
33 | }
34 | this.timer = timer;
35 | final long timePerSlot = TimeUnit.MILLISECONDS.convert(1, timeUnit);
36 | Executors.newSingleThreadScheduledExecutor()
37 | .scheduleAtFixedRate(this::flushRequests,
38 | timePerSlot - (this.timer.getCurrentTimeInMillis() % timePerSlot),
39 | timePerSlot, TimeUnit.MILLISECONDS);
40 | }
41 |
42 | public Future> flushRequests() {
43 | final int currentSlot = getCurrentSlot();
44 | return threads[currentSlot].submit(() -> {
45 | for (final Request request : slots[currentSlot]) {
46 | if (timer.getCurrentTime(timeUnit) - request.getStartTime() >= timeOutPeriod) {
47 | slots[currentSlot].remove(request);
48 | reverseIndex.remove(request.getRequestId());
49 | }
50 | }
51 | });
52 | }
53 |
54 | public Future> addRequest(final Request request) {
55 | final int currentSlot = getCurrentSlot();
56 | return threads[currentSlot].submit(() -> {
57 | if (slots[currentSlot].size() >= capacityPerSlot) {
58 | throw new RateLimitExceededException();
59 | }
60 | slots[currentSlot].add(request);
61 | reverseIndex.put(request.getRequestId(), currentSlot);
62 | });
63 | }
64 |
65 | public Future> evict(final String requestId) {
66 | final int currentSlot = reverseIndex.get(requestId);
67 | return threads[currentSlot].submit(() -> {
68 | slots[currentSlot].remove(new Request(requestId, 0));
69 | reverseIndex.remove(requestId);
70 | });
71 | }
72 |
73 | private int getCurrentSlot() {
74 | return (int) timer.getCurrentTime(timeUnit) % slots.length;
75 | }
76 | }
77 |
78 |
--------------------------------------------------------------------------------
/rate-limiter/src/main/java/exceptions/RateLimitExceededException.java:
--------------------------------------------------------------------------------
1 | package exceptions;
2 |
3 | public class RateLimitExceededException extends IllegalStateException {
4 | public RateLimitExceededException() {
5 | super("Rate limit exceeded");
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/rate-limiter/src/main/java/models/Request.java:
--------------------------------------------------------------------------------
1 | package models;
2 |
3 | import java.util.Objects;
4 |
5 | public class Request {
6 | private final String requestId;
7 | private final long startTime;
8 |
9 | public Request(String requestId, long startTime) {
10 | this.requestId = requestId;
11 | this.startTime = startTime;
12 | }
13 |
14 | public String getRequestId() {
15 | return requestId;
16 | }
17 |
18 | public long getStartTime() {
19 | return startTime;
20 | }
21 |
22 | @Override
23 | public boolean equals(Object o) {
24 | if (this == o) return true;
25 | if (o == null || getClass() != o.getClass()) return false;
26 | return requestId.equals(((Request) o).requestId);
27 | }
28 |
29 | @Override
30 | public int hashCode() {
31 | return requestId.hashCode();
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/rate-limiter/src/main/java/utils/Timer.java:
--------------------------------------------------------------------------------
1 | package utils;
2 |
3 | import java.util.concurrent.TimeUnit;
4 |
5 | public class Timer {
6 | public long getCurrentTime(final TimeUnit timeUnit) {
7 | return timeUnit.convert(getCurrentTimeInMillis(), TimeUnit.MILLISECONDS);
8 | }
9 |
10 | public long getCurrentTimeInMillis() {
11 | return System.currentTimeMillis();
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/rate-limiter/src/test/java/RateLimitTest.java:
--------------------------------------------------------------------------------
1 | import models.Request;
2 | import org.junit.Assert;
3 | import org.junit.Test;
4 |
5 | import java.util.concurrent.TimeUnit;
6 |
7 | public class RateLimitTest {
8 |
9 | @Test
10 | public void testDefaultBehaviour() throws Exception {
11 | final TimeUnit timeUnit = TimeUnit.SECONDS;
12 | final TestTimer timer = new TestTimer();
13 | final TimerWheel timerWheel = new TimerWheel(timeUnit, 6, 3, timer);
14 | timerWheel.addRequest(new Request("1", timer.getCurrentTime(timeUnit))).get();
15 | timerWheel.addRequest(new Request("2", timer.getCurrentTime(timeUnit))).get();
16 | timerWheel.addRequest(new Request("3", timer.getCurrentTime(timeUnit))).get();
17 | Throwable exception = null;
18 | try {
19 | timerWheel.addRequest(new Request("4", timer.getCurrentTime(timeUnit))).get();
20 | } catch (Exception e) {
21 | exception = e.getCause();
22 | }
23 | Assert.assertNotNull(exception);
24 | Assert.assertEquals("Rate limit exceeded", exception.getMessage());
25 | tick(timeUnit, timer, timerWheel);
26 | timerWheel.addRequest(new Request("4", timer.getCurrentTime(timeUnit))).get();
27 | timerWheel.addRequest(new Request("5", timer.getCurrentTime(timeUnit))).get();
28 | timerWheel.evict("1").get();
29 | timerWheel.evict("4").get();
30 | timerWheel.addRequest(new Request("6", timer.getCurrentTime(timeUnit))).get();
31 | timerWheel.addRequest(new Request("7", timer.getCurrentTime(timeUnit))).get();
32 | }
33 |
34 | @Test
35 | public void testClearing() throws Exception {
36 | final TimeUnit timeUnit = TimeUnit.SECONDS;
37 | final TestTimer timer = new TestTimer();
38 | final int timeOutPeriod = 6;
39 | final TimerWheel timerWheel = new TimerWheel(timeUnit, timeOutPeriod, 3, timer);
40 | timerWheel.addRequest(new Request("0", timer.getCurrentTime(timeUnit))).get();
41 | timerWheel.addRequest(new Request("1", timer.getCurrentTime(timeUnit))).get();
42 | timerWheel.addRequest(new Request("2", timer.getCurrentTime(timeUnit))).get();
43 |
44 | Throwable exception = null;
45 | try {
46 | timerWheel.addRequest(new Request("3", timer.getCurrentTime(timeUnit))).get();
47 | } catch (Exception e) {
48 | exception = e.getCause();
49 | }
50 | Assert.assertNotNull(exception);
51 | Assert.assertEquals("Rate limit exceeded", exception.getMessage());
52 |
53 | for (int i = 0; i < timeOutPeriod; i++) {
54 | tick(timeUnit, timer, timerWheel);
55 | }
56 | timerWheel.addRequest(new Request("4", timer.getCurrentTime(timeUnit))).get();
57 | timerWheel.addRequest(new Request("5", timer.getCurrentTime(timeUnit))).get();
58 | timerWheel.addRequest(new Request("6", timer.getCurrentTime(timeUnit))).get();
59 |
60 | exception = null;
61 | try {
62 | timerWheel.addRequest(new Request("7", timer.getCurrentTime(timeUnit))).get();
63 | } catch (Exception e) {
64 | exception = e.getCause();
65 | }
66 | Assert.assertNotNull(exception);
67 | Assert.assertEquals("Rate limit exceeded", exception.getMessage());
68 | }
69 |
70 | private void tick(TimeUnit timeUnit, TestTimer timer, TimerWheel timerWheel) throws Exception {
71 | timer.setTime(timer.getCurrentTimeInMillis() + TimeUnit.MILLISECONDS.convert(1, timeUnit));
72 | timerWheel.flushRequests().get();
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/rate-limiter/src/test/java/TestTimer.java:
--------------------------------------------------------------------------------
1 | import utils.Timer;
2 |
3 | public class TestTimer extends Timer {
4 | private long currentTime = System.currentTimeMillis();
5 |
6 | @Override
7 | public long getCurrentTimeInMillis() {
8 | return currentTime;
9 | }
10 |
11 | public void setTime(final long currentTime) {
12 | this.currentTime = currentTime;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/rate-limiter/target/classes/TimerWheel.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding-parrot/Low-Level-Design/b78735e2824db1e8b7a18699681a328f125edef6/rate-limiter/target/classes/TimerWheel.class
--------------------------------------------------------------------------------
/rate-limiter/target/classes/exceptions/RateLimitExceededException.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding-parrot/Low-Level-Design/b78735e2824db1e8b7a18699681a328f125edef6/rate-limiter/target/classes/exceptions/RateLimitExceededException.class
--------------------------------------------------------------------------------
/rate-limiter/target/classes/models/Request.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding-parrot/Low-Level-Design/b78735e2824db1e8b7a18699681a328f125edef6/rate-limiter/target/classes/models/Request.class
--------------------------------------------------------------------------------
/rate-limiter/target/classes/utils/Timer.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding-parrot/Low-Level-Design/b78735e2824db1e8b7a18699681a328f125edef6/rate-limiter/target/classes/utils/Timer.class
--------------------------------------------------------------------------------
/rate-limiter/target/test-classes/RateLimitTest.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding-parrot/Low-Level-Design/b78735e2824db1e8b7a18699681a328f125edef6/rate-limiter/target/test-classes/RateLimitTest.class
--------------------------------------------------------------------------------
/rate-limiter/target/test-classes/TestTimer.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding-parrot/Low-Level-Design/b78735e2824db1e8b7a18699681a328f125edef6/rate-limiter/target/test-classes/TestTimer.class
--------------------------------------------------------------------------------
/service-orchestrator/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/service-orchestrator/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/service-orchestrator/.idea/jarRepositories.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/service-orchestrator/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/service-orchestrator/.idea/uiDesigner.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | -
6 |
7 |
8 | -
9 |
10 |
11 | -
12 |
13 |
14 | -
15 |
16 |
17 | -
18 |
19 |
20 |
21 |
22 |
23 | -
24 |
25 |
26 |
27 |
28 |
29 | -
30 |
31 |
32 |
33 |
34 |
35 | -
36 |
37 |
38 |
39 |
40 |
41 | -
42 |
43 |
44 |
45 |
46 | -
47 |
48 |
49 |
50 |
51 | -
52 |
53 |
54 |
55 |
56 | -
57 |
58 |
59 |
60 |
61 | -
62 |
63 |
64 |
65 |
66 | -
67 |
68 |
69 |
70 |
71 | -
72 |
73 |
74 | -
75 |
76 |
77 |
78 |
79 | -
80 |
81 |
82 |
83 |
84 | -
85 |
86 |
87 |
88 |
89 | -
90 |
91 |
92 |
93 |
94 | -
95 |
96 |
97 |
98 |
99 | -
100 |
101 |
102 | -
103 |
104 |
105 | -
106 |
107 |
108 | -
109 |
110 |
111 | -
112 |
113 |
114 |
115 |
116 | -
117 |
118 |
119 | -
120 |
121 |
122 |
123 |
124 |
--------------------------------------------------------------------------------
/service-orchestrator/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/service-orchestrator/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | org.example
8 | service-orchestrator
9 | 1.0
10 |
11 |
12 |
13 | org.apache.maven.plugins
14 | maven-compiler-plugin
15 |
16 | 11
17 | 11
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | junit
26 | junit
27 | 4.13.1
28 | test
29 |
30 |
31 |
--------------------------------------------------------------------------------
/service-orchestrator/service-orchestrator.iml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/service-orchestrator/src/main/java/LoadBalancer.java:
--------------------------------------------------------------------------------
1 | import models.Node;
2 | import models.Request;
3 | import models.Service;
4 |
5 | import java.util.Map;
6 | import java.util.concurrent.ConcurrentHashMap;
7 |
8 | public class LoadBalancer {
9 | private final Map services;
10 | private final Map nodes;
11 |
12 | public LoadBalancer() {
13 | this.services = new ConcurrentHashMap<>();
14 | this.nodes = new ConcurrentHashMap<>();
15 | }
16 |
17 | public void register(Service service) {
18 | services.put(service.getId(), service);
19 | }
20 |
21 | public void addNode(String serviceId, Node node) {
22 | nodes.put(node.getId(), node);
23 | services.get(serviceId).getRouter().addNode(node);
24 | }
25 |
26 | public void removeNode(String serviceId, String nodeId) {
27 | services.get(serviceId).getRouter().removeNode(nodes.remove(nodeId));
28 | }
29 |
30 | public Node getHandler(Request request) {
31 | return services.get(request.getServiceId()).getRouter().getAssignedNode(request);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/service-orchestrator/src/main/java/algorithms/ConsistentHashing.java:
--------------------------------------------------------------------------------
1 | package algorithms;
2 |
3 | import models.Node;
4 | import models.Request;
5 |
6 | import java.util.List;
7 | import java.util.Map;
8 | import java.util.concurrent.ConcurrentHashMap;
9 | import java.util.concurrent.ConcurrentSkipListMap;
10 | import java.util.concurrent.CopyOnWriteArrayList;
11 | import java.util.function.Function;
12 |
13 | public class ConsistentHashing implements Router {
14 | private final Map> nodePositions;
15 | private final ConcurrentSkipListMap nodeMappings;
16 | private final Function hashFunction;
17 | private final int pointMultiplier;
18 |
19 |
20 | public ConsistentHashing(final Function hashFunction,
21 | final int pointMultiplier) {
22 | if (pointMultiplier == 0) {
23 | throw new IllegalArgumentException();
24 | }
25 | this.pointMultiplier = pointMultiplier;
26 | this.hashFunction = hashFunction;
27 | this.nodePositions = new ConcurrentHashMap<>();
28 | this.nodeMappings = new ConcurrentSkipListMap<>();
29 | }
30 |
31 | public void addNode(Node node) {
32 | nodePositions.put(node, new CopyOnWriteArrayList<>());
33 | for (int i = 0; i < pointMultiplier; i++) {
34 | for (int j = 0; j < node.getWeight(); j++) {
35 | final var point = hashFunction.apply((i * pointMultiplier + j) + node.getId());
36 | nodePositions.get(node).add(point);
37 | nodeMappings.put(point, node);
38 | }
39 | }
40 | }
41 |
42 | public void removeNode(Node node) {
43 | for (final Long point : nodePositions.remove(node)) {
44 | nodeMappings.remove(point);
45 | }
46 | }
47 |
48 | public Node getAssignedNode(Request request) {
49 | final var key = hashFunction.apply(request.getId());
50 | final var entry = nodeMappings.higherEntry(key);
51 | if (entry == null) {
52 | return nodeMappings.firstEntry().getValue();
53 | } else {
54 | return entry.getValue();
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/service-orchestrator/src/main/java/algorithms/Router.java:
--------------------------------------------------------------------------------
1 | package algorithms;
2 |
3 | import models.Node;
4 | import models.Request;
5 |
6 | public interface Router {
7 | void addNode(Node node);
8 |
9 | void removeNode(Node node);
10 |
11 | Node getAssignedNode(Request request);
12 | }
13 |
--------------------------------------------------------------------------------
/service-orchestrator/src/main/java/algorithms/WeightedRoundRobin.java:
--------------------------------------------------------------------------------
1 | package algorithms;
2 |
3 | import models.Node;
4 | import models.Request;
5 |
6 | import java.util.ArrayList;
7 | import java.util.List;
8 |
9 | public class WeightedRoundRobin implements Router {
10 | private final List nodes;
11 | private int assignTo;
12 | private int currentNodeAssignments;
13 | private final Object lock;
14 |
15 | public WeightedRoundRobin() {
16 | this.nodes = new ArrayList<>();
17 | this.assignTo = 0;
18 | this.lock = new Object();
19 | }
20 |
21 | public void addNode(Node node) {
22 | synchronized (this.lock) {
23 | nodes.add(node);
24 | }
25 | }
26 |
27 | public void removeNode(Node node) {
28 | synchronized (this.lock) {
29 | nodes.remove(node);
30 | assignTo--;
31 | currentNodeAssignments = 0;
32 | }
33 | }
34 |
35 | public Node getAssignedNode(Request request) {
36 | synchronized (this.lock) {
37 | assignTo = (assignTo + nodes.size()) % nodes.size();
38 | final var currentNode = nodes.get(assignTo);
39 | currentNodeAssignments++;
40 | if (currentNodeAssignments == currentNode.getWeight()) {
41 | assignTo++;
42 | currentNodeAssignments = 0;
43 | }
44 | return currentNode;
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/service-orchestrator/src/main/java/models/Node.java:
--------------------------------------------------------------------------------
1 | package models;
2 |
3 | import java.util.Objects;
4 |
5 | public class Node {
6 | private final String id;
7 | private final int weight;
8 | private final String ipAddress;
9 |
10 | public Node(String id, String ipAddress) {
11 | this(id, ipAddress, 1);
12 | }
13 |
14 | public Node(String id, String ipAddress, int weight) {
15 | this.id = id;
16 | this.weight = weight;
17 | this.ipAddress = ipAddress;
18 | }
19 |
20 | public String getId() {
21 | return id;
22 | }
23 |
24 | public int getWeight() {
25 | return weight;
26 | }
27 |
28 | public String getIpAddress() {
29 | return ipAddress;
30 | }
31 |
32 | @Override
33 | public boolean equals(Object o) {
34 | if (this == o) return true;
35 | if (o == null || getClass() != o.getClass()) return false;
36 | Node node = (Node) o;
37 | return id.equals(node.id);
38 | }
39 |
40 | @Override
41 | public int hashCode() {
42 | return Objects.hash(id);
43 | }
44 |
45 | }
46 |
--------------------------------------------------------------------------------
/service-orchestrator/src/main/java/models/Request.java:
--------------------------------------------------------------------------------
1 | package models;
2 |
3 | public class Request {
4 | private final String id;
5 |
6 | private final String serviceId;
7 | private final String method;
8 |
9 | public Request(String id, String serviceId, String method) {
10 | this.id = id;
11 | this.serviceId = serviceId;
12 | this.method = method;
13 | }
14 |
15 | public String getId() {
16 | return id;
17 | }
18 |
19 | public String getServiceId() {
20 | return serviceId;
21 | }
22 |
23 | public String getMethod() {
24 | return method;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/service-orchestrator/src/main/java/models/Service.java:
--------------------------------------------------------------------------------
1 | package models;
2 |
3 | import algorithms.Router;
4 |
5 | public class Service {
6 | private final Router router;
7 | private final String id;
8 | private final String[] methods;
9 |
10 | public Service(String id, Router router, String[] methods) {
11 | this.router = router;
12 | this.id = id;
13 | this.methods = methods;
14 | }
15 |
16 | public Router getRouter() {
17 | return router;
18 | }
19 |
20 | public String getId() {
21 | return id;
22 | }
23 |
24 | public String[] getMethods() {
25 | return methods;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/service-orchestrator/src/test/java/LBTester.java:
--------------------------------------------------------------------------------
1 | import algorithms.ConsistentHashing;
2 | import algorithms.WeightedRoundRobin;
3 | import models.Node;
4 | import models.Request;
5 | import models.Service;
6 | import org.junit.Assert;
7 | import org.junit.Test;
8 |
9 | public class LBTester {
10 | @Test
11 | public void LBDefaultBehaviour() {
12 | LoadBalancer loadBalancer = new LoadBalancer();
13 | final var consistentHashing = new ConsistentHashing(point -> (long) Math.abs(point.hashCode()) % 100, 1);
14 | final String profileServiceId = "profile", smsServiceId = "sms", emailServiceId = "email";
15 |
16 | loadBalancer.register(new Service(profileServiceId, consistentHashing, new String[]{"addProfile", "deleteProfile", "updateProfile"}));
17 | loadBalancer.register(new Service(smsServiceId, new WeightedRoundRobin(), new String[]{"sendSms", "addTemplate", "getSMSForUser"}));
18 | loadBalancer.register(new Service(emailServiceId, new WeightedRoundRobin(), new String[]{"sendEmail", "addTemplate", "getSMSForUser"}));
19 |
20 | final Node pNode1 = new Node("51", "35.45.55.65", 2), pNode2 = new Node("22", "35.45.55.66", 3);
21 | loadBalancer.addNode(profileServiceId, pNode1);
22 | loadBalancer.addNode(profileServiceId, pNode2);
23 |
24 | final Node sNode1 = new Node("13", "35.45.55.67"), sNode2 = new Node("64", "35.45.55.68");
25 | loadBalancer.addNode(smsServiceId, sNode1);
26 | loadBalancer.addNode(smsServiceId, sNode2);
27 |
28 | final Node eNode1 = new Node("node-35", "35.45.55.69", 2), eNode2 = new Node("node-76", "35.45.55.70");
29 | loadBalancer.addNode(emailServiceId, eNode1);
30 | loadBalancer.addNode(emailServiceId, eNode2);
31 |
32 | var profileNode1 = loadBalancer.getHandler(new Request("r-123", profileServiceId, "addProfile"));
33 | var profileNode2 = loadBalancer.getHandler(new Request("r-244", profileServiceId, "addProfile"));
34 | var profileNode3 = loadBalancer.getHandler(new Request("r-659", profileServiceId, "addProfile"));
35 | var profileNode4 = loadBalancer.getHandler(new Request("r-73", profileServiceId, "addProfile"));
36 | Assert.assertEquals(pNode1, profileNode1);
37 | Assert.assertEquals(pNode1, profileNode2);
38 | Assert.assertEquals(pNode2, profileNode3);
39 | Assert.assertEquals(pNode1, profileNode4);
40 |
41 | loadBalancer.removeNode(profileServiceId, pNode1.getId());
42 |
43 | profileNode1 = loadBalancer.getHandler(new Request("r-123", profileServiceId, "addProfile"));
44 | profileNode2 = loadBalancer.getHandler(new Request("r-244", profileServiceId, "addProfile"));
45 | profileNode3 = loadBalancer.getHandler(new Request("r-659", profileServiceId, "addProfile"));
46 | profileNode4 = loadBalancer.getHandler(new Request("r-73", profileServiceId, "addProfile"));
47 | Assert.assertEquals(pNode2, profileNode1);
48 | Assert.assertEquals(pNode2, profileNode2);
49 | Assert.assertEquals(pNode2, profileNode3);
50 | Assert.assertEquals(pNode2, profileNode4);
51 |
52 | final var smsNode1 = loadBalancer.getHandler(new Request("r-124", smsServiceId, "addTemplate"));
53 | final var smsNode2 = loadBalancer.getHandler(new Request("r-1214", smsServiceId, "addTemplate"));
54 | final var smsNode3 = loadBalancer.getHandler(new Request("r-4", smsServiceId, "addTemplate"));
55 |
56 | Assert.assertEquals(sNode1, smsNode1);
57 | Assert.assertEquals(sNode2, smsNode2);
58 | Assert.assertEquals(sNode1, smsNode3);
59 |
60 | final var emailNode1 = loadBalancer.getHandler(new Request("r-1232", emailServiceId, "addTemplate"));
61 | final var emailNode2 = loadBalancer.getHandler(new Request("r-4134", emailServiceId, "addTemplate"));
62 | final var emailNode3 = loadBalancer.getHandler(new Request("r-23432", emailServiceId, "addTemplate"));
63 | final var emailNode4 = loadBalancer.getHandler(new Request("r-5345", emailServiceId, "addTemplate"));
64 |
65 | Assert.assertEquals(eNode1, emailNode1);
66 | Assert.assertEquals(eNode1, emailNode2);
67 | Assert.assertEquals(eNode2, emailNode3);
68 | Assert.assertEquals(eNode1, emailNode4);
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/service-orchestrator/src/test/java/RouterTester.java:
--------------------------------------------------------------------------------
1 | import algorithms.ConsistentHashing;
2 | import algorithms.Router;
3 | import algorithms.WeightedRoundRobin;
4 | import models.Node;
5 | import models.Request;
6 | import org.junit.Assert;
7 | import org.junit.Test;
8 |
9 | import java.util.ArrayList;
10 | import java.util.List;
11 | import java.util.function.Function;
12 |
13 | public class RouterTester {
14 | String ipAddress = "127.0.0.1", serviceId = "service", method = "method";
15 |
16 | @Test
17 | public void defaultRoundRobin() {
18 | final Router router = new WeightedRoundRobin();
19 | final Node node1 = newNode("node-1"), node2 = newNode("node-2"), node3 = newNode("node-3");
20 | router.addNode(node1);
21 | router.addNode(node2);
22 | router.addNode(node3);
23 |
24 | Assert.assertEquals(node1, router.getAssignedNode(newRequest("r-123")));
25 | Assert.assertEquals(node2, router.getAssignedNode(newRequest("r-124")));
26 | Assert.assertEquals(node3, router.getAssignedNode(newRequest("r-125")));
27 |
28 | router.removeNode(node1);
29 |
30 | Assert.assertEquals(node2, router.getAssignedNode(newRequest("r-125")));
31 | Assert.assertEquals(node3, router.getAssignedNode(newRequest("r-126")));
32 | Assert.assertEquals(node2, router.getAssignedNode(newRequest("r-127")));
33 | Assert.assertEquals(node3, router.getAssignedNode(newRequest("r-128")));
34 |
35 | final Node node4 = new Node("node-4", ipAddress, 2);
36 | router.addNode(node4);
37 |
38 | Assert.assertEquals(node4, router.getAssignedNode(newRequest("r-129")));
39 | Assert.assertEquals(node4, router.getAssignedNode(newRequest("r-130")));
40 | Assert.assertEquals(node2, router.getAssignedNode(newRequest("r-131")));
41 | }
42 |
43 | @Test
44 | public void defaultConsistentHashing() {
45 | final List hashes = new ArrayList<>();
46 | hashes.add(1L);
47 | hashes.add(11L);
48 | hashes.add(21L);
49 | hashes.add(31L);
50 | final Function hashFunction = id -> {
51 | if (id.contains("000000")) {
52 | return hashes.remove(0);
53 | } else {
54 | return Long.parseLong(id);
55 | }
56 | };
57 | final Router router = new ConsistentHashing(hashFunction, 1);
58 | final Node node1 = newNode("1000000"), node2 = newNode("2000000"), node3 = newNode("3000000");
59 | router.addNode(node1);
60 | router.addNode(node2);
61 | router.addNode(node3);
62 |
63 | Assert.assertEquals(node1, router.getAssignedNode(newRequest("35")));
64 | Assert.assertEquals(node2, router.getAssignedNode(newRequest("5")));
65 | Assert.assertEquals(node3, router.getAssignedNode(newRequest("15")));
66 |
67 | router.removeNode(node1);
68 |
69 | Assert.assertEquals(node2, router.getAssignedNode(newRequest("22")));
70 | Assert.assertEquals(node3, router.getAssignedNode(newRequest("12")));
71 | Assert.assertEquals(node2, router.getAssignedNode(newRequest("23")));
72 | Assert.assertEquals(node3, router.getAssignedNode(newRequest("13")));
73 |
74 | final Node node4 = newNode("4000000");
75 | router.addNode(node4);
76 |
77 | Assert.assertEquals(node4, router.getAssignedNode(newRequest("25")));
78 | }
79 |
80 | @Test(expected = IllegalArgumentException.class)
81 | public void consistentHashingConstruction() {
82 | new ConsistentHashing(Long::valueOf, 0);
83 | }
84 |
85 | @Test
86 | public void consistentHashingWithWeights() {
87 | final List hashes = new ArrayList<>();
88 | hashes.add(1L); // remaining is node 1
89 | hashes.add(21L); // 12 to 21 is for node 1
90 | hashes.add(11L); // 2 to 11 is for node 2
91 | hashes.add(41L); // 32 to 41 is for node 2
92 | hashes.add(31L); // 22 to 31 is for node 3
93 | hashes.add(51L); // 42 to 51 is for node 3 --> 10 points
94 | final Function hashFunction = id -> {
95 | //range should be (0, 60)
96 | if (id.contains("000000")) {
97 | return hashes.remove(0);
98 | } else {
99 | return Long.parseLong(id);
100 | }
101 | };
102 | final Router router = new ConsistentHashing(hashFunction, 2);
103 | final Node node1 = newNode("1000000"), node2 = newNode("2000000"), node3 = newNode("3000000");
104 | router.addNode(node1);
105 | router.addNode(node2);
106 | router.addNode(node3);
107 |
108 | Assert.assertEquals(node1, router.getAssignedNode(newRequest("55")));
109 | Assert.assertEquals(node1, router.getAssignedNode(newRequest("15")));
110 | Assert.assertEquals(node2, router.getAssignedNode(newRequest("8")));
111 | Assert.assertEquals(node2, router.getAssignedNode(newRequest("33")));
112 | Assert.assertEquals(node3, router.getAssignedNode(newRequest("28")));
113 | Assert.assertEquals(node3, router.getAssignedNode(newRequest("47")));
114 |
115 | router.removeNode(node1);
116 | // remaining is node 2
117 | // 12 to 21 is now for node 3
118 | Assert.assertEquals(node2, router.getAssignedNode(newRequest("58")));
119 | Assert.assertEquals(node3, router.getAssignedNode(newRequest("12")));
120 | Assert.assertEquals(node3, router.getAssignedNode(newRequest("23")));
121 | Assert.assertEquals(node2, router.getAssignedNode(newRequest("54")));
122 |
123 | final Node node4 = newNode("4000000");
124 | hashes.add(6L); // 0 to 6 is for node 4, 52 to remaining is for node 4
125 | hashes.add(26L); // 12 to 26 is for node 4
126 | router.addNode(node4);
127 |
128 | Assert.assertEquals(node4, router.getAssignedNode(newRequest("15")));
129 | Assert.assertEquals(node4, router.getAssignedNode(newRequest("59")));
130 | Assert.assertEquals(node4, router.getAssignedNode(newRequest("5")));
131 | }
132 |
133 | private Request newRequest(String s) {
134 | return new Request(s, serviceId, method);
135 | }
136 |
137 | private Node newNode(String s) {
138 | return new Node(s, ipAddress);
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/service-orchestrator/target/classes/LoadBalancer.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding-parrot/Low-Level-Design/b78735e2824db1e8b7a18699681a328f125edef6/service-orchestrator/target/classes/LoadBalancer.class
--------------------------------------------------------------------------------
/service-orchestrator/target/classes/META-INF/service-orchestrator.kotlin_module:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/service-orchestrator/target/classes/algorithms/ConsistentHashing.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding-parrot/Low-Level-Design/b78735e2824db1e8b7a18699681a328f125edef6/service-orchestrator/target/classes/algorithms/ConsistentHashing.class
--------------------------------------------------------------------------------
/service-orchestrator/target/classes/algorithms/Router.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding-parrot/Low-Level-Design/b78735e2824db1e8b7a18699681a328f125edef6/service-orchestrator/target/classes/algorithms/Router.class
--------------------------------------------------------------------------------
/service-orchestrator/target/classes/algorithms/WeightedRoundRobin.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding-parrot/Low-Level-Design/b78735e2824db1e8b7a18699681a328f125edef6/service-orchestrator/target/classes/algorithms/WeightedRoundRobin.class
--------------------------------------------------------------------------------
/service-orchestrator/target/classes/models/Node.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding-parrot/Low-Level-Design/b78735e2824db1e8b7a18699681a328f125edef6/service-orchestrator/target/classes/models/Node.class
--------------------------------------------------------------------------------
/service-orchestrator/target/classes/models/Request.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding-parrot/Low-Level-Design/b78735e2824db1e8b7a18699681a328f125edef6/service-orchestrator/target/classes/models/Request.class
--------------------------------------------------------------------------------
/service-orchestrator/target/classes/models/Service.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding-parrot/Low-Level-Design/b78735e2824db1e8b7a18699681a328f125edef6/service-orchestrator/target/classes/models/Service.class
--------------------------------------------------------------------------------
/service-orchestrator/target/test-classes/LBTester.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding-parrot/Low-Level-Design/b78735e2824db1e8b7a18699681a328f125edef6/service-orchestrator/target/test-classes/LBTester.class
--------------------------------------------------------------------------------
/service-orchestrator/target/test-classes/META-INF/service-orchestrator.kotlin_module:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/service-orchestrator/target/test-classes/RouterTester.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coding-parrot/Low-Level-Design/b78735e2824db1e8b7a18699681a328f125edef6/service-orchestrator/target/test-classes/RouterTester.class
--------------------------------------------------------------------------------