├── 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 | 9 | 10 | 14 | 15 | 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 | 9 | 10 | 14 | 15 | 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 | 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 | 9 | 10 | 14 | 15 | 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 | 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 | 9 | 10 | 14 | 15 | 19 | 20 | -------------------------------------------------------------------------------- /service-orchestrator/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 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 --------------------------------------------------------------------------------