From 0d2fabc9629bd989e6d7bb8f921a25a859f4e0da Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 15 Jul 2020 18:02:38 +0530 Subject: [PATCH] Added LLD projects --- distributed-cache/.idea/.gitignore | 3 + distributed-cache/.idea/compiler.xml | 13 + distributed-cache/.idea/jarRepositories.xml | 20 + distributed-cache/.idea/misc.xml | 14 + distributed-cache/.idea/uiDesigner.xml | 124 ++++++ distributed-cache/.idea/vcs.xml | 6 + distributed-cache/DistributedCache.iml | 2 + distributed-cache/Requirements.ME | 9 + distributed-cache/pom.xml | 32 ++ distributed-cache/src/main/java/Cache.java | 179 +++++++++ .../src/main/java/CacheBuilder.java | 75 ++++ .../src/main/java/DataSource.java | 8 + .../src/main/java/events/Event.java | 37 ++ .../src/main/java/events/Eviction.java | 28 ++ .../src/main/java/events/Load.java | 10 + .../src/main/java/events/Update.java | 13 + .../src/main/java/events/Write.java | 10 + .../src/main/java/models/AccessDetails.java | 50 +++ .../main/java/models/EvictionAlgorithm.java | 5 + .../src/main/java/models/FetchAlgorithm.java | 5 + .../src/main/java/models/Record.java | 46 +++ .../src/main/java/models/Timer.java | 7 + .../src/test/java/TestCache.java | 366 ++++++++++++++++++ .../src/test/java/models/SettableTimer.java | 14 + distributed-cache/target/classes/Cache.class | Bin 0 -> 14690 bytes .../target/classes/CacheBuilder.class | Bin 0 -> 3207 bytes .../target/classes/DataSource.class | Bin 0 -> 511 bytes .../target/classes/events/Event.class | Bin 0 -> 1829 bytes .../target/classes/events/Eviction$Type.class | Bin 0 -> 1017 bytes .../target/classes/events/Eviction.class | Bin 0 -> 1521 bytes .../target/classes/events/Load.class | Bin 0 -> 561 bytes .../target/classes/events/Update.class | Bin 0 -> 700 bytes .../target/classes/events/Write.class | Bin 0 -> 565 bytes .../target/classes/models/AccessDetails.class | Bin 0 -> 2076 bytes .../classes/models/EvictionAlgorithm.class | Bin 0 -> 961 bytes .../classes/models/FetchAlgorithm.class | Bin 0 -> 957 bytes .../target/classes/models/Record.class | Bin 0 -> 2115 bytes .../target/classes/models/Timer.class | Bin 0 -> 380 bytes .../target/test-classes/TestCache$1.class | Bin 0 -> 2023 bytes .../target/test-classes/TestCache$2.class | Bin 0 -> 3175 bytes .../target/test-classes/TestCache.class | Bin 0 -> 18079 bytes .../test-classes/models/SettableTimer.class | Bin 0 -> 587 bytes distributed-event-bus/.idea/.gitignore | 3 + distributed-event-bus/.idea/compiler.xml | 16 + .../.idea/jarRepositories.xml | 20 + .../Maven__aopalliance_aopalliance_1_0.xml | 13 + ...Maven__com_google_code_gson_gson_2_8_6.xml | 13 + .../Maven__com_google_guava_guava_16_0_1.xml | 13 + .../Maven__com_google_inject_guice_4_0.xml | 13 + .../Maven__javax_inject_javax_inject_1.xml | 13 + .../libraries/Maven__junit_junit_4_13.xml | 13 + .../Maven__org_hamcrest_hamcrest_core_1_3.xml | 13 + distributed-event-bus/.idea/misc.xml | 13 + distributed-event-bus/.idea/modules.xml | 8 + distributed-event-bus/.idea/uiDesigner.xml | 124 ++++++ distributed-event-bus/.idea/vcs.xml | 6 + distributed-event-bus/README.ME | 8 + .../distributed-event-bus.iml | 22 ++ distributed-event-bus/pom.xml | 42 ++ .../src/main/java/EventBus.java | 203 ++++++++++ .../RetryLimitExceededException.java | 7 + .../exceptions/UnsubscribedPollException.java | 4 + .../src/main/java/lib/KeyedExecutor.java | 31 ++ .../src/main/java/models/Event.java | 43 ++ .../src/main/java/models/EventType.java | 5 + .../src/main/java/models/FailureEvent.java | 20 + .../src/main/java/models/Subscription.java | 57 +++ .../src/main/java/util/Timer.java | 10 + .../src/main/resources/application.properties | 0 .../src/test/java/EventBusTest.java | 340 ++++++++++++++++ .../src/test/java/TestTimer.java | 18 + .../target/classes/EventBus.class | Bin 0 -> 16455 bytes .../distributed-event-bus.kotlin_module | Bin 0 -> 16 bytes .../target/classes/application.properties | 0 .../RetryLimitExceededException.class | Bin 0 -> 406 bytes .../UnsubscribedPollException.class | Bin 0 -> 332 bytes .../target/classes/lib/KeyedExecutor.class | Bin 0 -> 3331 bytes .../target/classes/models/Event.class | Bin 0 -> 1119 bytes .../target/classes/models/EventType.class | Bin 0 -> 962 bytes .../target/classes/models/FailureEvent.class | Bin 0 -> 898 bytes .../target/classes/models/Subscription.class | Bin 0 -> 2246 bytes .../target/classes/util/Timer.class | Bin 0 -> 448 bytes .../target/test-classes/EventBusTest.class | Bin 0 -> 16010 bytes .../target/test-classes/TestTimer.class | Bin 0 -> 524 bytes rate-limiter/.idea/.gitignore | 3 + rate-limiter/.idea/compiler.xml | 16 + rate-limiter/.idea/jarRepositories.xml | 20 + .../libraries/Maven__junit_junit_4_13.xml | 13 + .../Maven__org_hamcrest_hamcrest_core_1_3.xml | 13 + rate-limiter/.idea/misc.xml | 13 + rate-limiter/.idea/modules.xml | 8 + rate-limiter/.idea/uiDesigner.xml | 124 ++++++ rate-limiter/.idea/vcs.xml | 6 + rate-limiter/pom.xml | 31 ++ rate-limiter/rate-limiter.iml | 17 + rate-limiter/src/main/java/TimerWheel.java | 77 ++++ .../RateLimitExceededException.java | 7 + .../src/main/java/models/Request.java | 33 ++ rate-limiter/src/main/java/utils/Timer.java | 13 + rate-limiter/src/test/java/RateLimitTest.java | 74 ++++ rate-limiter/src/test/java/TestTimer.java | 14 + rate-limiter/target/classes/TimerWheel.class | Bin 0 -> 4902 bytes .../RateLimitExceededException.class | Bin 0 -> 395 bytes .../target/classes/models/Request.class | Bin 0 -> 957 bytes rate-limiter/target/classes/utils/Timer.class | Bin 0 -> 700 bytes .../target/test-classes/RateLimitTest.class | Bin 0 -> 3005 bytes .../target/test-classes/TestTimer.class | Bin 0 -> 531 bytes service-orchestrator/.idea/.gitignore | 3 + service-orchestrator/.idea/compiler.xml | 13 + .../.idea/jarRepositories.xml | 20 + service-orchestrator/.idea/misc.xml | 14 + service-orchestrator/.idea/uiDesigner.xml | 124 ++++++ service-orchestrator/.idea/vcs.xml | 6 + service-orchestrator/pom.xml | 31 ++ service-orchestrator/service-orchestrator.iml | 2 + .../src/main/java/LoadBalancer.java | 33 ++ .../java/algorithms/ConsistentHashing.java | 57 +++ .../src/main/java/algorithms/Router.java | 12 + .../java/algorithms/WeightedRoundRobin.java | 47 +++ .../src/main/java/models/Node.java | 45 +++ .../src/main/java/models/Request.java | 26 ++ .../src/main/java/models/Service.java | 27 ++ .../src/test/java/LBTester.java | 70 ++++ .../src/test/java/RouterTester.java | 140 +++++++ .../target/classes/LoadBalancer.class | Bin 0 -> 1704 bytes .../service-orchestrator.kotlin_module | Bin 0 -> 16 bytes .../algorithms/ConsistentHashing.class | Bin 0 -> 3370 bytes .../target/classes/algorithms/Router.class | Bin 0 -> 222 bytes .../algorithms/WeightedRoundRobin.class | Bin 0 -> 1600 bytes .../target/classes/models/Node.class | Bin 0 -> 1290 bytes .../target/classes/models/Request.class | Bin 0 -> 723 bytes .../target/classes/models/Service.class | Bin 0 -> 813 bytes .../target/test-classes/LBTester.class | Bin 0 -> 3884 bytes .../service-orchestrator.kotlin_module | Bin 0 -> 16 bytes .../target/test-classes/RouterTester.class | Bin 0 -> 5329 bytes 135 files changed, 3319 insertions(+) create mode 100644 distributed-cache/.idea/.gitignore create mode 100644 distributed-cache/.idea/compiler.xml create mode 100644 distributed-cache/.idea/jarRepositories.xml create mode 100644 distributed-cache/.idea/misc.xml create mode 100644 distributed-cache/.idea/uiDesigner.xml create mode 100644 distributed-cache/.idea/vcs.xml create mode 100644 distributed-cache/DistributedCache.iml create mode 100644 distributed-cache/Requirements.ME create mode 100644 distributed-cache/pom.xml create mode 100644 distributed-cache/src/main/java/Cache.java create mode 100644 distributed-cache/src/main/java/CacheBuilder.java create mode 100644 distributed-cache/src/main/java/DataSource.java create mode 100644 distributed-cache/src/main/java/events/Event.java create mode 100644 distributed-cache/src/main/java/events/Eviction.java create mode 100644 distributed-cache/src/main/java/events/Load.java create mode 100644 distributed-cache/src/main/java/events/Update.java create mode 100644 distributed-cache/src/main/java/events/Write.java create mode 100644 distributed-cache/src/main/java/models/AccessDetails.java create mode 100644 distributed-cache/src/main/java/models/EvictionAlgorithm.java create mode 100644 distributed-cache/src/main/java/models/FetchAlgorithm.java create mode 100644 distributed-cache/src/main/java/models/Record.java create mode 100644 distributed-cache/src/main/java/models/Timer.java create mode 100644 distributed-cache/src/test/java/TestCache.java create mode 100644 distributed-cache/src/test/java/models/SettableTimer.java create mode 100644 distributed-cache/target/classes/Cache.class create mode 100644 distributed-cache/target/classes/CacheBuilder.class create mode 100644 distributed-cache/target/classes/DataSource.class create mode 100644 distributed-cache/target/classes/events/Event.class create mode 100644 distributed-cache/target/classes/events/Eviction$Type.class create mode 100644 distributed-cache/target/classes/events/Eviction.class create mode 100644 distributed-cache/target/classes/events/Load.class create mode 100644 distributed-cache/target/classes/events/Update.class create mode 100644 distributed-cache/target/classes/events/Write.class create mode 100644 distributed-cache/target/classes/models/AccessDetails.class create mode 100644 distributed-cache/target/classes/models/EvictionAlgorithm.class create mode 100644 distributed-cache/target/classes/models/FetchAlgorithm.class create mode 100644 distributed-cache/target/classes/models/Record.class create mode 100644 distributed-cache/target/classes/models/Timer.class create mode 100644 distributed-cache/target/test-classes/TestCache$1.class create mode 100644 distributed-cache/target/test-classes/TestCache$2.class create mode 100644 distributed-cache/target/test-classes/TestCache.class create mode 100644 distributed-cache/target/test-classes/models/SettableTimer.class create mode 100644 distributed-event-bus/.idea/.gitignore create mode 100644 distributed-event-bus/.idea/compiler.xml create mode 100644 distributed-event-bus/.idea/jarRepositories.xml create mode 100644 distributed-event-bus/.idea/libraries/Maven__aopalliance_aopalliance_1_0.xml create mode 100644 distributed-event-bus/.idea/libraries/Maven__com_google_code_gson_gson_2_8_6.xml create mode 100644 distributed-event-bus/.idea/libraries/Maven__com_google_guava_guava_16_0_1.xml create mode 100644 distributed-event-bus/.idea/libraries/Maven__com_google_inject_guice_4_0.xml create mode 100644 distributed-event-bus/.idea/libraries/Maven__javax_inject_javax_inject_1.xml create mode 100644 distributed-event-bus/.idea/libraries/Maven__junit_junit_4_13.xml create mode 100644 distributed-event-bus/.idea/libraries/Maven__org_hamcrest_hamcrest_core_1_3.xml create mode 100644 distributed-event-bus/.idea/misc.xml create mode 100644 distributed-event-bus/.idea/modules.xml create mode 100644 distributed-event-bus/.idea/uiDesigner.xml create mode 100644 distributed-event-bus/.idea/vcs.xml create mode 100644 distributed-event-bus/README.ME create mode 100644 distributed-event-bus/distributed-event-bus.iml create mode 100644 distributed-event-bus/pom.xml create mode 100644 distributed-event-bus/src/main/java/EventBus.java create mode 100644 distributed-event-bus/src/main/java/exceptions/RetryLimitExceededException.java create mode 100644 distributed-event-bus/src/main/java/exceptions/UnsubscribedPollException.java create mode 100644 distributed-event-bus/src/main/java/lib/KeyedExecutor.java create mode 100644 distributed-event-bus/src/main/java/models/Event.java create mode 100644 distributed-event-bus/src/main/java/models/EventType.java create mode 100644 distributed-event-bus/src/main/java/models/FailureEvent.java create mode 100644 distributed-event-bus/src/main/java/models/Subscription.java create mode 100644 distributed-event-bus/src/main/java/util/Timer.java create mode 100644 distributed-event-bus/src/main/resources/application.properties create mode 100644 distributed-event-bus/src/test/java/EventBusTest.java create mode 100644 distributed-event-bus/src/test/java/TestTimer.java create mode 100644 distributed-event-bus/target/classes/EventBus.class create mode 100644 distributed-event-bus/target/classes/META-INF/distributed-event-bus.kotlin_module create mode 100644 distributed-event-bus/target/classes/application.properties create mode 100644 distributed-event-bus/target/classes/exceptions/RetryLimitExceededException.class create mode 100644 distributed-event-bus/target/classes/exceptions/UnsubscribedPollException.class create mode 100644 distributed-event-bus/target/classes/lib/KeyedExecutor.class create mode 100644 distributed-event-bus/target/classes/models/Event.class create mode 100644 distributed-event-bus/target/classes/models/EventType.class create mode 100644 distributed-event-bus/target/classes/models/FailureEvent.class create mode 100644 distributed-event-bus/target/classes/models/Subscription.class create mode 100644 distributed-event-bus/target/classes/util/Timer.class create mode 100644 distributed-event-bus/target/test-classes/EventBusTest.class create mode 100644 distributed-event-bus/target/test-classes/TestTimer.class create mode 100644 rate-limiter/.idea/.gitignore create mode 100644 rate-limiter/.idea/compiler.xml create mode 100644 rate-limiter/.idea/jarRepositories.xml create mode 100644 rate-limiter/.idea/libraries/Maven__junit_junit_4_13.xml create mode 100644 rate-limiter/.idea/libraries/Maven__org_hamcrest_hamcrest_core_1_3.xml create mode 100644 rate-limiter/.idea/misc.xml create mode 100644 rate-limiter/.idea/modules.xml create mode 100644 rate-limiter/.idea/uiDesigner.xml create mode 100644 rate-limiter/.idea/vcs.xml create mode 100644 rate-limiter/pom.xml create mode 100644 rate-limiter/rate-limiter.iml create mode 100644 rate-limiter/src/main/java/TimerWheel.java create mode 100644 rate-limiter/src/main/java/exceptions/RateLimitExceededException.java create mode 100644 rate-limiter/src/main/java/models/Request.java create mode 100644 rate-limiter/src/main/java/utils/Timer.java create mode 100644 rate-limiter/src/test/java/RateLimitTest.java create mode 100644 rate-limiter/src/test/java/TestTimer.java create mode 100644 rate-limiter/target/classes/TimerWheel.class create mode 100644 rate-limiter/target/classes/exceptions/RateLimitExceededException.class create mode 100644 rate-limiter/target/classes/models/Request.class create mode 100644 rate-limiter/target/classes/utils/Timer.class create mode 100644 rate-limiter/target/test-classes/RateLimitTest.class create mode 100644 rate-limiter/target/test-classes/TestTimer.class create mode 100644 service-orchestrator/.idea/.gitignore create mode 100644 service-orchestrator/.idea/compiler.xml create mode 100644 service-orchestrator/.idea/jarRepositories.xml create mode 100644 service-orchestrator/.idea/misc.xml create mode 100644 service-orchestrator/.idea/uiDesigner.xml create mode 100644 service-orchestrator/.idea/vcs.xml create mode 100644 service-orchestrator/pom.xml create mode 100644 service-orchestrator/service-orchestrator.iml create mode 100644 service-orchestrator/src/main/java/LoadBalancer.java create mode 100644 service-orchestrator/src/main/java/algorithms/ConsistentHashing.java create mode 100644 service-orchestrator/src/main/java/algorithms/Router.java create mode 100644 service-orchestrator/src/main/java/algorithms/WeightedRoundRobin.java create mode 100644 service-orchestrator/src/main/java/models/Node.java create mode 100644 service-orchestrator/src/main/java/models/Request.java create mode 100644 service-orchestrator/src/main/java/models/Service.java create mode 100644 service-orchestrator/src/test/java/LBTester.java create mode 100644 service-orchestrator/src/test/java/RouterTester.java create mode 100644 service-orchestrator/target/classes/LoadBalancer.class create mode 100644 service-orchestrator/target/classes/META-INF/service-orchestrator.kotlin_module create mode 100644 service-orchestrator/target/classes/algorithms/ConsistentHashing.class create mode 100644 service-orchestrator/target/classes/algorithms/Router.class create mode 100644 service-orchestrator/target/classes/algorithms/WeightedRoundRobin.class create mode 100644 service-orchestrator/target/classes/models/Node.class create mode 100644 service-orchestrator/target/classes/models/Request.class create mode 100644 service-orchestrator/target/classes/models/Service.class create mode 100644 service-orchestrator/target/test-classes/LBTester.class create mode 100644 service-orchestrator/target/test-classes/META-INF/service-orchestrator.kotlin_module create mode 100644 service-orchestrator/target/test-classes/RouterTester.class diff --git a/distributed-cache/.idea/.gitignore b/distributed-cache/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/distributed-cache/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/distributed-cache/.idea/compiler.xml b/distributed-cache/.idea/compiler.xml new file mode 100644 index 0000000..bddbcc6 --- /dev/null +++ b/distributed-cache/.idea/compiler.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/distributed-cache/.idea/jarRepositories.xml b/distributed-cache/.idea/jarRepositories.xml new file mode 100644 index 0000000..712ab9d --- /dev/null +++ b/distributed-cache/.idea/jarRepositories.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/distributed-cache/.idea/misc.xml b/distributed-cache/.idea/misc.xml new file mode 100644 index 0000000..d24ea8e --- /dev/null +++ b/distributed-cache/.idea/misc.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/distributed-cache/.idea/uiDesigner.xml b/distributed-cache/.idea/uiDesigner.xml new file mode 100644 index 0000000..e96534f --- /dev/null +++ b/distributed-cache/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/distributed-cache/.idea/vcs.xml b/distributed-cache/.idea/vcs.xml new file mode 100644 index 0000000..6c0b863 --- /dev/null +++ b/distributed-cache/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/distributed-cache/DistributedCache.iml b/distributed-cache/DistributedCache.iml new file mode 100644 index 0000000..78b2cc5 --- /dev/null +++ b/distributed-cache/DistributedCache.iml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/distributed-cache/Requirements.ME b/distributed-cache/Requirements.ME new file mode 100644 index 0000000..b64fdad --- /dev/null +++ b/distributed-cache/Requirements.ME @@ -0,0 +1,9 @@ +Should allow: + 1) listeners on load and evict + 2) hot loading elements on startup + 3) multiple eviction algorithms like LRU and LFU + 4) expiration time + 5) multiple fetch algorithms like write back and write through + 6) return futures + 7) request collapsing + 8) Avoid thrashing with rate limiting \ No newline at end of file diff --git a/distributed-cache/pom.xml b/distributed-cache/pom.xml new file mode 100644 index 0000000..86533e0 --- /dev/null +++ b/distributed-cache/pom.xml @@ -0,0 +1,32 @@ + + + 4.0.0 + + interviewready.io + distributed-cache + 1.0 + + + + org.apache.maven.plugins + maven-compiler-plugin + + 11 + 11 + + + + + + + + junit + junit + 4.13 + test + + + + \ No newline at end of file diff --git a/distributed-cache/src/main/java/Cache.java b/distributed-cache/src/main/java/Cache.java new file mode 100644 index 0000000..167aa5f --- /dev/null +++ b/distributed-cache/src/main/java/Cache.java @@ -0,0 +1,179 @@ +import events.*; +import models.*; + +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.*; +import java.util.function.Function; + +public class Cache { + private final int maximumSize; + private final FetchAlgorithm fetchAlgorithm; + private final Duration expiryTime; + private final Map>> cache; + private final ConcurrentSkipListMap> priorityQueue; + private final ConcurrentSkipListMap> expiryQueue; + private final DataSource dataSource; + private final List> eventQueue; + private final ExecutorService[] executorPool; + private final Timer timer; + + protected Cache(final int maximumSize, + final Duration expiryTime, + final FetchAlgorithm fetchAlgorithm, + final EvictionAlgorithm evictionAlgorithm, + final DataSource dataSource, + final Set keysToEagerlyLoad, + final Timer timer, + final int poolSize) { + this.expiryTime = expiryTime; + this.maximumSize = maximumSize; + this.fetchAlgorithm = fetchAlgorithm; + this.timer = timer; + this.cache = new ConcurrentHashMap<>(); + this.eventQueue = new CopyOnWriteArrayList<>(); + this.dataSource = dataSource; + this.executorPool = new ExecutorService[poolSize]; + for (int i = 0; i < poolSize; i++) { + executorPool[i] = Executors.newSingleThreadExecutor(); + } + priorityQueue = new ConcurrentSkipListMap<>((first, second) -> { + final var accessTimeDifference = (int) (first.getLastAccessTime() - second.getLastAccessTime()); + if (evictionAlgorithm.equals(EvictionAlgorithm.LRU)) { + return accessTimeDifference; + } else { + final var accessCountDifference = first.getAccessCount() - second.getAccessCount(); + return accessCountDifference != 0 ? accessCountDifference : accessTimeDifference; + } + }); + expiryQueue = new ConcurrentSkipListMap<>(); + final var eagerLoading = keysToEagerlyLoad.stream() + .map(key -> getThreadFor(key, addToCache(key, loadFromDB(dataSource, key)))) + .toArray(CompletableFuture[]::new); + CompletableFuture.allOf(eagerLoading).join(); + } + + private CompletionStage getThreadFor(KEY key, CompletionStage task) { + return CompletableFuture.supplyAsync(() -> task, executorPool[Math.abs(key.hashCode() % executorPool.length)]).thenCompose(Function.identity()); + } + + public CompletionStage get(KEY key) { + return getThreadFor(key, getFromCache(key)); + } + + public CompletionStage set(KEY key, VALUE value) { + return getThreadFor(key, setInCache(key, value)); + } + + private CompletionStage getFromCache(KEY key) { + final CompletionStage> result; + if (!cache.containsKey(key)) { + result = addToCache(key, loadFromDB(dataSource, key)); + } else { + result = cache.get(key).thenCompose(record -> { + if (hasExpired(record)) { + priorityQueue.get(record.getAccessDetails()).remove(key); + expiryQueue.get(record.getInsertionTime()).remove(key); + eventQueue.add(new Eviction<>(record, Eviction.Type.EXPIRY, timer.getCurrentTime())); + return addToCache(key, loadFromDB(dataSource, key)); + } else { + return CompletableFuture.completedFuture(record); + } + }); + } + return result.thenApply(record -> { + priorityQueue.get(record.getAccessDetails()).remove(key); + final AccessDetails updatedAccessDetails = record.getAccessDetails().update(timer.getCurrentTime()); + priorityQueue.putIfAbsent(updatedAccessDetails, new CopyOnWriteArrayList<>()); + priorityQueue.get(updatedAccessDetails).add(key); + record.setAccessDetails(updatedAccessDetails); + return record.getValue(); + }); + } + + public CompletionStage setInCache(KEY key, VALUE value) { + CompletionStage result = CompletableFuture.completedFuture(null); + if (cache.containsKey(key)) { + result = cache.remove(key) + .thenAccept(oldRecord -> { + priorityQueue.get(oldRecord.getAccessDetails()).remove(key); + expiryQueue.get(oldRecord.getInsertionTime()).remove(key); + if (hasExpired(oldRecord)) { + eventQueue.add(new Eviction<>(oldRecord, Eviction.Type.EXPIRY, timer.getCurrentTime())); + } else { + eventQueue.add(new Update<>(new Record<>(key, value, timer.getCurrentTime()), oldRecord, timer.getCurrentTime())); + } + }); + } + return result.thenCompose(__ -> addToCache(key, CompletableFuture.completedFuture(value))).thenCompose(record -> { + final CompletionStage writeOperation = persistRecord(record); + return fetchAlgorithm == FetchAlgorithm.WRITE_THROUGH ? writeOperation : CompletableFuture.completedFuture(null); + }); + } + + private CompletionStage> addToCache(final KEY key, final CompletionStage valueFuture) { + manageEntries(); + final var recordFuture = valueFuture.thenApply(value -> { + final Record record = new Record<>(key, value, timer.getCurrentTime()); + expiryQueue.putIfAbsent(record.getInsertionTime(), new CopyOnWriteArrayList<>()); + expiryQueue.get(record.getInsertionTime()).add(key); + priorityQueue.putIfAbsent(record.getAccessDetails(), new CopyOnWriteArrayList<>()); + priorityQueue.get(record.getAccessDetails()).add(key); + return record; + }); + cache.put(key, recordFuture); + return recordFuture; + } + + private synchronized void manageEntries() { + if (cache.size() >= maximumSize) { + while (!expiryQueue.isEmpty() && hasExpired(expiryQueue.firstKey())) { + final List keys = expiryQueue.pollFirstEntry().getValue(); + for (final KEY key : keys) { + final Record expiredRecord = cache.remove(key).toCompletableFuture().join(); + priorityQueue.remove(expiredRecord.getAccessDetails()); + eventQueue.add(new Eviction<>(expiredRecord, Eviction.Type.EXPIRY, timer.getCurrentTime())); + } + } + } + if (cache.size() >= maximumSize) { + List keys = priorityQueue.pollFirstEntry().getValue(); + while (keys.isEmpty()) { + keys = priorityQueue.pollFirstEntry().getValue(); + } + for (final KEY key : keys) { + final Record lowestPriorityRecord = cache.remove(key).toCompletableFuture().join(); + expiryQueue.get(lowestPriorityRecord.getInsertionTime()).remove(lowestPriorityRecord.getKey()); + eventQueue.add(new Eviction<>(lowestPriorityRecord, Eviction.Type.REPLACEMENT, timer.getCurrentTime())); + } + } + } + + private CompletionStage persistRecord(final Record record) { + return dataSource.persist(record.getKey(), record.getValue(), record.getInsertionTime()) + .thenAccept(__ -> eventQueue.add(new Write<>(record, timer.getCurrentTime()))); + } + + private boolean hasExpired(final Record record) { + return hasExpired(record.getInsertionTime()); + } + + private boolean hasExpired(final Long time) { + return Duration.ofNanos(timer.getCurrentTime() - time).compareTo(expiryTime) > 0; + } + + public List> getEventQueue() { + return eventQueue; + } + + private CompletionStage loadFromDB(final DataSource dataSource, KEY key) { + return dataSource.load(key).whenComplete((value, throwable) -> { + if (throwable == null) { + eventQueue.add(new Load<>(new Record<>(key, value, timer.getCurrentTime()), timer.getCurrentTime())); + } + }); + } +} + diff --git a/distributed-cache/src/main/java/CacheBuilder.java b/distributed-cache/src/main/java/CacheBuilder.java new file mode 100644 index 0000000..4dbee63 --- /dev/null +++ b/distributed-cache/src/main/java/CacheBuilder.java @@ -0,0 +1,75 @@ +import models.EvictionAlgorithm; +import models.FetchAlgorithm; +import models.Timer; + +import java.time.Duration; +import java.util.HashSet; +import java.util.Set; + +public class CacheBuilder { + private int maximumSize; + private Duration expiryTime; + private final Set onStartLoad; + private EvictionAlgorithm evictionAlgorithm; + private FetchAlgorithm fetchAlgorithm; + private DataSource dataSource; + private Timer timer; + private int poolSize; + + public CacheBuilder() { + maximumSize = 1000; + expiryTime = Duration.ofDays(365); + fetchAlgorithm = FetchAlgorithm.WRITE_THROUGH; + evictionAlgorithm = EvictionAlgorithm.LRU; + onStartLoad = new HashSet<>(); + poolSize = 1; + timer = new Timer(); + } + + public CacheBuilder maximumSize(final int maximumSize) { + this.maximumSize = maximumSize; + return this; + } + + public CacheBuilder expiryTime(final Duration expiryTime) { + this.expiryTime = expiryTime; + return this; + } + + public CacheBuilder loadKeysOnStart(final Set keys) { + this.onStartLoad.addAll(keys); + return this; + } + + public CacheBuilder evictionAlgorithm(final EvictionAlgorithm evictionAlgorithm) { + this.evictionAlgorithm = evictionAlgorithm; + return this; + } + + public CacheBuilder fetchAlgorithm(final FetchAlgorithm fetchAlgorithm) { + this.fetchAlgorithm = fetchAlgorithm; + return this; + } + + public CacheBuilder dataSource(final DataSource dataSource) { + this.dataSource = dataSource; + return this; + } + + public CacheBuilder timer(final Timer timer) { + this.timer = timer; + return this; + } + + public CacheBuilder poolSize(final int poolSize) { + this.poolSize = poolSize; + return this; + } + + public Cache build() { + if (dataSource == null) { + throw new IllegalArgumentException("No datasource configured"); + } + return new Cache<>(maximumSize, expiryTime, fetchAlgorithm, evictionAlgorithm, dataSource, onStartLoad, timer, poolSize); + } +} diff --git a/distributed-cache/src/main/java/DataSource.java b/distributed-cache/src/main/java/DataSource.java new file mode 100644 index 0000000..7a891ab --- /dev/null +++ b/distributed-cache/src/main/java/DataSource.java @@ -0,0 +1,8 @@ +import java.util.concurrent.CompletionStage; + +public interface DataSource { + + CompletionStage load(KEY key); + + CompletionStage persist(KEY key, VALUE value, long timestamp); +} diff --git a/distributed-cache/src/main/java/events/Event.java b/distributed-cache/src/main/java/events/Event.java new file mode 100644 index 0000000..44bdbf9 --- /dev/null +++ b/distributed-cache/src/main/java/events/Event.java @@ -0,0 +1,37 @@ +package events; + +import models.Record; + +import java.util.UUID; + +public abstract class Event { + private final String id; + private final Record element; + private final long timestamp; + + public Event(Record element, long timestamp) { + this.element = element; + this.timestamp = timestamp; + id = UUID.randomUUID().toString(); + } + + public String getId() { + return id; + } + + public Record getElement() { + return element; + } + + public long getTimestamp() { + return timestamp; + } + + @Override + public String toString() { + return this.getClass().getName() + "{" + + "element=" + element + + ", timestamp=" + timestamp + + "}\n"; + } +} diff --git a/distributed-cache/src/main/java/events/Eviction.java b/distributed-cache/src/main/java/events/Eviction.java new file mode 100644 index 0000000..f5b8b3b --- /dev/null +++ b/distributed-cache/src/main/java/events/Eviction.java @@ -0,0 +1,28 @@ +package events; + +import models.Record; + +public class Eviction extends Event { + private final Type type; + + public Eviction(Record element, Type type, long timestamp) { + super(element, timestamp); + this.type = type; + } + + public Type getType() { + return type; + } + + public enum Type { + EXPIRY, REPLACEMENT + } + + @Override + public String toString() { + return "Eviction{" + + "type=" + type + + ", "+super.toString() + + "}\n"; + } +} diff --git a/distributed-cache/src/main/java/events/Load.java b/distributed-cache/src/main/java/events/Load.java new file mode 100644 index 0000000..b2736e8 --- /dev/null +++ b/distributed-cache/src/main/java/events/Load.java @@ -0,0 +1,10 @@ +package events; + +import models.Record; + +public class Load extends Event { + + public Load(Record element, long timestamp) { + super(element, timestamp); + } +} diff --git a/distributed-cache/src/main/java/events/Update.java b/distributed-cache/src/main/java/events/Update.java new file mode 100644 index 0000000..90a922b --- /dev/null +++ b/distributed-cache/src/main/java/events/Update.java @@ -0,0 +1,13 @@ +package events; + +import models.Record; + +public class Update extends Event { + + private final Record previousValue; + + public Update(Record element, Record previousValue, long timestamp) { + super(element, timestamp); + this.previousValue = previousValue; + } +} diff --git a/distributed-cache/src/main/java/events/Write.java b/distributed-cache/src/main/java/events/Write.java new file mode 100644 index 0000000..34888ba --- /dev/null +++ b/distributed-cache/src/main/java/events/Write.java @@ -0,0 +1,10 @@ +package events; + +import models.Record; + +public class Write extends Event { + + public Write(Record element, long timestamp) { + super(element, timestamp); + } +} diff --git a/distributed-cache/src/main/java/models/AccessDetails.java b/distributed-cache/src/main/java/models/AccessDetails.java new file mode 100644 index 0000000..ef6e041 --- /dev/null +++ b/distributed-cache/src/main/java/models/AccessDetails.java @@ -0,0 +1,50 @@ +package models; + +import java.util.Objects; +import java.util.concurrent.atomic.LongAdder; + +public class AccessDetails { + private final LongAdder accessCount; + private long lastAccessTime; + + public AccessDetails(long lastAccessTime) { + accessCount = new LongAdder(); + this.lastAccessTime = lastAccessTime; + } + + public long getLastAccessTime() { + return lastAccessTime; + } + + public int getAccessCount() { + return (int) accessCount.longValue(); + } + + public AccessDetails update(long lastAccessTime) { + final AccessDetails accessDetails = new AccessDetails(lastAccessTime); + accessDetails.accessCount.add(this.accessCount.longValue() + 1); + return accessDetails; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + AccessDetails that = (AccessDetails) o; + return lastAccessTime == that.lastAccessTime && + this.getAccessCount() == that.getAccessCount(); + } + + @Override + public int hashCode() { + return Objects.hash(getAccessCount(), lastAccessTime); + } + + @Override + public String toString() { + return "AccessDetails{" + + "accessCount=" + accessCount + + ", lastAccessTime=" + lastAccessTime + + '}'; + } +} diff --git a/distributed-cache/src/main/java/models/EvictionAlgorithm.java b/distributed-cache/src/main/java/models/EvictionAlgorithm.java new file mode 100644 index 0000000..43ceba2 --- /dev/null +++ b/distributed-cache/src/main/java/models/EvictionAlgorithm.java @@ -0,0 +1,5 @@ +package models; + +public enum EvictionAlgorithm { + LRU, LFU +} diff --git a/distributed-cache/src/main/java/models/FetchAlgorithm.java b/distributed-cache/src/main/java/models/FetchAlgorithm.java new file mode 100644 index 0000000..4911e69 --- /dev/null +++ b/distributed-cache/src/main/java/models/FetchAlgorithm.java @@ -0,0 +1,5 @@ +package models; + +public enum FetchAlgorithm { + WRITE_THROUGH, WRITE_BACK +} diff --git a/distributed-cache/src/main/java/models/Record.java b/distributed-cache/src/main/java/models/Record.java new file mode 100644 index 0000000..51ff658 --- /dev/null +++ b/distributed-cache/src/main/java/models/Record.java @@ -0,0 +1,46 @@ +package models; + +public class Record { + private final KEY key; + private final VALUE value; + private final long insertionTime; + private AccessDetails accessDetails; + + public Record(KEY key, VALUE value, long insertionTime) { + this.key = key; + this.value = value; + this.insertionTime = insertionTime; + this.accessDetails = new AccessDetails(insertionTime); + } + + public KEY getKey() { + return key; + } + + public VALUE getValue() { + return value; + } + + public long getInsertionTime() { + return insertionTime; + } + + public AccessDetails getAccessDetails() { + return accessDetails; + } + + public void setAccessDetails(final AccessDetails accessDetails) { + this.accessDetails = accessDetails; + } + + @Override + public String toString() { + return "Record{" + + "key=" + key + + ", value=" + value + + ", insertionTime=" + insertionTime + + ", accessDetails=" + accessDetails + + '}'; + } +} + diff --git a/distributed-cache/src/main/java/models/Timer.java b/distributed-cache/src/main/java/models/Timer.java new file mode 100644 index 0000000..ae0746c --- /dev/null +++ b/distributed-cache/src/main/java/models/Timer.java @@ -0,0 +1,7 @@ +package models; + +public class Timer { + public long getCurrentTime() { + return System.nanoTime(); + } +} diff --git a/distributed-cache/src/test/java/TestCache.java b/distributed-cache/src/test/java/TestCache.java new file mode 100644 index 0000000..5340ba9 --- /dev/null +++ b/distributed-cache/src/test/java/TestCache.java @@ -0,0 +1,366 @@ +import events.Eviction; +import events.Load; +import events.Update; +import events.Write; +import models.EvictionAlgorithm; +import models.FetchAlgorithm; +import models.SettableTimer; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.time.Duration; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; + +public class TestCache { + + private static final String PROFILE_MUMBAI_ENGINEER = "profile_mumbai_engineer", PROFILE_HYDERABAD_ENGINEER = "profile_hyderabad_engineer"; + private final Map dataMap = new ConcurrentHashMap<>(); + private DataSource dataSource; + private final Queue> writeOperations = new LinkedList<>(); + private DataSource writeBackDataSource; + + @Before + public void setUp() { + dataMap.clear(); + writeOperations.clear(); + dataMap.put(PROFILE_MUMBAI_ENGINEER, "violet"); + dataMap.put(PROFILE_HYDERABAD_ENGINEER, "blue"); + dataSource = new DataSource<>() { + @Override + public CompletionStage load(String key) { + if (dataMap.containsKey(key)) { + return CompletableFuture.completedFuture(dataMap.get(key)); + } else { + return CompletableFuture.failedStage(new NullPointerException()); + } + } + + @Override + public CompletionStage persist(String key, String value, long timestamp) { + dataMap.put(key, value); + return CompletableFuture.completedFuture(null); + } + }; + + writeBackDataSource = new DataSource<>() { + @Override + public CompletionStage load(String key) { + if (dataMap.containsKey(key)) { + return CompletableFuture.completedFuture(dataMap.get(key)); + } else { + return CompletableFuture.failedStage(new NullPointerException()); + } + } + + @Override + public CompletionStage persist(String key, String value, long timestamp) { + final CompletableFuture hold = new CompletableFuture<>(); + writeOperations.add(hold); + return hold.thenAccept(__ -> dataMap.put(key, value)); + } + }; + } + + private void acceptWrite() { + final CompletableFuture write = writeOperations.poll(); + if (write != null) { + write.complete(null); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testCacheConstructionWithoutDataSourceFailure() { + new CacheBuilder<>().build(); + } + + @Test + public void testCacheDefaultBehavior() throws ExecutionException, InterruptedException { + final var cache = new CacheBuilder().dataSource(dataSource).build(); + Assert.assertNotNull(cache); + assert isEqualTo(cache.get(PROFILE_MUMBAI_ENGINEER), "violet"); + assert cache.get("random") + .exceptionally(throwable -> Boolean.TRUE.toString()) + .thenApply(Boolean::valueOf) + .toCompletableFuture() + .get(); + assert isEqualTo(cache.set(PROFILE_MUMBAI_ENGINEER, "brown").thenCompose(__ -> cache.get(PROFILE_MUMBAI_ENGINEER)), "brown"); + Assert.assertEquals(3, cache.getEventQueue().size()); + assert cache.getEventQueue().get(0) instanceof Load; + assert cache.getEventQueue().get(1) instanceof Update; + assert cache.getEventQueue().get(2) instanceof Write; + } + + @Test + public void Eviction_LRU() { + final var maximumSize = 2; + final var cache = new CacheBuilder() + .maximumSize(maximumSize) + .evictionAlgorithm(EvictionAlgorithm.LRU) + .fetchAlgorithm(FetchAlgorithm.WRITE_BACK) + .dataSource(writeBackDataSource).build(); + cache.get(PROFILE_MUMBAI_ENGINEER).toCompletableFuture().join(); + for (int i = 0; i < maximumSize; i++) { + cache.set("key" + i, "value" + i).toCompletableFuture().join(); + } + Assert.assertEquals(2, cache.getEventQueue().size()); + assert cache.getEventQueue().get(0) instanceof Load; + assert cache.getEventQueue().get(1) instanceof Eviction; + final var evictionEvent = (Eviction) cache.getEventQueue().get(1); + Assert.assertEquals(Eviction.Type.REPLACEMENT, evictionEvent.getType()); + Assert.assertEquals(PROFILE_MUMBAI_ENGINEER, evictionEvent.getElement().getKey()); + cache.getEventQueue().clear(); + final var permutation = new ArrayList(); + for (int i = 0; i < maximumSize; i++) { + permutation.add(i); + } + Collections.shuffle(permutation); + for (final int index : permutation) { + cache.get("key" + index).toCompletableFuture().join(); + } + for (int i = 0; i < maximumSize; i++) { + cache.set("random" + permutation.get(i), "random_value").toCompletableFuture().join(); + assert cache.getEventQueue().get(i) instanceof Eviction; + final var eviction = (Eviction) cache.getEventQueue().get(i); + Assert.assertEquals(Eviction.Type.REPLACEMENT, eviction.getType()); + Assert.assertEquals("key" + permutation.get(i), eviction.getElement().getKey()); + } + } + + @Test + public void Eviction_LFU() { + final var maximumSize = 2; + final var cache = new CacheBuilder() + .maximumSize(maximumSize) + .evictionAlgorithm(EvictionAlgorithm.LFU) + .fetchAlgorithm(FetchAlgorithm.WRITE_BACK) + .dataSource(writeBackDataSource) + .build(); + cache.get(PROFILE_MUMBAI_ENGINEER).toCompletableFuture().join(); + for (int i = 0; i < maximumSize; i++) { + cache.set("key" + i, "value" + i).toCompletableFuture().join(); + } + Assert.assertEquals(2, cache.getEventQueue().size()); + assert cache.getEventQueue().get(0) instanceof Load; + assert cache.getEventQueue().get(1) instanceof Eviction; + final var evictionEvent = (Eviction) cache.getEventQueue().get(1); + Assert.assertEquals(Eviction.Type.REPLACEMENT, evictionEvent.getType()); + Assert.assertEquals("key0", evictionEvent.getElement().getKey()); + for (int i = 0; i < maximumSize; i++) { + acceptWrite(); + } + final var permutation = new ArrayList(); + for (int i = 0; i < maximumSize; i++) { + permutation.add(i); + } + Collections.shuffle(permutation); + for (final int index : permutation) { + for (int i = 0; i <= index; i++) { + cache.get("key" + index).toCompletableFuture().join(); + } + } + cache.getEventQueue().clear(); + for (int i = 0; i < maximumSize; i++) { + cache.set("random" + i, "random_value").toCompletableFuture().join(); + acceptWrite(); + for (int j = 0; j <= maximumSize; j++) { + cache.get("random" + i).toCompletableFuture().join(); + } + Assert.assertEquals(Eviction.class.getName(), cache.getEventQueue().get(i * 2).getClass().getName()); + Assert.assertEquals(Write.class.getName(), cache.getEventQueue().get(i * 2 + 1).getClass().getName()); + final var eviction = (Eviction) cache.getEventQueue().get(i * 2); + System.out.println(cache.getEventQueue().get(i)); + Assert.assertEquals(Eviction.Type.REPLACEMENT, eviction.getType()); + Assert.assertEquals("key" + i, eviction.getElement().getKey()); + } + } + + @Test + public void ExpiryOnGet() { + final var timer = new SettableTimer(); + final var startTime = System.nanoTime(); + final var cache = new CacheBuilder().timer(timer).dataSource(dataSource).expiryTime(Duration.ofSeconds(10)).build(); + timer.setTime(startTime); + cache.get(PROFILE_MUMBAI_ENGINEER).toCompletableFuture().join(); + Assert.assertEquals(1, cache.getEventQueue().size()); + assert cache.getEventQueue().get(0) instanceof Load; + Assert.assertEquals(PROFILE_MUMBAI_ENGINEER, cache.getEventQueue().get(0).getElement().getKey()); + timer.setTime(startTime + Duration.ofSeconds(10).toNanos() + 1); + cache.get(PROFILE_MUMBAI_ENGINEER).toCompletableFuture().join(); + Assert.assertEquals(3, cache.getEventQueue().size()); + assert cache.getEventQueue().get(1) instanceof Eviction; + assert cache.getEventQueue().get(2) instanceof Load; + final var eviction = (Eviction) cache.getEventQueue().get(1); + Assert.assertEquals(Eviction.Type.EXPIRY, eviction.getType()); + Assert.assertEquals(PROFILE_MUMBAI_ENGINEER, eviction.getElement().getKey()); + } + + @Test + public void ExpiryOnSet() { + final var timer = new SettableTimer(); + final var startTime = System.nanoTime(); + final var cache = new CacheBuilder().timer(timer).dataSource(dataSource).expiryTime(Duration.ofSeconds(10)).build(); + timer.setTime(startTime); + cache.get(PROFILE_MUMBAI_ENGINEER).toCompletableFuture().join(); + Assert.assertEquals(1, cache.getEventQueue().size()); + assert cache.getEventQueue().get(0) instanceof Load; + Assert.assertEquals(PROFILE_MUMBAI_ENGINEER, cache.getEventQueue().get(0).getElement().getKey()); + timer.setTime(startTime + Duration.ofSeconds(10).toNanos() + 1); + cache.set(PROFILE_MUMBAI_ENGINEER, "blue").toCompletableFuture().join(); + Assert.assertEquals(3, cache.getEventQueue().size()); + assert cache.getEventQueue().get(1) instanceof Eviction; + assert cache.getEventQueue().get(2) instanceof Write; + final var eviction = (Eviction) cache.getEventQueue().get(1); + Assert.assertEquals(Eviction.Type.EXPIRY, eviction.getType()); + Assert.assertEquals(PROFILE_MUMBAI_ENGINEER, eviction.getElement().getKey()); + } + + @Test + public void ExpiryOnEviction() { + final var timer = new SettableTimer(); + final var startTime = System.nanoTime(); + final var cache = new CacheBuilder().maximumSize(2).timer(timer).dataSource(dataSource).expiryTime(Duration.ofSeconds(10)).build(); + timer.setTime(startTime); + cache.get(PROFILE_MUMBAI_ENGINEER).toCompletableFuture().join(); + cache.get(PROFILE_HYDERABAD_ENGINEER).toCompletableFuture().join(); + timer.setTime(startTime + Duration.ofSeconds(10).toNanos() + 1); + cache.set("randomKey", "randomValue").toCompletableFuture().join(); + Assert.assertEquals(5, cache.getEventQueue().size()); + assert cache.getEventQueue().get(2) instanceof Eviction; + assert cache.getEventQueue().get(3) instanceof Eviction; + assert cache.getEventQueue().get(4) instanceof Write; + final var eviction1 = (Eviction) cache.getEventQueue().get(2); + Assert.assertEquals(Eviction.Type.EXPIRY, eviction1.getType()); + Assert.assertEquals(PROFILE_MUMBAI_ENGINEER, eviction1.getElement().getKey()); + final var eviction2 = (Eviction) cache.getEventQueue().get(3); + Assert.assertEquals(Eviction.Type.EXPIRY, eviction2.getType()); + Assert.assertEquals(PROFILE_HYDERABAD_ENGINEER, eviction2.getElement().getKey()); + } + + @Test + public void FetchingWriteBack() { + final var cache = new CacheBuilder() + .maximumSize(1) + .dataSource(writeBackDataSource) + .fetchAlgorithm(FetchAlgorithm.WRITE_BACK) + .build(); + cache.set("randomKey", "randomValue").toCompletableFuture().join(); + Assert.assertEquals(0, cache.getEventQueue().size()); + Assert.assertNull(dataMap.get("randomValue")); + acceptWrite(); + } + + @Test + public void FetchingWriteThrough() { + final var cache = new CacheBuilder().dataSource(dataSource).fetchAlgorithm(FetchAlgorithm.WRITE_THROUGH).build(); + cache.set("randomKey", "randomValue").toCompletableFuture().join(); + Assert.assertEquals(1, cache.getEventQueue().size()); + assert cache.getEventQueue().get(0) instanceof Write; + Assert.assertEquals("randomValue", dataMap.get("randomKey")); + } + + @Test + public void EagerLoading() { + final var eagerlyLoad = new HashSet(); + eagerlyLoad.add(PROFILE_MUMBAI_ENGINEER); + eagerlyLoad.add(PROFILE_HYDERABAD_ENGINEER); + final var cache = new CacheBuilder() + .loadKeysOnStart(eagerlyLoad) + .dataSource(dataSource) + .build(); + Assert.assertEquals(2, cache.getEventQueue().size()); + assert cache.getEventQueue().get(0) instanceof Load; + assert cache.getEventQueue().get(1) instanceof Load; + cache.getEventQueue().clear(); + dataMap.clear(); + isEqualTo(cache.get(PROFILE_MUMBAI_ENGINEER), "violet"); + isEqualTo(cache.get(PROFILE_HYDERABAD_ENGINEER), "blue"); + Assert.assertEquals(0, cache.getEventQueue().size()); + } + + @Test + public void RaceConditions() throws ExecutionException, InterruptedException { + final var cache = new CacheBuilder() + .poolSize(8) + .dataSource(dataSource).build(); + final var cacheEntries = new HashMap>(); + final var numberOfEntries = 100; + final var numberOfValues = 1000; + final String[] keyList = new String[numberOfEntries]; + final Map inverseMapping = new HashMap<>(); + for (int entry = 0; entry < numberOfEntries; entry++) { + final var key = UUID.randomUUID().toString(); + keyList[entry] = key; + inverseMapping.put(key, entry); + cacheEntries.put(key, new ArrayList<>()); + final var firstValue = UUID.randomUUID().toString(); + dataMap.put(key, firstValue); + cacheEntries.get(key).add(firstValue); + for (int value = 0; value < numberOfValues - 1; value++) { + cacheEntries.get(key).add(UUID.randomUUID().toString()); + } + } + final Random random = new Random(); + final List> futures = new ArrayList<>(); + final List queries = new ArrayList<>(); + final int[] updates = new int[numberOfEntries]; + for (int i = 0; i < 1000000; i++) { + final var index = random.nextInt(numberOfEntries); + final var key = keyList[index]; + if (Math.random() <= 0.05) { + if (updates[index] - 1 < numberOfEntries) { + updates[index]++; + } + cache.set(key, cacheEntries.get(key).get(updates[index] + 1)); + } else { + queries.add(key); + futures.add(cache.get(key)); + } + } + final CompletionStage> results = CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new)) + .thenApply(__ -> futures.stream() + .map(CompletionStage::toCompletableFuture) + .map(CompletableFuture::join) + .collect(Collectors.toList())); + final int[] currentIndexes = new int[numberOfEntries]; + final StringBuilder stringBuilder = new StringBuilder(); + results.thenAccept(values -> { + for (int i = 0; i < values.size(); i++) { + final var key = queries.get(i); + final var possibleValuesForKey = cacheEntries.get(key); + final var currentValue = currentIndexes[inverseMapping.get(key)]; + if (!possibleValuesForKey.get(currentValue).equals(values.get(i))) { + int offset = 1; + while (currentValue + offset < numberOfValues && !possibleValuesForKey.get(currentValue + offset).equals(values.get(i))) { + offset++; + } + if (currentValue + offset == numberOfValues) { + System.out.println(Arrays.stream(stringBuilder.toString().split("\n")).filter(line -> line.contains(key)).collect(Collectors.joining("\n"))); + System.err.println(key); + System.err.println(possibleValuesForKey); + System.err.println(possibleValuesForKey.get(currentValue) + " index: " + currentIndexes[inverseMapping.get(key)]); + System.err.println(values.get(i)); + throw new IllegalStateException(); + } + currentIndexes[inverseMapping.get(key)] += offset; + stringBuilder.append(key).append(" index: ").append(currentIndexes[inverseMapping.get(key)]).append(" ").append(values.get(i)).append('\n'); + } + } + }).toCompletableFuture().join(); + } + + private boolean isEqualTo(CompletionStage future, String value) { + return future.thenApply(result -> { + if (result.equals(value)) { + return true; + } else { + throw new AssertionError(); + } + }).toCompletableFuture().join(); + } +} diff --git a/distributed-cache/src/test/java/models/SettableTimer.java b/distributed-cache/src/test/java/models/SettableTimer.java new file mode 100644 index 0000000..08c4111 --- /dev/null +++ b/distributed-cache/src/test/java/models/SettableTimer.java @@ -0,0 +1,14 @@ +package models; + +public class SettableTimer extends Timer { + private long time = -1; + + @Override + public long getCurrentTime() { + return time == -1 ? System.nanoTime() : time; + } + + public void setTime(long time) { + this.time = time; + } +} diff --git a/distributed-cache/target/classes/Cache.class b/distributed-cache/target/classes/Cache.class new file mode 100644 index 0000000000000000000000000000000000000000..d0cde8bf683bac9e2ece47014c16dd0f49010cd6 GIT binary patch literal 14690 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 literal 0 HcmV?d00001 diff --git a/distributed-cache/target/classes/CacheBuilder.class b/distributed-cache/target/classes/CacheBuilder.class new file mode 100644 index 0000000000000000000000000000000000000000..16194b63ea94e236fb3de00ed621b8b1331e1b5e GIT binary patch literal 3207 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 bnv!7DhW5n_p5g^R>>1o+zMY6H|5JYf6Zdc> literal 0 HcmV?d00001 diff --git a/distributed-cache/target/classes/events/Load.class b/distributed-cache/target/classes/events/Load.class new file mode 100644 index 0000000000000000000000000000000000000000..aa916aaeae73032214787d5491d95c009ab98983 GIT binary patch literal 561 zcmZuuO;5r=6r6=hZ2?gb@Z*9v(Zn82EFmUF571~Lr1zz4a7jN%TS)v_o=iOW1N>3O zw-pE&o9uRGI&XI0>(}?kCxC05n8>54<4DIbL!l!>nRXd!=S~ofg`Zf@!i(au-D|rH z+8t>O6-S2RaUP6BJme#vl&TYX+;@2_6<>;4`XUpCvLhBEOcTqA_}FI9g)agURL=Gp z3`Qygk)%ABF|Zy(V>|bCw#u$=hIWR1d+6F-3U6*lZhSEYauV`%9@BMC|8%#uWnJEO z`Zvy$FSzCNaAG};rou~Y_aCjhy2ZWfaGklYoOo+*tV7?yqZ literal 0 HcmV?d00001 diff --git a/distributed-cache/target/classes/events/Update.class b/distributed-cache/target/classes/events/Update.class new file mode 100644 index 0000000000000000000000000000000000000000..e74578691fe8dd88fa71c4bf1555b35410250cfa GIT binary patch literal 700 zcmZuu$xa(V6s)$5*C8Yr?Cgp;5eH$33ld{lNKtYyRw4yw?rpn8+M30h#S+*2Q4R=+ z10TRgA*#oV7%P|QSM_S@z3P5=yt@Z*ijP$k@TP)1-j?vLg!cm5vrx@-kVKAj6D6=S z@PdhQqs}ii4#J5oP`T7szKoNQ-DbAi9bVW&$L>*8>}p@fJ%NM6tj6Ae{m+g-;VXxj zVcJ(e5^to!p&Yqnb_c;&x{eIB!Ar6b|Iv|AtGV*ysPlU^k+I^?l5&+tic7}YQ&B9v znSkgEG_pR|vz1pfYw|3?$;RUARyM$zPJ^6xFU|&2IhP$*`d6Kwqp2Flw(}pZw;KG- zgjl->R4;=h9INlzkhx)-^JJsoLUcwZF7(A=*bb$o(VE!{YY^Pf%N=wnk=~sGa&+?ClWoPrOY1 pO=TwKQtlP!=22(;DfKY=JtI%`W{ZWsO#NcB{dLtqlieI%KLKuVowNV| literal 0 HcmV?d00001 diff --git a/distributed-cache/target/classes/events/Write.class b/distributed-cache/target/classes/events/Write.class new file mode 100644 index 0000000000000000000000000000000000000000..ff5ad211123908c4c26631b0cc34c819769266d5 GIT binary patch literal 565 zcmZuu%TB^T6g>l#N<~o6;$wq5ABkO>SVByUE}+pwNL>$QfA7d--hO<(y#u(wfq^2LI`(wzGnCpglxc^daqI-qNcf5MAiO9Z+1(SD zLAxf6q2|a?+|7cahzES=lTvpgkNYl$G7w41gDC^+GMM?`=jkHc%%!#mJ$vBV9ZGNJGgrQp`*IxebQaTv5C8PHv}Ju> zwR@M&gwMI<^Kfk44=2J)ZTBCov$)2M>T#LEVDzI|?1@{c#$#lypR2?QG^{aHR%h}{ zvlJYmKtDx2Gz8@*ULqz4YxLGn&swiwF9eIwX*b9KtdprsJJ8C?bz%lFD!>LR6s6E6 fs^qQ_HnS?_+ES*>wN0)%;Q|`8zR?bLGsXM@*}Z@! literal 0 HcmV?d00001 diff --git a/distributed-cache/target/classes/models/AccessDetails.class b/distributed-cache/target/classes/models/AccessDetails.class new file mode 100644 index 0000000000000000000000000000000000000000..c0c58e77e6bab71339b9e7ea6b5f0ea89d7001b9 GIT binary patch literal 2076 zcma)6-%}e^6#h0p*koA(O@TszLY3MOenhR>mLIh=6&nnohElQW%q3Z2%d(5f4Kp3b zcmDt%9AETBeZd)>X@O}+$0r~Bn;ggQ?ruXuaddX}-o5vp^L^)h=iL4C?&oyAwY;k1nt|(>HZX%(19P~MCe|$t?V?<^ zYPGyqccq5Q#RpdhX_YrD9cQG^89+lk>jss=Z@r z&=xhc&DyRlZ)k{R7RS~&<-H1zI*YbzE!TH8tm>-RaJZ8$dS&6PiK?yE;bu&3*)ZbeAa^)sA*L+uCz2x;hK zeO4C^_aB%srY@Jnva;R+!EBVqa;Ondr9nuX(hHqZ7UEW zA-$5U+U}-??#x&dMzG5qjgnWdmaPTbSE)sTDMe|b2YC&b{#ThyoI%>eS#+DYjUker zrmTYOHSv}j7Vx%-3mDeWbuvL6cT5zpsN)?q7j-O|SjGxXZ+V`q$*S00vgDRmsdcvD z-2~n<@jgl#SPQm7(qZ$g38&6-L`9{dA;F^M*+rE4Y>OEFrmFXG2nAPKn^skWzNa8o zK4qI^3e5_J7OFFk-BiOWbXLE0vw`k+dQe=(5mTj7(D$3{pOwB~U=?>YOtnx6lO<`9w%qJ zd)~H{3rqse1yPn>^`VZB*xW_W+ph0wFbl40Rehx`2J&@tuZI{xG4BYs;#jrOz;UKY zmoSW`ofgN{hr8A(=BFw9JVahNPRW+mjE;|=b-RDrS}0m$({SNA9a?h0Mg9p*{`MkB ztDE6}@p5q1!TXM0j_0_#izpwUCwmAj`xKG&Lqs1VmK}bK_%FU&AICIe91V``=p=AA zI?&IZUJ}q5=P|%peJK+DI53EzkRnGLjc4)fBgB8=M+6G8&EM?uVa&iFgkg3V4xO)r z&Ph7Q4$$`H{t1BYCV*rEV1xn$2@1sh*ax0MUmre1VtJBCA(|l$kevLv0h#i(=tGjA z{RC4fj`({6jT8sM6q6L?1*RP((Tf-j>AoeI(sMdH{sZE%{qaAL${xTN=leb4(f#q? zp)V;VCis5!4bnVqpWyo)^w^JxMOA`PU!n8V2vkC641G*!hoeaXnZIo4C~Nus>+Nt#9g2^sxpJ|Utz9CRU}9Jr>`q{;zvl8j?j5h z?~G3!6l}1ecNXJBQVn&@7X-{YkU9PYUBI!#LD?spP*NdE6chB1U=r(`4dUIyeSAR6 MH@Qx5eUtC}KWfa$zW@LL literal 0 HcmV?d00001 diff --git a/distributed-cache/target/classes/models/EvictionAlgorithm.class b/distributed-cache/target/classes/models/EvictionAlgorithm.class new file mode 100644 index 0000000000000000000000000000000000000000..c4422ee2a194c42a3c2cfe66d27b8e8bdb2901e6 GIT binary patch literal 961 zcmaJ!lwqjPqgMuE{0rF1R~yMDE~+5joo2T;}b#E6xyN}@OT&zlVZt4 zzUlgt%bO|cRxs>0Z}+%O4b|J>FmkTP*QOV3Qa((ZY-Z8_a#ZE<*u%4SdrH8Ai=C6 zkB3;D7yH4=6hbk6NcowSl;-=kk{&G z#<@{+47Xcs2EOffOZr?|weFdQ(>8pY_n9;kU87Gu)&CnY#LBj72fGZJ8+Z=&HiH=S zZ1Rz4+FjQOhCX!?G@gi;O=nW9PqnVO!+v>=z+FDerZ@CWYv1NRmuE*^;8qnZ@+(|Y z@Dv4x+c#c{f((iZ?qFR(5-9~~q!?1yqvA3cqNd}y#B)oVme}Z=S!O_5UQC;=I>um7 znsV}Q+Ou6tdpq=m6nb~U-Wn$dXeWMPG0U}6MnnQCmO;!EZPLz{JOcZQ$Q6Xo6p=Oh z$HJUI_zYGi_~(d(r?M*Mz9D)Ak>evX_*FVl#we^I8|G;mgyM`l5aTz>r$u`gkI zn(n$>!d)&M_d?7D;<%5EiI5{Q!dKx3#8XugE)l;#A`E0P3>Kn4DJ6{PeVmFTEHp2` YINR8qOmg@zvLq%lVR%5};AXb|0_1?lng9R* literal 0 HcmV?d00001 diff --git a/distributed-cache/target/classes/models/Record.class b/distributed-cache/target/classes/models/Record.class new file mode 100644 index 0000000000000000000000000000000000000000..5746ab47f32d4f64341ed45ede44f583c984205a GIT binary patch literal 2115 zcmb7ETW=Fb7(L_L+K$1-C5F%jFa)|b0n;m#Tw2E@5aO1KxT#Q|HqIznve}Jx*O8*C z|D(@+uhdG7kf_uj&>vOx%MqJXX^6(o^ojSYcJhrR^LUDKA|4fZyr+cP#TBGs?fAHI*HK8Gpy5 zg7UF~7qaiTTfDk9%s$*dJ~2H^oev_9!d>LSiXKM-Nx5z5ucOs0L{?8;V131zGpecy2pHL!tlUB}rS>%kVr&ZNJ8~MLxoGJ~iwu zOL{Zm!MUW27&3IQ$A4%H{ihf|dRyW@N#vqqSk}7f%Q+2e0=E+JjngJ@_1!$w`Q8=z z++|QzZ3cD+trX7FX2+e_720gRV2?exU$YUEy`^&210Ea&UZ*AUG+M@ThikC1NG zkC18pjqJ<%Yvhig{Yg$5mpNWRj$?s07dcMwM%BINaE+@e?lieWf8>40S*=2SYNeE= z0!(R2ezNDR}{ea)Y F{{cL%vNr$# literal 0 HcmV?d00001 diff --git a/distributed-cache/target/classes/models/Timer.class b/distributed-cache/target/classes/models/Timer.class new file mode 100644 index 0000000000000000000000000000000000000000..65e9434b7d2897a73d8f5748910ef9d8ea5d8408 GIT binary patch literal 380 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 literal 0 HcmV?d00001 diff --git a/distributed-cache/target/test-classes/TestCache.class b/distributed-cache/target/test-classes/TestCache.class new file mode 100644 index 0000000000000000000000000000000000000000..14759e661da1325a6bcafa752a52a7a2b0f3047e GIT binary patch literal 18079 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 H!hii=S>nHj literal 0 HcmV?d00001 diff --git a/distributed-cache/target/test-classes/models/SettableTimer.class b/distributed-cache/target/test-classes/models/SettableTimer.class new file mode 100644 index 0000000000000000000000000000000000000000..0e2dfaef9ab9be02c3c46f6aad2c32c0288d66da GIT binary patch literal 587 zcmZvay-ve06orq|G-*TnO9}rJw$g!)NPxOjVkm{BRd=^oCD5cOaglfrCSHJv0SN?R z-~o6j;M(#45{YDA-*eA*Z>)TNy?+4M$F>I-E7@=J0&UZH7!^cSuTSc1}vE7sldj9*smY;G-#Js=e8mPlr4S)!8I%c@d@zK`$cLG;Q^T zlv*Zya>!7e2-%q@iHPMuccdoNv(T4(e09pN4aQA{(d|Z8P2u%t^JFZJL*4lgb?=hj za33BPeB_XKvE`$VDnsqR85n$%R{ax(va+|PJf5`rx2Y5nLoVj=Ov%`xi*o4(87#%7 zC_^!|qSdF;H`Alck_xfgaT{ QFsj#|OFskxa$8vb0c0#_fB*mh literal 0 HcmV?d00001 diff --git a/distributed-event-bus/.idea/.gitignore b/distributed-event-bus/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/distributed-event-bus/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/distributed-event-bus/.idea/compiler.xml b/distributed-event-bus/.idea/compiler.xml new file mode 100644 index 0000000..f84848a --- /dev/null +++ b/distributed-event-bus/.idea/compiler.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/distributed-event-bus/.idea/jarRepositories.xml b/distributed-event-bus/.idea/jarRepositories.xml new file mode 100644 index 0000000..712ab9d --- /dev/null +++ b/distributed-event-bus/.idea/jarRepositories.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/distributed-event-bus/.idea/libraries/Maven__aopalliance_aopalliance_1_0.xml b/distributed-event-bus/.idea/libraries/Maven__aopalliance_aopalliance_1_0.xml new file mode 100644 index 0000000..30ff5cb --- /dev/null +++ b/distributed-event-bus/.idea/libraries/Maven__aopalliance_aopalliance_1_0.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/distributed-event-bus/.idea/libraries/Maven__com_google_code_gson_gson_2_8_6.xml b/distributed-event-bus/.idea/libraries/Maven__com_google_code_gson_gson_2_8_6.xml new file mode 100644 index 0000000..82a9f20 --- /dev/null +++ b/distributed-event-bus/.idea/libraries/Maven__com_google_code_gson_gson_2_8_6.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/distributed-event-bus/.idea/libraries/Maven__com_google_guava_guava_16_0_1.xml b/distributed-event-bus/.idea/libraries/Maven__com_google_guava_guava_16_0_1.xml new file mode 100644 index 0000000..b7c7684 --- /dev/null +++ b/distributed-event-bus/.idea/libraries/Maven__com_google_guava_guava_16_0_1.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/distributed-event-bus/.idea/libraries/Maven__com_google_inject_guice_4_0.xml b/distributed-event-bus/.idea/libraries/Maven__com_google_inject_guice_4_0.xml new file mode 100644 index 0000000..221b0da --- /dev/null +++ b/distributed-event-bus/.idea/libraries/Maven__com_google_inject_guice_4_0.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/distributed-event-bus/.idea/libraries/Maven__javax_inject_javax_inject_1.xml b/distributed-event-bus/.idea/libraries/Maven__javax_inject_javax_inject_1.xml new file mode 100644 index 0000000..93cf65a --- /dev/null +++ b/distributed-event-bus/.idea/libraries/Maven__javax_inject_javax_inject_1.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/distributed-event-bus/.idea/libraries/Maven__junit_junit_4_13.xml b/distributed-event-bus/.idea/libraries/Maven__junit_junit_4_13.xml new file mode 100644 index 0000000..59fc5c4 --- /dev/null +++ b/distributed-event-bus/.idea/libraries/Maven__junit_junit_4_13.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/distributed-event-bus/.idea/libraries/Maven__org_hamcrest_hamcrest_core_1_3.xml b/distributed-event-bus/.idea/libraries/Maven__org_hamcrest_hamcrest_core_1_3.xml new file mode 100644 index 0000000..f58bbc1 --- /dev/null +++ b/distributed-event-bus/.idea/libraries/Maven__org_hamcrest_hamcrest_core_1_3.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/distributed-event-bus/.idea/misc.xml b/distributed-event-bus/.idea/misc.xml new file mode 100644 index 0000000..2e289ef --- /dev/null +++ b/distributed-event-bus/.idea/misc.xml @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/distributed-event-bus/.idea/modules.xml b/distributed-event-bus/.idea/modules.xml new file mode 100644 index 0000000..0b19bb8 --- /dev/null +++ b/distributed-event-bus/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/distributed-event-bus/.idea/uiDesigner.xml b/distributed-event-bus/.idea/uiDesigner.xml new file mode 100644 index 0000000..e96534f --- /dev/null +++ b/distributed-event-bus/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/distributed-event-bus/.idea/vcs.xml b/distributed-event-bus/.idea/vcs.xml new file mode 100644 index 0000000..6c0b863 --- /dev/null +++ b/distributed-event-bus/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/distributed-event-bus/README.ME b/distributed-event-bus/README.ME new file mode 100644 index 0000000..6d79a30 --- /dev/null +++ b/distributed-event-bus/README.ME @@ -0,0 +1,8 @@ +1) Multiple publishers and subscribers (Register from any class to eventbus) +2) Causal ordering of topics +3) Supports configurable retry attempts. +4) Have a dead letter queue. +5) Idempotency on event receiving +6) Allow both pull and push models +7) Allow subscribing from a timestamp or offset +8) Allow preconditions for event subscription \ No newline at end of file diff --git a/distributed-event-bus/distributed-event-bus.iml b/distributed-event-bus/distributed-event-bus.iml new file mode 100644 index 0000000..c03d76f --- /dev/null +++ b/distributed-event-bus/distributed-event-bus.iml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/distributed-event-bus/pom.xml b/distributed-event-bus/pom.xml new file mode 100644 index 0000000..b905052 --- /dev/null +++ b/distributed-event-bus/pom.xml @@ -0,0 +1,42 @@ + + + 4.0.0 + + interviewready.io + distributed-event-bus + 1.0 + + + + org.apache.maven.plugins + maven-compiler-plugin + + 11 + 11 + + + + + + + + junit + junit + 4.13 + test + + + com.google.inject + guice + 4.0 + + + com.google.code.gson + gson + 2.8.6 + + + + \ No newline at end of file diff --git a/distributed-event-bus/src/main/java/EventBus.java b/distributed-event-bus/src/main/java/EventBus.java new file mode 100644 index 0000000..581ccbf --- /dev/null +++ b/distributed-event-bus/src/main/java/EventBus.java @@ -0,0 +1,203 @@ +import com.google.inject.Inject; +import com.google.inject.Singleton; +import exceptions.RetryLimitExceededException; +import exceptions.UnsubscribedPollException; +import lib.KeyedExecutor; +import models.Event; +import models.FailureEvent; +import models.Subscription; +import util.Timer; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.*; +import java.util.function.Function; +import java.util.function.Predicate; + +@Singleton +public class EventBus { + private final Map> topics; + private final Map> eventIndexes; + private final Map> eventTimestamps; + private final Map> pullSubscriptions; + private final Map> pushSubscriptions; + private final KeyedExecutor eventExecutor; + private final KeyedExecutor broadcastExecutor; + private EventBus deadLetterQueue; + private final Timer timer; + + @Inject + public EventBus(final KeyedExecutor eventExecutor, final KeyedExecutor broadcastExecutor, final Timer timer) { + this.topics = new ConcurrentHashMap<>(); + this.eventIndexes = new ConcurrentHashMap<>(); + this.eventTimestamps = new ConcurrentHashMap<>(); + this.pullSubscriptions = new ConcurrentHashMap<>(); + this.pushSubscriptions = new ConcurrentHashMap<>(); + this.eventExecutor = eventExecutor; + this.broadcastExecutor = broadcastExecutor; + this.timer = timer; + } + + public void setDeadLetterQueue(final EventBus deadLetterQueue) { + this.deadLetterQueue = deadLetterQueue; + } + + public CompletionStage publish(final String topic, final Event event) { + return eventExecutor.getThreadFor(topic, publishToBus(topic, event)); + } + + private CompletionStage publishToBus(final String topic, final Event event) { + if (eventIndexes.containsKey(topic) && eventIndexes.get(topic).containsKey(event.getId())) { + return null; + } + topics.putIfAbsent(topic, new CopyOnWriteArrayList<>()); + eventIndexes.putIfAbsent(topic, new ConcurrentHashMap<>()); + eventIndexes.get(topic).put(event.getId(), topics.get(topic).size()); + eventTimestamps.putIfAbsent(topic, new ConcurrentSkipListMap<>()); + eventTimestamps.get(topic).put(timer.getCurrentTime(), event.getId()); + topics.get(topic).add(event); + return notifyPushSubscribers(topic, event); + } + + private CompletionStage notifyPushSubscribers(String topic, Event event) { + if (!pushSubscriptions.containsKey(topic)) { + return CompletableFuture.completedStage(null); + } + final var subscribersForTopic = pushSubscriptions.get(topic); + final var notifications = subscribersForTopic.values() + .stream() + .filter(subscription -> subscription.getPrecondition().test(event)) + .map(subscription -> executeEventHandler(event, subscription)) + .toArray(CompletableFuture[]::new); + return CompletableFuture.allOf(notifications); + } + + private CompletionStage executeEventHandler(final Event event, Subscription subscription) { + return broadcastExecutor.getThreadFor(subscription.getTopic() + subscription.getSubscriber(), + doWithRetry(event, subscription.getEventHandler(), + 1, subscription.getNumberOfRetries()) + .exceptionally(throwable -> { + if (deadLetterQueue != null) { + deadLetterQueue.publish(subscription.getTopic(), new FailureEvent(event, throwable, timer.getCurrentTime())); + } + return null; + })); + } + + private CompletionStage doWithRetry(final Event event, + final Function> task, + final int coolDownIntervalInMillis, + final int remainingTries) { + return task.apply(event).handle((__, throwable) -> { + if (throwable != null) { + if (remainingTries == 1) { + throw new RetryLimitExceededException(throwable); + } + try { + Thread.sleep(coolDownIntervalInMillis); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + return doWithRetry(event, task, Math.max(coolDownIntervalInMillis * 2, 10), remainingTries - 1); + } else { + return CompletableFuture.completedFuture((Void) null); + } + }).thenCompose(Function.identity()); + } + + + public CompletionStage poll(final String topic, final String subscriber) { + return eventExecutor.getThreadFor(topic + subscriber, () -> pollBus(topic, subscriber)); + } + + private Event pollBus(final String topic, final String subscriber) { + var subscription = pullSubscriptions.getOrDefault(topic, new HashMap<>()).get(subscriber); + if (subscription == null) { + throw new UnsubscribedPollException(); + } + for (var index = subscription.getCurrentIndex(); index.intValue() < topics.get(topic).size(); index.increment()) { + var event = topics.get(topic).get(index.intValue()); + if (subscription.getPrecondition().test(event)) { + index.increment(); + return event; + } + } + return null; + } + + public CompletionStage subscribeToEventsAfter(final String topic, final String subscriber, final long timeStamp) { + return eventExecutor.getThreadFor(topic + subscriber, () -> moveIndexAtTimestamp(topic, subscriber, timeStamp)); + } + + private void moveIndexAtTimestamp(final String topic, final String subscriber, final long timeStamp) { + final var closestEventAfter = eventTimestamps.get(topic).higherEntry(timeStamp); + if (closestEventAfter == null) { + pullSubscriptions.get(topic).get(subscriber).setCurrentIndex(eventIndexes.get(topic).size()); + } else { + final var eventIndex = eventIndexes.get(topic).get(closestEventAfter.getValue()); + pullSubscriptions.get(topic).get(subscriber).setCurrentIndex(eventIndex); + } + } + + public CompletionStage subscribeToEventsAfter(final String topic, final String subscriber, final String eventId) { + return eventExecutor.getThreadFor(topic + subscriber, () -> moveIndexAfterEvent(topic, subscriber, eventId)); + } + + private void moveIndexAfterEvent(final String topic, final String subscriber, final String eventId) { + if (eventId == null) { + pullSubscriptions.get(topic).get(subscriber).setCurrentIndex(0); + } else { + final var eventIndex = eventIndexes.get(topic).get(eventId) + 1; + pullSubscriptions.get(topic).get(subscriber).setCurrentIndex(eventIndex); + } + } + + public CompletionStage subscribeForPush(final String topic, + final String subscriber, + final Predicate precondition, + final Function> handler, + final int numberOfRetries) { + return eventExecutor.getThreadFor(topic + subscriber, + () -> subscribeForPushEvents(topic, subscriber, precondition, handler, numberOfRetries)); + } + + private void subscribeForPushEvents(final String topic, + final String subscriber, + final Predicate precondition, + final Function> handler, + final int numberOfRetries) { + addSubscriber(pushSubscriptions, subscriber, precondition, topic, handler, numberOfRetries); + } + + private void addSubscriber(final Map> pullSubscriptions, + final String subscriber, + final Predicate precondition, + final String topic, + final Function> handler, + final int numberOfRetries) { + pullSubscriptions.putIfAbsent(topic, new ConcurrentHashMap<>()); + final var subscription = new Subscription(topic, subscriber, precondition, handler, numberOfRetries); + subscription.setCurrentIndex(topics.getOrDefault(topic, new ArrayList<>()).size()); + pullSubscriptions.get(topic).put(subscriber, subscription); + } + + public CompletionStage subscribeForPull(final String topic, final String subscriber, final Predicate precondition) { + return eventExecutor.getThreadFor(topic + subscriber, () -> subscribeForPullEvents(topic, subscriber, precondition)); + } + + private void subscribeForPullEvents(final String topic, final String subscriber, final Predicate precondition) { + addSubscriber(pullSubscriptions, subscriber, precondition, topic, null, 0); + } + + public CompletionStage unsubscribe(final String topic, final String subscriber) { + return eventExecutor.getThreadFor(topic + subscriber, () -> unsubscribeFromTopic(topic, subscriber)); + } + + private void unsubscribeFromTopic(final String topic, final String subscriber) { + pushSubscriptions.getOrDefault(topic, new HashMap<>()).remove(subscriber); + pullSubscriptions.getOrDefault(topic, new HashMap<>()).remove(subscriber); + } +} + diff --git a/distributed-event-bus/src/main/java/exceptions/RetryLimitExceededException.java b/distributed-event-bus/src/main/java/exceptions/RetryLimitExceededException.java new file mode 100644 index 0000000..ceb2ae1 --- /dev/null +++ b/distributed-event-bus/src/main/java/exceptions/RetryLimitExceededException.java @@ -0,0 +1,7 @@ +package exceptions; + +public class RetryLimitExceededException extends RuntimeException { + public RetryLimitExceededException(Throwable cause) { + super(cause); + } +} diff --git a/distributed-event-bus/src/main/java/exceptions/UnsubscribedPollException.java b/distributed-event-bus/src/main/java/exceptions/UnsubscribedPollException.java new file mode 100644 index 0000000..3a6e8e8 --- /dev/null +++ b/distributed-event-bus/src/main/java/exceptions/UnsubscribedPollException.java @@ -0,0 +1,4 @@ +package exceptions; + +public class UnsubscribedPollException extends RuntimeException { +} diff --git a/distributed-event-bus/src/main/java/lib/KeyedExecutor.java b/distributed-event-bus/src/main/java/lib/KeyedExecutor.java new file mode 100644 index 0000000..ddc628f --- /dev/null +++ b/distributed-event-bus/src/main/java/lib/KeyedExecutor.java @@ -0,0 +1,31 @@ +package lib; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.function.Function; +import java.util.function.Supplier; + +public class KeyedExecutor { + private final Executor[] executorPool; + + public KeyedExecutor(final int poolSize) { + this.executorPool = new Executor[poolSize]; + for (int i = 0; i < poolSize; i++) { + executorPool[i] = Executors.newSingleThreadExecutor(); + } + } + + public CompletionStage getThreadFor(KEY key, Runnable task) { + return CompletableFuture.runAsync(task, executorPool[Math.abs(key.hashCode() % executorPool.length)]); + } + + public CompletionStage getThreadFor(KEY key, Supplier task) { + return CompletableFuture.supplyAsync(task, executorPool[Math.abs(key.hashCode() % executorPool.length)]); + } + + public CompletionStage getThreadFor(KEY key, CompletionStage task) { + return CompletableFuture.supplyAsync(() -> task, executorPool[Math.abs(key.hashCode() % executorPool.length)]).thenCompose(Function.identity()); + } +} diff --git a/distributed-event-bus/src/main/java/models/Event.java b/distributed-event-bus/src/main/java/models/Event.java new file mode 100644 index 0000000..417deff --- /dev/null +++ b/distributed-event-bus/src/main/java/models/Event.java @@ -0,0 +1,43 @@ +package models; + +import java.util.Objects; +import java.util.UUID; + +public class Event { + private final String id; + private final String publisher; + private final EventType eventType; + private final String description; + private final long creationTime; + + public Event(final String publisher, + final EventType eventType, + final String description, + final long creationTime) { + this.description = description; + this.id = UUID.randomUUID().toString(); + this.publisher = publisher; + this.eventType = eventType; + this.creationTime = creationTime; + } + + public String getId() { + return id; + } + + public String getPublisher() { + return publisher; + } + + public EventType getEventType() { + return eventType; + } + + public long getCreationTime() { + return creationTime; + } + + public String getDescription() { + return description; + } +} \ No newline at end of file diff --git a/distributed-event-bus/src/main/java/models/EventType.java b/distributed-event-bus/src/main/java/models/EventType.java new file mode 100644 index 0000000..4b74232 --- /dev/null +++ b/distributed-event-bus/src/main/java/models/EventType.java @@ -0,0 +1,5 @@ +package models; + +public enum EventType { + PRIORITY, LOGGING, ERROR +} diff --git a/distributed-event-bus/src/main/java/models/FailureEvent.java b/distributed-event-bus/src/main/java/models/FailureEvent.java new file mode 100644 index 0000000..4bf5c86 --- /dev/null +++ b/distributed-event-bus/src/main/java/models/FailureEvent.java @@ -0,0 +1,20 @@ +package models; + +public class FailureEvent extends Event { + private final Event event; + private final Throwable throwable; + + public FailureEvent(Event event, Throwable throwable, long failureTimestamp) { + super("dead-letter-queue", EventType.ERROR, throwable.getMessage(), failureTimestamp); + this.event = event; + this.throwable = throwable; + } + + public Event getEvent() { + return event; + } + + public Throwable getThrowable() { + return throwable; + } +} diff --git a/distributed-event-bus/src/main/java/models/Subscription.java b/distributed-event-bus/src/main/java/models/Subscription.java new file mode 100644 index 0000000..f23c11a --- /dev/null +++ b/distributed-event-bus/src/main/java/models/Subscription.java @@ -0,0 +1,57 @@ +package models; + +import java.util.concurrent.CompletionStage; +import java.util.concurrent.atomic.LongAdder; +import java.util.function.Function; +import java.util.function.Predicate; + +public class Subscription { + private final String topic; + private final String subscriber; + private final Predicate precondition; + private final Function> eventHandler; + private final int numberOfRetries; + private final LongAdder currentIndex; + + public Subscription(final String topic, + final String subscriber, + final Predicate precondition, + final Function> eventHandler, + final int numberOfRetries) { + this.topic = topic; + this.subscriber = subscriber; + this.precondition = precondition; + this.eventHandler = eventHandler; + this.currentIndex = new LongAdder(); + this.numberOfRetries = numberOfRetries; + } + + public String getTopic() { + return topic; + } + + public String getSubscriber() { + return subscriber; + } + + public Predicate getPrecondition() { + return precondition; + } + + public Function> getEventHandler() { + return eventHandler; + } + + public LongAdder getCurrentIndex() { + return currentIndex; + } + + public void setCurrentIndex(final int offset) { + currentIndex.reset(); + currentIndex.add(offset); + } + + public int getNumberOfRetries() { + return numberOfRetries; + } +} diff --git a/distributed-event-bus/src/main/java/util/Timer.java b/distributed-event-bus/src/main/java/util/Timer.java new file mode 100644 index 0000000..4e15d88 --- /dev/null +++ b/distributed-event-bus/src/main/java/util/Timer.java @@ -0,0 +1,10 @@ +package util; + +import com.google.inject.Singleton; + +@Singleton +public class Timer { + public long getCurrentTime() { + return System.nanoTime(); + } +} diff --git a/distributed-event-bus/src/main/resources/application.properties b/distributed-event-bus/src/main/resources/application.properties new file mode 100644 index 0000000..e69de29 diff --git a/distributed-event-bus/src/test/java/EventBusTest.java b/distributed-event-bus/src/test/java/EventBusTest.java new file mode 100644 index 0000000..45fccea --- /dev/null +++ b/distributed-event-bus/src/test/java/EventBusTest.java @@ -0,0 +1,340 @@ +import com.google.gson.Gson; +import exceptions.RetryLimitExceededException; +import exceptions.UnsubscribedPollException; +import lib.KeyedExecutor; +import models.Event; +import models.EventType; +import models.FailureEvent; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import util.Timer; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicLong; + + +// Causal ordering of topics + +public class EventBusTest { + public static final String TOPIC_1 = "topic-1"; + public static final String TOPIC_2 = "topic-2"; + public static final String PUBLISHER_1 = "publisher-1"; + public static final String SUBSCRIBER_1 = "subscriber-1"; + public static final String SUBSCRIBER_2 = "subscriber-2"; + private Timer timer; + private KeyedExecutor keyedExecutor; + private KeyedExecutor broadcastExecutor; + + @Before + public void setUp() { + keyedExecutor = new KeyedExecutor<>(16); + broadcastExecutor = new KeyedExecutor<>(16); + timer = new Timer(); + } + + private Event constructEvent(EventType priority, String description) { + return new Event(PUBLISHER_1, priority, description, timer.getCurrentTime()); + } + + @Test + public void defaultBehavior() { + final EventBus eventBus = new EventBus(keyedExecutor, broadcastExecutor, timer); + eventBus.publish(TOPIC_1, constructEvent(EventType.LOGGING, "first event")); + eventBus.subscribeForPull(TOPIC_1, SUBSCRIBER_1, (event) -> true).toCompletableFuture().join(); + Assert.assertNull(eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join()); + + eventBus.publish(TOPIC_1, constructEvent(EventType.PRIORITY, "second event")); + final Event secondEvent = eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join(); + + Assert.assertEquals(EventType.PRIORITY, secondEvent.getEventType()); + Assert.assertEquals("second event", secondEvent.getDescription()); + + eventBus.subscribeToEventsAfter(TOPIC_1, SUBSCRIBER_1, null).toCompletableFuture().join(); + final Event firstEvent = eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join(); + + Assert.assertEquals(EventType.LOGGING, firstEvent.getEventType()); + Assert.assertEquals("first event", firstEvent.getDescription()); + Assert.assertEquals(PUBLISHER_1, firstEvent.getPublisher()); + + final List eventCollector = new ArrayList<>(); + eventBus.subscribeForPush(TOPIC_1, + SUBSCRIBER_2, + (event) -> true, + (event) -> CompletableFuture.runAsync(() -> eventCollector.add(event)), + 0).toCompletableFuture().join(); + eventBus.publish(TOPIC_1, constructEvent(EventType.ERROR, "third event")).toCompletableFuture().join(); + + Assert.assertEquals(EventType.ERROR, eventCollector.get(0).getEventType()); + Assert.assertEquals("third event", eventCollector.get(0).getDescription()); + + eventBus.unsubscribe(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join(); + eventBus.publish(TOPIC_1, constructEvent(EventType.LOGGING, "fourth event")).toCompletableFuture().join(); + Assert.assertTrue(eventBus.poll(TOPIC_1, SUBSCRIBER_1) + .handle((__, throwable) -> throwable.getCause() instanceof UnsubscribedPollException) + .toCompletableFuture().join()); + + eventCollector.clear(); + eventBus.unsubscribe(TOPIC_1, SUBSCRIBER_2).toCompletableFuture().join(); + eventBus.publish(TOPIC_1, constructEvent(EventType.LOGGING, "fifth event")).toCompletableFuture().join(); + Assert.assertTrue(eventCollector.isEmpty()); + } + + @Test + public void indexMove() { + final EventBus eventBus = new EventBus(keyedExecutor, broadcastExecutor, timer); + eventBus.subscribeForPull(TOPIC_1, SUBSCRIBER_1, (event) -> true).toCompletableFuture().join(); + final Event firstEvent = constructEvent(EventType.PRIORITY, "first event"); + final Event secondEvent = constructEvent(EventType.PRIORITY, "second event"); + final Event thirdEvent = constructEvent(EventType.PRIORITY, "third event"); + eventBus.publish(TOPIC_1, firstEvent).toCompletableFuture().join(); + eventBus.publish(TOPIC_1, secondEvent).toCompletableFuture().join(); + eventBus.publish(TOPIC_1, thirdEvent).toCompletableFuture().join(); + + eventBus.subscribeToEventsAfter(TOPIC_1, SUBSCRIBER_1, secondEvent.getId()).toCompletableFuture().join(); + final Event firstPoll = eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join(); + Assert.assertEquals("third event", firstPoll.getDescription()); + Assert.assertNull(eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join()); + + eventBus.subscribeToEventsAfter(TOPIC_1, SUBSCRIBER_1, null).toCompletableFuture().join(); + final Event secondPoll = eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join(); + Assert.assertEquals("first event", secondPoll.getDescription()); + + eventBus.subscribeToEventsAfter(TOPIC_1, SUBSCRIBER_1, firstEvent.getId()).toCompletableFuture().join(); + final Event thirdPoll = eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join(); + Assert.assertEquals("second event", thirdPoll.getDescription()); + + eventBus.subscribeToEventsAfter(TOPIC_1, SUBSCRIBER_1, thirdEvent.getId()).toCompletableFuture().join(); + Assert.assertNull(eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join()); + } + + @Test + public void timestampMove() { + final TestTimer timer = new TestTimer(); + final EventBus eventBus = new EventBus(keyedExecutor, broadcastExecutor, timer); + eventBus.subscribeForPull(TOPIC_1, SUBSCRIBER_1, (event) -> true).toCompletableFuture().join(); + + final Event firstEvent = new Event(PUBLISHER_1, EventType.PRIORITY, "first event", timer.getCurrentTime()); + eventBus.publish(TOPIC_1, firstEvent).toCompletableFuture().join(); + timer.setCurrentTime(timer.getCurrentTime() + Duration.ofSeconds(10).toNanos()); + + final Event secondEvent = new Event(PUBLISHER_1, EventType.PRIORITY, "second event", timer.getCurrentTime()); + eventBus.publish(TOPIC_1, secondEvent).toCompletableFuture().join(); + timer.setCurrentTime(timer.getCurrentTime() + Duration.ofSeconds(10).toNanos()); + + final Event thirdEvent = new Event(PUBLISHER_1, EventType.PRIORITY, "third event", timer.getCurrentTime()); + eventBus.publish(TOPIC_1, thirdEvent).toCompletableFuture().join(); + + eventBus.subscribeToEventsAfter(TOPIC_1, SUBSCRIBER_1, secondEvent.getCreationTime() + Duration.ofSeconds(5).toNanos()).toCompletableFuture().join(); + final Event firstPoll = eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join(); + Assert.assertEquals("third event", firstPoll.getDescription()); + Assert.assertNull(eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join()); + + eventBus.subscribeToEventsAfter(TOPIC_1, SUBSCRIBER_1, 0).toCompletableFuture().join(); + final Event secondPoll = eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join(); + Assert.assertEquals("first event", secondPoll.getDescription()); + + eventBus.subscribeToEventsAfter(TOPIC_1, SUBSCRIBER_1, firstEvent.getCreationTime() + Duration.ofSeconds(5).toNanos()).toCompletableFuture().join(); + final Event thirdPoll = eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join(); + Assert.assertEquals("second event", thirdPoll.getDescription()); + + eventBus.subscribeToEventsAfter(TOPIC_1, SUBSCRIBER_1, thirdEvent.getCreationTime() + Duration.ofNanos(1).toNanos()).toCompletableFuture().join(); + Assert.assertNull(eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join()); + } + + @Test + public void idempotency() { + final EventBus eventBus = new EventBus(keyedExecutor, broadcastExecutor, timer); + eventBus.subscribeForPull(TOPIC_1, SUBSCRIBER_1, (event) -> true).toCompletableFuture().join(); + Event event1 = new Gson().fromJson("{\n" + + " \"id\": \"event-5435\",\n" + + " \"publisher\": \"random-publisher-1\",\n" + + " \"eventType\": \"LOGGING\",\n" + + " \"description\": \"random-event-1\",\n" + + " \"creationTime\": 31884739810179363\n" + + "}", Event.class); + eventBus.publish(TOPIC_1, event1); + + Event event2 = new Gson().fromJson("{\n" + + " \"id\": \"event-5435\",\n" + + " \"publisher\": \"random-publisher-2\",\n" + + " \"eventType\": \"PRIORITY\",\n" + + " \"description\": \"random-event-2\",\n" + + " \"creationTime\": 31824735510179363\n" + + "}", Event.class); + eventBus.publish(TOPIC_1, event2); + + + final Event firstEvent = eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join(); + Assert.assertEquals(EventType.LOGGING, firstEvent.getEventType()); + Assert.assertEquals("random-event-1", firstEvent.getDescription()); + Assert.assertNull(eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join()); + } + + @Test + public void unsubscribePushEvents() { + final EventBus eventBus = new EventBus(keyedExecutor, broadcastExecutor, timer); + final List topic1 = new ArrayList<>(), topic2 = new ArrayList<>(); + eventBus.subscribeForPush(TOPIC_1, SUBSCRIBER_1, event -> true, event -> { + topic1.add(event); + return CompletableFuture.completedStage(null); + }, 0).toCompletableFuture().join(); + eventBus.subscribeForPush(TOPIC_2, SUBSCRIBER_1, event -> true, event -> { + topic2.add(event); + return CompletableFuture.completedStage(null); + }, 0).toCompletableFuture().join(); + + for (int i = 0; i < 3; i++) { + eventBus.publish(TOPIC_1, constructEvent(EventType.PRIORITY, UUID.randomUUID().toString())).toCompletableFuture().join(); + } + eventBus.publish(TOPIC_2, constructEvent(EventType.PRIORITY, UUID.randomUUID().toString())).toCompletableFuture().join(); + eventBus.unsubscribe(TOPIC_1, SUBSCRIBER_1); + Assert.assertEquals(3, topic1.size()); + Assert.assertEquals(1, topic2.size()); + + for (int i = 0; i < 2; i++) { + eventBus.publish(TOPIC_2, constructEvent(EventType.PRIORITY, UUID.randomUUID().toString())).toCompletableFuture().join(); + } + for (int i = 0; i < 3; i++) { + eventBus.publish(TOPIC_1, constructEvent(EventType.PRIORITY, UUID.randomUUID().toString())).toCompletableFuture().join(); + } + Assert.assertEquals(3, topic1.size()); + Assert.assertEquals(3, topic2.size()); + + eventBus.subscribeForPush(TOPIC_1, SUBSCRIBER_1, event -> true, event -> { + topic1.add(event); + return CompletableFuture.completedStage(null); + }, 0).toCompletableFuture().join(); + for (int i = 0; i < 3; i++) { + eventBus.publish(TOPIC_1, constructEvent(EventType.PRIORITY, UUID.randomUUID().toString())).toCompletableFuture().join(); + } + Assert.assertEquals(6, topic1.size()); + Assert.assertEquals(3, topic2.size()); + } + + @Test + public void unsubscribePullEvents() { + final EventBus eventBus = new EventBus(keyedExecutor, broadcastExecutor, timer); + eventBus.subscribeForPull(TOPIC_1, SUBSCRIBER_1, event -> true).toCompletableFuture().join(); + eventBus.subscribeForPull(TOPIC_2, SUBSCRIBER_1, event -> true).toCompletableFuture().join(); + for (int i = 0; i < 3; i++) { + eventBus.publish(TOPIC_1, constructEvent(EventType.PRIORITY, UUID.randomUUID().toString())).toCompletableFuture().join(); + } + eventBus.publish(TOPIC_2, constructEvent(EventType.PRIORITY, UUID.randomUUID().toString())).toCompletableFuture().join(); + + for (int i = 0; i < 3; i++) { + Assert.assertNotNull(eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join()); + } + Assert.assertNotNull(eventBus.poll(TOPIC_2, SUBSCRIBER_1).toCompletableFuture().join()); + + eventBus.unsubscribe(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join(); + for (int i = 0; i < 2; i++) { + eventBus.publish(TOPIC_2, constructEvent(EventType.PRIORITY, UUID.randomUUID().toString())).toCompletableFuture().join(); + } + for (int i = 0; i < 3; i++) { + eventBus.publish(TOPIC_1, constructEvent(EventType.PRIORITY, UUID.randomUUID().toString())).toCompletableFuture().join(); + } + + Assert.assertTrue(eventBus.poll(TOPIC_1, SUBSCRIBER_1) + .handle((__, throwable) -> throwable.getCause() instanceof UnsubscribedPollException).toCompletableFuture().join()); + for (int i = 0; i < 2; i++) { + Assert.assertNotNull(eventBus.poll(TOPIC_2, SUBSCRIBER_1).toCompletableFuture().join()); + } + + eventBus.subscribeForPull(TOPIC_1, SUBSCRIBER_1, event -> true).toCompletableFuture().join(); + for (int i = 0; i < 3; i++) { + eventBus.publish(TOPIC_1, constructEvent(EventType.PRIORITY, UUID.randomUUID().toString())).toCompletableFuture().join(); + } + + for (int i = 0; i < 3; i++) { + Assert.assertNotNull(eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join()); + } + Assert.assertNull(eventBus.poll(TOPIC_2, SUBSCRIBER_1).toCompletableFuture().join()); + } + + @Test + public void deadLetterQueue() { + final EventBus eventBus = new EventBus(keyedExecutor, broadcastExecutor, timer); + final EventBus dlq = new EventBus(new KeyedExecutor<>(3), new KeyedExecutor<>(3), new Timer()); + eventBus.setDeadLetterQueue(dlq); + dlq.subscribeForPull(TOPIC_1, SUBSCRIBER_1, event -> event.getEventType().equals(EventType.ERROR)); + final AtomicLong attempts = new AtomicLong(); + final int maxTries = 5; + eventBus.subscribeForPush(TOPIC_1, SUBSCRIBER_1, event -> true, event -> { + attempts.incrementAndGet(); + return CompletableFuture.failedStage(new RuntimeException()); + }, maxTries).toCompletableFuture().join(); + final Event event = new Event(PUBLISHER_1, EventType.LOGGING, "random", timer.getCurrentTime()); + eventBus.publish(TOPIC_1, event).toCompletableFuture().join(); + Assert.assertEquals(5, attempts.intValue()); + final Event failureEvent = dlq.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join(); + Assert.assertTrue(failureEvent instanceof FailureEvent); + Assert.assertEquals(event.getId(), ((FailureEvent) failureEvent).getEvent().getId()); + Assert.assertEquals(EventType.ERROR, failureEvent.getEventType()); + Assert.assertTrue(((FailureEvent) failureEvent).getThrowable().getCause() instanceof RetryLimitExceededException); + } + + @Test + public void retrySuccess() { + final EventBus eventBus = new EventBus(keyedExecutor, broadcastExecutor, timer); + final AtomicLong attempts = new AtomicLong(); + final int maxTries = 5; + final List events = new ArrayList<>(); + eventBus.subscribeForPush(TOPIC_1, SUBSCRIBER_1, event -> true, event -> { + if (attempts.incrementAndGet() == maxTries) { + events.add(event); + return CompletableFuture.completedStage(null); + } else { + return CompletableFuture.failedStage(new RuntimeException("TRY no: " + attempts.intValue())); + } + }, maxTries).toCompletableFuture().join(); + eventBus.publish(TOPIC_1, new Event(PUBLISHER_1, EventType.LOGGING, "random", timer.getCurrentTime())).toCompletableFuture().join(); + + Assert.assertEquals(EventType.LOGGING, events.get(0).getEventType()); + Assert.assertEquals("random", events.get(0).getDescription()); + Assert.assertEquals(5, attempts.intValue()); + Assert.assertEquals(1, events.size()); + } + + @Test + public void preconditionCheckForPush() { + final EventBus eventBus = new EventBus(keyedExecutor, broadcastExecutor, timer); + final List events = new ArrayList<>(); + eventBus.subscribeForPush(TOPIC_1, SUBSCRIBER_1, event -> event.getDescription().contains("-1"), event -> { + events.add(event); + return CompletableFuture.completedStage(null); + }, 0).toCompletableFuture().join(); + eventBus.publish(TOPIC_1, new Event(PUBLISHER_1, EventType.LOGGING, "random-event-1", timer.getCurrentTime())).toCompletableFuture().join(); + eventBus.publish(TOPIC_1, new Event(PUBLISHER_1, EventType.LOGGING, "random-event-2", timer.getCurrentTime())).toCompletableFuture().join(); + eventBus.publish(TOPIC_1, new Event(PUBLISHER_1, EventType.LOGGING, "random-event-12", timer.getCurrentTime())).toCompletableFuture().join(); + eventBus.publish(TOPIC_1, new Event(PUBLISHER_1, EventType.LOGGING, "random-event-21", timer.getCurrentTime())).toCompletableFuture().join(); + + Assert.assertEquals(events.size(), 2); + Assert.assertEquals(EventType.LOGGING, events.get(0).getEventType()); + Assert.assertEquals("random-event-1", events.get(0).getDescription()); + Assert.assertEquals(EventType.LOGGING, events.get(1).getEventType()); + Assert.assertEquals("random-event-12", events.get(1).getDescription()); + } + + @Test + public void preconditionCheckForPull() { + final EventBus eventBus = new EventBus(keyedExecutor, broadcastExecutor, timer); + eventBus.subscribeForPull(TOPIC_1, SUBSCRIBER_1, event -> event.getDescription().contains("-1")).toCompletableFuture().join(); + eventBus.publish(TOPIC_1, new Event(PUBLISHER_1, EventType.LOGGING, "random-event-1", timer.getCurrentTime())).toCompletableFuture().join(); + eventBus.publish(TOPIC_1, new Event(PUBLISHER_1, EventType.LOGGING, "random-event-2", timer.getCurrentTime())).toCompletableFuture().join(); + eventBus.publish(TOPIC_1, new Event(PUBLISHER_1, EventType.LOGGING, "random-event-12", timer.getCurrentTime())).toCompletableFuture().join(); + eventBus.publish(TOPIC_1, new Event(PUBLISHER_1, EventType.LOGGING, "random-event-21", timer.getCurrentTime())).toCompletableFuture().join(); + + final Event event1 = eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join(); + Assert.assertEquals(EventType.LOGGING, event1.getEventType()); + Assert.assertEquals("random-event-1", event1.getDescription()); + final Event event2 = eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join(); + Assert.assertEquals(EventType.LOGGING, event2.getEventType()); + Assert.assertEquals("random-event-12", event2.getDescription()); + Assert.assertNull(eventBus.poll(TOPIC_1, SUBSCRIBER_1).toCompletableFuture().join()); + } +} diff --git a/distributed-event-bus/src/test/java/TestTimer.java b/distributed-event-bus/src/test/java/TestTimer.java new file mode 100644 index 0000000..b6d818f --- /dev/null +++ b/distributed-event-bus/src/test/java/TestTimer.java @@ -0,0 +1,18 @@ +import util.Timer; + +public class TestTimer extends Timer { + private long currentTime; + + public TestTimer() { + this.currentTime = System.nanoTime(); + } + + @Override + public long getCurrentTime() { + return currentTime; + } + + public void setCurrentTime(final long currentTime) { + this.currentTime = currentTime; + } +} diff --git a/distributed-event-bus/target/classes/EventBus.class b/distributed-event-bus/target/classes/EventBus.class new file mode 100644 index 0000000000000000000000000000000000000000..94f6ec7b0e28d9171dbebef12a9b0a2ea39d2163 GIT binary patch literal 16455 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 literal 0 HcmV?d00001 diff --git a/distributed-event-bus/target/classes/META-INF/distributed-event-bus.kotlin_module b/distributed-event-bus/target/classes/META-INF/distributed-event-bus.kotlin_module new file mode 100644 index 0000000000000000000000000000000000000000..a49347afef10a9b5f95305e1058ba36adec7d6dd GIT binary patch literal 16 RcmZQzU|?ooU|@t|0RRA102TlM literal 0 HcmV?d00001 diff --git a/distributed-event-bus/target/classes/application.properties b/distributed-event-bus/target/classes/application.properties new file mode 100644 index 0000000..e69de29 diff --git a/distributed-event-bus/target/classes/exceptions/RetryLimitExceededException.class b/distributed-event-bus/target/classes/exceptions/RetryLimitExceededException.class new file mode 100644 index 0000000000000000000000000000000000000000..330a2c8b2d78be35f578dd74a9f410e1b6b6b2f8 GIT binary patch literal 406 zcma)2Jx{|x41EsyfKVt@0wE??PyvaI46V8#m?DN!b-yMSJ(LfWTw3^9Oh^p;0Dct0 z7Z5B6;gf8?XY1L%xA&J<0O#n1SV1?yR)B4SKh%XbBf?%kzEcYoWvaN1CiALXs#(T^ z<0-+tC{reMV_oodlh3%C*czc1mx;=zs?rvJk*=BRn$VB=A>n(Y%c73PY^ujt=h|Ef z$tkBcw#5O#OH@;f+|I8X5W<_XsS>`@cILq!T4(lx4qSMIqyLR3?EjoSZVIDw{@x9T zaOAVBkv!2A?+Y3U?@07dPM<+fk~s*(hb`+#pO00+yPsWHlZrJT>*%yv8-g5cegN9Y BW_hx5DgAV zgW3tsz+ujrnKR7C=VlAw5(hCl*o&|qVL%A4*>XK5bWYC}1b^a|nlQ-N>bqv0>vFDg zBg$ds3S}0mI$8C_LxC(~C(j5na^LNmj2p%HA VP-MtLS1{St0eXmq5&D82k}s7BQOp1U literal 0 HcmV?d00001 diff --git a/distributed-event-bus/target/classes/lib/KeyedExecutor.class b/distributed-event-bus/target/classes/lib/KeyedExecutor.class new file mode 100644 index 0000000000000000000000000000000000000000..aedf9667712d012e2c7587398bb49ab31382072d GIT binary patch literal 3331 zcmbVO-&Y$&6#gaw7QzC7LSqXpZAvi&Y-;^cZBn#FXpul`6R5RTyGe#*$+8?^SG6sg@ZgeRFmH4E+vdhM|mF)D_D%Y0H_I zGuUZCZ-7$1r8|aNa430r?4F&o-nI3-*ij;5S1Ovy&E7YE&JA~r8-{4N`BZk>fw!01 zzTt!I9m=wwip~LC2R#zw1$)|^3t!&19(8YVC?n>5o9^O|VX-H}8co$6X*xdSN%)%5 zl>J;mne$iC+|@=MwW`I5S&OHrf;;6wdulL*GKOvD`I_p8O_PRP5rrrt3{skNT?I|X zD8?jwC}RU@8JpOWa8Jg4d?ew4j100ewm~u%zHJy*)iRaJCbvpPp*l8zhhcn7&7&|r zk?|OxN_ZmUGkh-NDHMkBHf)vc|Mu<3n`s z7P_25Yc&wZR7eb~e(1*+Y!f%_GR-jEIHdaYnkVOU;%b2|aMhywX|{&GnXHIX)dC(@3*P+cjzN}k>#gmrko z(UU+glYz8hl6LM^NjGHowO*|{8RUgsQ8aq)SUf3VpW$*FKb`hdQw+bKCOcfDyd0rdA3aQj$kA6n zeTOmbi~!#wNPobk_-|nG>E99f6~W-mndu)e5&s3jnIrUNz74P+U;gzonf2im{hq}D z{fc(Em6B zFKJ;=g3M^{Z*ol1jq~J0yj;JafC6h|Ng!K%hJMk$s2pM7+7YCmXe#L78*n^N)v?f$ zI)Vsh37?1@A#$SRT3~T;3M+v{%*_&3hB^8U;Iac_-h=T?R~Un>FwXP<<5draAkGEb z48sK*BP$osf&(b-0s77Z6kU3bA)pgEgGUHG!*DCMb3Sa*Iz`bsMNy(?u?d^7a&#F}VKT|SoU_+nYwtOK|NZ_0;01O}m{`jphjkN=@K~FwPHWoO+H7dE znZZ^D+X4yI5-1&>x#zCaalN*46!^+(zY@sxhmDRJoJyauJeOWj|J0YdxZ7*V&cJzl zamM0>mK-#F)elt96R?`Tbahx)T_VJRK>D@vRPaXNWp$oxzT=m$U11GHL{h zSwMkRyH}=8R%@I)&-1aE|c3tFmi#^Nixyp*0yxpND_T~9)dL3z9 zB8;@ML>Osp$uQFDQoO%|m9aL5Ue_D;cNkw8MC5t^u&E}A8X98~fxEHb^eng>3odi6 pXe-mqnOSfp7JMHMCg8+GcJ|AR?Dbf1C02G~mbn%Sei%}${s$CWygdK_ literal 0 HcmV?d00001 diff --git a/distributed-event-bus/target/classes/models/EventType.class b/distributed-event-bus/target/classes/models/EventType.class new file mode 100644 index 0000000000000000000000000000000000000000..4b86a61af2e53a1802a46a0b85755dd972b3944c GIT binary patch literal 962 zcmZuv?{Csj6g`g?TDlHc$EM8rYvKlM4$&DuK$a~F5|U;ZiX_DNpr5GqA{#VKp zXEd69_Kz~&_eM76cun5Tz4zR6&%J$r{{Hw4;3f8=kg=s9gRFw2f}DmJw)wTABCb(# zha;Y-SmJh9g~4rJ!BYnHb-h}vR~sz`!|e6nJKiu~9y@-}`25K!GAL%PQmGzS7{cXx zy;i44x_M~6DO(K7Z|7A+#|B6+L~R)2rt3S$qh8w?H0-uV5k*wiJ}_)% z%oBTT=RMo+=B;4h`rTr7UNgh&cWkd|4_r<$<&k~6M|8$b4u(j{_1$2fVRcT6YPQKB z2OnKhAGO@BZwI3RF-aMoP)i+eTAZGtF8iZi>87@QTApV0M}v;@%H`9=F4wxtXVbCF ze^ydQ0nZo`|L$ofOv>HP3js-~c z0ZMLr{S1+>B0bygs$9VXE*%dAW&8$(+bMn&@mGK-NG$jO zJ_>Qiagam3xpQCVo_l8I_n)7?06fLO#4QWAamU78+_Pb#W5K|EWgZxKXy6e+ljkx@ z35`)WpUNN^?y2}W!Aei#`A2aaNW#|Wy*L-cKt!|QWSQn3{e>U-=}W>+cNJdQf6zN3 z)L$_TVSVIB@=X>V%XqTja7XhK5gduwS8B0R_>1NCg&$w}>ZxD3er# zX9PMRm@}E?Ere#bx5O?%4p;w)AZ&9-wL1H7p2a6}-&a?#QKo*P0GyjjF&zXlO=Udz zkjYFMcXYGUb-AkKAkbWwl^NX9TVIYHLAwX z(5l#W{iB+Eom>Zml-bi@Kd70-^k>TR)ebqvgay|e6E35 zcZ2OZMd#~Y;#8KT?Xs_ MgWXE-jhy1#@}xS>CWa2m{v}?g}&})8mO9 zxW_UOFvfwjea~@2*Y^ZAB8O?{j=CpP&sL}1uL9|~wiQY`*9Y#P6@!;fZ$r{OtfHrNS3>1(>J-ZTT3uy0@yO9n2erHPh-6|4$e*KeNg(j0vTUd2TNuVCH4CyMGm4g_9~ z->h^zfeU}Xq7RNANt?`QxMux zNfflD(kN<6B~a3q;ws~H+N%j_IYF%v6{UTRNnOVct}3Z6zlyzV{({0!Gzwa7MIUZx zNk#;SCGjE7buMO|H*=la=)~nrM=oWZZ{#>f@=dDQ!p$6#a+btAm*f^nwsr2=8q~5R z+c_kBXz&&(l=BxXv1&?_1*|(izO4zP9yOCbH*uT& ZQ@yxD#EZSD|CrE4z1hjt^^Qip`xk0A2gv{c literal 0 HcmV?d00001 diff --git a/distributed-event-bus/target/classes/util/Timer.class b/distributed-event-bus/target/classes/util/Timer.class new file mode 100644 index 0000000000000000000000000000000000000000..40f6d1ad0d4e75931ee5ce652bae910f692249d6 GIT binary patch literal 448 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 aC?E?9?b5zm)^ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/rate-limiter/.idea/jarRepositories.xml b/rate-limiter/.idea/jarRepositories.xml new file mode 100644 index 0000000..712ab9d --- /dev/null +++ b/rate-limiter/.idea/jarRepositories.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/rate-limiter/.idea/libraries/Maven__junit_junit_4_13.xml b/rate-limiter/.idea/libraries/Maven__junit_junit_4_13.xml new file mode 100644 index 0000000..59fc5c4 --- /dev/null +++ b/rate-limiter/.idea/libraries/Maven__junit_junit_4_13.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/rate-limiter/.idea/libraries/Maven__org_hamcrest_hamcrest_core_1_3.xml b/rate-limiter/.idea/libraries/Maven__org_hamcrest_hamcrest_core_1_3.xml new file mode 100644 index 0000000..f58bbc1 --- /dev/null +++ b/rate-limiter/.idea/libraries/Maven__org_hamcrest_hamcrest_core_1_3.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/rate-limiter/.idea/misc.xml b/rate-limiter/.idea/misc.xml new file mode 100644 index 0000000..2e289ef --- /dev/null +++ b/rate-limiter/.idea/misc.xml @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/rate-limiter/.idea/modules.xml b/rate-limiter/.idea/modules.xml new file mode 100644 index 0000000..9fd586a --- /dev/null +++ b/rate-limiter/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/rate-limiter/.idea/uiDesigner.xml b/rate-limiter/.idea/uiDesigner.xml new file mode 100644 index 0000000..e96534f --- /dev/null +++ b/rate-limiter/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/rate-limiter/.idea/vcs.xml b/rate-limiter/.idea/vcs.xml new file mode 100644 index 0000000..6c0b863 --- /dev/null +++ b/rate-limiter/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/rate-limiter/pom.xml b/rate-limiter/pom.xml new file mode 100644 index 0000000..af18a0d --- /dev/null +++ b/rate-limiter/pom.xml @@ -0,0 +1,31 @@ + + + 4.0.0 + + interviewready.io + rate-limiter + 1.0 + + + + org.apache.maven.plugins + maven-compiler-plugin + + 10 + 10 + + + + + + + + junit + junit + 4.13 + test + + + \ No newline at end of file diff --git a/rate-limiter/rate-limiter.iml b/rate-limiter/rate-limiter.iml new file mode 100644 index 0000000..da9b2a5 --- /dev/null +++ b/rate-limiter/rate-limiter.iml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/rate-limiter/src/main/java/TimerWheel.java b/rate-limiter/src/main/java/TimerWheel.java new file mode 100644 index 0000000..735b924 --- /dev/null +++ b/rate-limiter/src/main/java/TimerWheel.java @@ -0,0 +1,77 @@ +import exceptions.RateLimitExceededException; +import models.Request; +import utils.Timer; + +import java.util.Map; +import java.util.concurrent.*; + +public class TimerWheel { + private final int timeOutPeriod; + private final int capacityPerSlot; + private final TimeUnit timeUnit; + private final ArrayBlockingQueue[] slots; + private final Map reverseIndex; + private final Timer timer; + private final ExecutorService[] threads; + + public TimerWheel(final TimeUnit timeUnit, + final int timeOutPeriod, + final int capacityPerSlot, + final Timer timer) { + this.timeUnit = timeUnit; + this.timeOutPeriod = timeOutPeriod; + this.capacityPerSlot = capacityPerSlot; + if (this.timeOutPeriod > 1000) { + throw new IllegalArgumentException(); + } + this.slots = new ArrayBlockingQueue[this.timeOutPeriod]; + this.threads = new ExecutorService[this.timeOutPeriod]; + this.reverseIndex = new ConcurrentHashMap<>(); + for (int i = 0; i < slots.length; i++) { + slots[i] = new ArrayBlockingQueue<>(capacityPerSlot); + threads[i] = Executors.newSingleThreadExecutor(); + } + this.timer = timer; + final long timePerSlot = TimeUnit.MILLISECONDS.convert(1, timeUnit); + Executors.newSingleThreadScheduledExecutor() + .scheduleAtFixedRate(this::flushRequests, + timePerSlot - (this.timer.getCurrentTimeInMillis() % timePerSlot), + timePerSlot, TimeUnit.MILLISECONDS); + } + + public Future flushRequests() { + final int currentSlot = getCurrentSlot(); + return threads[currentSlot].submit(() -> { + for (final Request request : slots[currentSlot]) { + if (timer.getCurrentTime(timeUnit) - request.getStartTime() >= timeOutPeriod) { + slots[currentSlot].remove(request); + reverseIndex.remove(request.getRequestId()); + } + } + }); + } + + public Future addRequest(final Request request) { + final int currentSlot = getCurrentSlot(); + return threads[currentSlot].submit(() -> { + if (slots[currentSlot].size() >= capacityPerSlot) { + throw new RateLimitExceededException(); + } + slots[currentSlot].add(request); + reverseIndex.put(request.getRequestId(), currentSlot); + }); + } + + public Future evict(final String requestId) { + final int currentSlot = reverseIndex.get(requestId); + return threads[currentSlot].submit(() -> { + slots[currentSlot].remove(new Request(requestId, 0)); + reverseIndex.remove(requestId); + }); + } + + private int getCurrentSlot() { + return (int) timer.getCurrentTime(timeUnit) % slots.length; + } +} + diff --git a/rate-limiter/src/main/java/exceptions/RateLimitExceededException.java b/rate-limiter/src/main/java/exceptions/RateLimitExceededException.java new file mode 100644 index 0000000..3c82f38 --- /dev/null +++ b/rate-limiter/src/main/java/exceptions/RateLimitExceededException.java @@ -0,0 +1,7 @@ +package exceptions; + +public class RateLimitExceededException extends IllegalStateException { + public RateLimitExceededException() { + super("Rate limit exceeded"); + } +} diff --git a/rate-limiter/src/main/java/models/Request.java b/rate-limiter/src/main/java/models/Request.java new file mode 100644 index 0000000..467dde9 --- /dev/null +++ b/rate-limiter/src/main/java/models/Request.java @@ -0,0 +1,33 @@ +package models; + +import java.util.Objects; + +public class Request { + private final String requestId; + private final long startTime; + + public Request(String requestId, long startTime) { + this.requestId = requestId; + this.startTime = startTime; + } + + public String getRequestId() { + return requestId; + } + + public long getStartTime() { + return startTime; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + return requestId.equals(((Request) o).requestId); + } + + @Override + public int hashCode() { + return requestId.hashCode(); + } +} diff --git a/rate-limiter/src/main/java/utils/Timer.java b/rate-limiter/src/main/java/utils/Timer.java new file mode 100644 index 0000000..4545d91 --- /dev/null +++ b/rate-limiter/src/main/java/utils/Timer.java @@ -0,0 +1,13 @@ +package utils; + +import java.util.concurrent.TimeUnit; + +public class Timer { + public long getCurrentTime(final TimeUnit timeUnit) { + return timeUnit.convert(getCurrentTimeInMillis(), TimeUnit.MILLISECONDS); + } + + public long getCurrentTimeInMillis() { + return System.currentTimeMillis(); + } +} diff --git a/rate-limiter/src/test/java/RateLimitTest.java b/rate-limiter/src/test/java/RateLimitTest.java new file mode 100644 index 0000000..8cf3098 --- /dev/null +++ b/rate-limiter/src/test/java/RateLimitTest.java @@ -0,0 +1,74 @@ +import models.Request; +import org.junit.Assert; +import org.junit.Test; + +import java.util.concurrent.TimeUnit; + +public class RateLimitTest { + + @Test + public void testDefaultBehaviour() throws Exception { + final TimeUnit timeUnit = TimeUnit.SECONDS; + final TestTimer timer = new TestTimer(); + final TimerWheel timerWheel = new TimerWheel(timeUnit, 6, 3, timer); + timerWheel.addRequest(new Request("1", timer.getCurrentTime(timeUnit))).get(); + timerWheel.addRequest(new Request("2", timer.getCurrentTime(timeUnit))).get(); + timerWheel.addRequest(new Request("3", timer.getCurrentTime(timeUnit))).get(); + Throwable exception = null; + try { + timerWheel.addRequest(new Request("4", timer.getCurrentTime(timeUnit))).get(); + } catch (Exception e) { + exception = e.getCause(); + } + Assert.assertNotNull(exception); + Assert.assertEquals("Rate limit exceeded", exception.getMessage()); + tick(timeUnit, timer, timerWheel); + timerWheel.addRequest(new Request("4", timer.getCurrentTime(timeUnit))).get(); + timerWheel.addRequest(new Request("5", timer.getCurrentTime(timeUnit))).get(); + timerWheel.evict("1").get(); + timerWheel.evict("4").get(); + timerWheel.addRequest(new Request("6", timer.getCurrentTime(timeUnit))).get(); + timerWheel.addRequest(new Request("7", timer.getCurrentTime(timeUnit))).get(); + } + + @Test + public void testClearing() throws Exception { + final TimeUnit timeUnit = TimeUnit.SECONDS; + final TestTimer timer = new TestTimer(); + final int timeOutPeriod = 6; + final TimerWheel timerWheel = new TimerWheel(timeUnit, timeOutPeriod, 3, timer); + timerWheel.addRequest(new Request("0", timer.getCurrentTime(timeUnit))).get(); + timerWheel.addRequest(new Request("1", timer.getCurrentTime(timeUnit))).get(); + timerWheel.addRequest(new Request("2", timer.getCurrentTime(timeUnit))).get(); + + Throwable exception = null; + try { + timerWheel.addRequest(new Request("3", timer.getCurrentTime(timeUnit))).get(); + } catch (Exception e) { + exception = e.getCause(); + } + Assert.assertNotNull(exception); + Assert.assertEquals("Rate limit exceeded", exception.getMessage()); + + for (int i = 0; i < timeOutPeriod; i++) { + tick(timeUnit, timer, timerWheel); + } + timerWheel.addRequest(new Request("4", timer.getCurrentTime(timeUnit))).get(); + timerWheel.addRequest(new Request("5", timer.getCurrentTime(timeUnit))).get(); + timerWheel.addRequest(new Request("6", timer.getCurrentTime(timeUnit))).get(); + + exception = null; + try { + timerWheel.addRequest(new Request("7", timer.getCurrentTime(timeUnit))).get(); + } catch (Exception e) { + exception = e.getCause(); + } + Assert.assertNotNull(exception); + Assert.assertEquals("Rate limit exceeded", exception.getMessage()); + } + + private void tick(TimeUnit timeUnit, TestTimer timer, TimerWheel timerWheel) throws Exception { + timer.setTime(timer.getCurrentTimeInMillis() + TimeUnit.MILLISECONDS.convert(1, timeUnit)); + timerWheel.flushRequests().get(); + } +} diff --git a/rate-limiter/src/test/java/TestTimer.java b/rate-limiter/src/test/java/TestTimer.java new file mode 100644 index 0000000..7c4cacf --- /dev/null +++ b/rate-limiter/src/test/java/TestTimer.java @@ -0,0 +1,14 @@ +import utils.Timer; + +public class TestTimer extends Timer { + private long currentTime = System.currentTimeMillis(); + + @Override + public long getCurrentTimeInMillis() { + return currentTime; + } + + public void setTime(final long currentTime) { + this.currentTime = currentTime; + } +} diff --git a/rate-limiter/target/classes/TimerWheel.class b/rate-limiter/target/classes/TimerWheel.class new file mode 100644 index 0000000000000000000000000000000000000000..af03599862c7f8e4f905c4f7c7accb55fa52e865 GIT binary patch literal 4902 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 literal 0 HcmV?d00001 diff --git a/rate-limiter/target/test-classes/RateLimitTest.class b/rate-limiter/target/test-classes/RateLimitTest.class new file mode 100644 index 0000000000000000000000000000000000000000..1b8b30b52fbb8654f69b0296075d837978a2bd3e GIT binary patch literal 3005 zcmai0OH&+G6#g#EbPq!V!#k5efJ6=N33(6$kw}0fg9Ah$L^M8{nGQ4zGs#R3iEpFP z=)x*DmOHJ=CgqY?HAGY84{)Kb{Q*|F(5+RLmH6H6W*!a+P~G?5bI&>Vob!F>_Wbnl zhaUk9;uAj(V61|(aTODOlw(ZASwFlur{cU1VJVvQL&XbH7^%b*rhS<4;ern@s+jd7 zfH^~(qA8-BiYxm=POQ57*2hJbR) zOqkY)KzV1^f`Df%8Kbl&VkV5~%=(g%n%9@&l+;C%Q9Zt(r%btag&u3gOiS%q-7+HP zx@pZDX=_-Z!Qwb>T+uUe>x{9YZt&pJlEa z7a5B{o1>Dk%y=l8OhhxOl##GP^R5Dwy;7MDDf&+=2ue5>oO(kRLxm>bZtt%YNw~-ii%}=%|*#ngR_~0JY~U5n=~3tB$AdcGvHnz zl1wd!Rx`{YuPBwmnhVRjNY z{~RkEHO`tgSVegB%Yth-h^I8Hpr5tK6^(ZJDoGUvSiMdP#M7Z!<64H%GvWaat5}oU zaSiK8s7PwKifi0D!sjTTV*>R%;Hn{ovzrM`HYn<+v$3-4|i%uQ=d#k z&!m|`bCE`_R8$J&i9ec78!5W0((TD~(wff1*^Z5cskz?m%;B>o=_ZvTL8AQ z<7?ttMvt@44(;Ho;8H1Y?lm?{$$)JH3+uXD&6@yfxO>_rMa3m>}!j?-)SRUbtRrjueccVl(y|+|DU%msJ!hv_#u&h z9thdod_T=qh@;@>?cRdeBZU6Ht`iEhy7{51=mA`FeY@sMvx(i^}hz30!jIRgaNZm&nDC$1$lX*}4mD z$Q$%#Q9I-bdhVmHu0H6=qTxOoyL)>3w$SuAOGn;Z;<2yJm&G18(7KxZ+)gG~s&C8p zmdH0*tQg+mMHFDcWLX!`$af_%(D+}4AgVEd8ic8#qn6tB)Ni6i18!3HQ#9jq?7>&m z{2F_a#XkIu7W{@*{Eq#2h&IuLb{nP zS`Od{ql)1>9K|t4bexg3pzIM3_p0bnF{q+kmGcu2Dgr({OAX1M08VlyDKf-SV0fDl zWjduQMMH&z2q+_koRBEl1>09rFn6PrF78GtUj(6dyp?iB5<>|pk{INNLk!YH5~F-j zX;cEr9n_K{K@T}{OA&V;{pI>9C7HWX$`yB`loa{MN{R7=$To@28oyPy5fEOnUv%WS zlOj4`r9EstjclqR>aE0hkZn1`rd%QJYsC2$k-bfvKO@p#5aTb2?>9vETWW4{+#-&@ z62U((B+76~crhaOW0WVI76`HRguug$t1D32Jjl4Bj|mZ#mns1^U9;cu8vAYqyO zF~HKvD=p5zJRDnkd$VY{hgLstpZ$6kZGBm^J8hl6>8cqRaE{G)p3N1uQ|@-`-K1n& XI1MCcP6NrA(?I#A5E!MWGMxDjwKPZC&D*nf~!!GR3g&H;_CK7Z5VP9~9V@gS;t%(oi!bGES;RE}Iqm%W%OvG(I8w)jz#xYfz{rMz{N0E{`FN$_{FH>^u4@GJ`)dfRq zDzfe$<~q5Naje^jKtL}Q1>SD=G}0f;b2SlHQe)PZPNxsiW5iKL#lt?w0qPtsJci1$ zg~)j(<22OcD2BS0gmIKi!@*OUi5Wv`UigI6sn49?71lEa3j@~_%PV4MDQ=)y ih&zO^NaF@a|KK?dP%{oKa~-#EpnfwF{aS1o;^r@HJXW#* literal 0 HcmV?d00001 diff --git a/service-orchestrator/.idea/.gitignore b/service-orchestrator/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/service-orchestrator/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/service-orchestrator/.idea/compiler.xml b/service-orchestrator/.idea/compiler.xml new file mode 100644 index 0000000..a50050e --- /dev/null +++ b/service-orchestrator/.idea/compiler.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/service-orchestrator/.idea/jarRepositories.xml b/service-orchestrator/.idea/jarRepositories.xml new file mode 100644 index 0000000..712ab9d --- /dev/null +++ b/service-orchestrator/.idea/jarRepositories.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/service-orchestrator/.idea/misc.xml b/service-orchestrator/.idea/misc.xml new file mode 100644 index 0000000..d24ea8e --- /dev/null +++ b/service-orchestrator/.idea/misc.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/service-orchestrator/.idea/uiDesigner.xml b/service-orchestrator/.idea/uiDesigner.xml new file mode 100644 index 0000000..e96534f --- /dev/null +++ b/service-orchestrator/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/service-orchestrator/.idea/vcs.xml b/service-orchestrator/.idea/vcs.xml new file mode 100644 index 0000000..6c0b863 --- /dev/null +++ b/service-orchestrator/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/service-orchestrator/pom.xml b/service-orchestrator/pom.xml new file mode 100644 index 0000000..a139c76 --- /dev/null +++ b/service-orchestrator/pom.xml @@ -0,0 +1,31 @@ + + + 4.0.0 + + org.example + service-orchestrator + 1.0 + + + + org.apache.maven.plugins + maven-compiler-plugin + + 11 + 11 + + + + + + + + junit + junit + 4.13 + test + + + \ No newline at end of file diff --git a/service-orchestrator/service-orchestrator.iml b/service-orchestrator/service-orchestrator.iml new file mode 100644 index 0000000..78b2cc5 --- /dev/null +++ b/service-orchestrator/service-orchestrator.iml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/service-orchestrator/src/main/java/LoadBalancer.java b/service-orchestrator/src/main/java/LoadBalancer.java new file mode 100644 index 0000000..c885ca1 --- /dev/null +++ b/service-orchestrator/src/main/java/LoadBalancer.java @@ -0,0 +1,33 @@ +import models.Node; +import models.Request; +import models.Service; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class LoadBalancer { + private final Map services; + private final Map nodes; + + public LoadBalancer() { + this.services = new ConcurrentHashMap<>(); + this.nodes = new ConcurrentHashMap<>(); + } + + public void register(Service service) { + services.put(service.getId(), service); + } + + public void addNode(String serviceId, Node node) { + nodes.put(node.getId(), node); + services.get(serviceId).getRouter().addNode(node); + } + + public void removeNode(String serviceId, String nodeId) { + services.get(serviceId).getRouter().removeNode(nodes.remove(nodeId)); + } + + public Node getHandler(Request request) { + return services.get(request.getServiceId()).getRouter().getAssignedNode(request); + } +} diff --git a/service-orchestrator/src/main/java/algorithms/ConsistentHashing.java b/service-orchestrator/src/main/java/algorithms/ConsistentHashing.java new file mode 100644 index 0000000..9ddd4c3 --- /dev/null +++ b/service-orchestrator/src/main/java/algorithms/ConsistentHashing.java @@ -0,0 +1,57 @@ +package algorithms; + +import models.Node; +import models.Request; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Function; + +public class ConsistentHashing implements Router { + private final Map> nodePositions; + private final ConcurrentSkipListMap nodeMappings; + private final Function hashFunction; + private final int pointMultiplier; + + + public ConsistentHashing(final Function hashFunction, + final int pointMultiplier) { + if (pointMultiplier == 0) { + throw new IllegalArgumentException(); + } + this.pointMultiplier = pointMultiplier; + this.hashFunction = hashFunction; + this.nodePositions = new ConcurrentHashMap<>(); + this.nodeMappings = new ConcurrentSkipListMap<>(); + } + + public void addNode(Node node) { + nodePositions.put(node, new CopyOnWriteArrayList<>()); + for (int i = 0; i < pointMultiplier; i++) { + for (int j = 0; j < node.getWeight(); j++) { + final var point = hashFunction.apply((i * pointMultiplier + j) + node.getId()); + nodePositions.get(node).add(point); + nodeMappings.put(point, node); + } + } + } + + public void removeNode(Node node) { + for (final Long point : nodePositions.remove(node)) { + nodeMappings.remove(point); + } + } + + public Node getAssignedNode(Request request) { + final var key = hashFunction.apply(request.getId()); + final var entry = nodeMappings.higherEntry(key); + if (entry == null) { + return nodeMappings.firstEntry().getValue(); + } else { + return entry.getValue(); + } + } +} diff --git a/service-orchestrator/src/main/java/algorithms/Router.java b/service-orchestrator/src/main/java/algorithms/Router.java new file mode 100644 index 0000000..7b300a5 --- /dev/null +++ b/service-orchestrator/src/main/java/algorithms/Router.java @@ -0,0 +1,12 @@ +package algorithms; + +import models.Node; +import models.Request; + +public interface Router { + void addNode(Node node); + + void removeNode(Node node); + + Node getAssignedNode(Request request); +} diff --git a/service-orchestrator/src/main/java/algorithms/WeightedRoundRobin.java b/service-orchestrator/src/main/java/algorithms/WeightedRoundRobin.java new file mode 100644 index 0000000..e8919e3 --- /dev/null +++ b/service-orchestrator/src/main/java/algorithms/WeightedRoundRobin.java @@ -0,0 +1,47 @@ +package algorithms; + +import models.Node; +import models.Request; + +import java.util.ArrayList; +import java.util.List; + +public class WeightedRoundRobin implements Router { + private final List nodes; + private int assignTo; + private int currentNodeAssignments; + private final Object lock; + + public WeightedRoundRobin() { + this.nodes = new ArrayList<>(); + this.assignTo = 0; + this.lock = new Object(); + } + + public void addNode(Node node) { + synchronized (this.lock) { + nodes.add(node); + } + } + + public void removeNode(Node node) { + synchronized (this.lock) { + nodes.remove(node); + assignTo--; + currentNodeAssignments = 0; + } + } + + public Node getAssignedNode(Request request) { + synchronized (this.lock) { + assignTo = assignTo % nodes.size(); + final var currentNode = nodes.get(assignTo); + currentNodeAssignments++; + if (currentNodeAssignments == currentNode.getWeight()) { + assignTo++; + currentNodeAssignments = 0; + } + return currentNode; + } + } +} diff --git a/service-orchestrator/src/main/java/models/Node.java b/service-orchestrator/src/main/java/models/Node.java new file mode 100644 index 0000000..8231120 --- /dev/null +++ b/service-orchestrator/src/main/java/models/Node.java @@ -0,0 +1,45 @@ +package models; + +import java.util.Objects; + +public class Node { + private final String id; + private final int weight; + private final String ipAddress; + + public Node(String id, String ipAddress) { + this(id, ipAddress, 1); + } + + public Node(String id, String ipAddress, int weight) { + this.id = id; + this.weight = weight; + this.ipAddress = ipAddress; + } + + public String getId() { + return id; + } + + public int getWeight() { + return weight; + } + + public String getIpAddress() { + return ipAddress; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Node node = (Node) o; + return id.equals(node.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + +} diff --git a/service-orchestrator/src/main/java/models/Request.java b/service-orchestrator/src/main/java/models/Request.java new file mode 100644 index 0000000..c59b1bd --- /dev/null +++ b/service-orchestrator/src/main/java/models/Request.java @@ -0,0 +1,26 @@ +package models; + +public class Request { + private final String id; + + private final String serviceId; + private final String method; + + public Request(String id, String serviceId, String method) { + this.id = id; + this.serviceId = serviceId; + this.method = method; + } + + public String getId() { + return id; + } + + public String getServiceId() { + return serviceId; + } + + public String getMethod() { + return method; + } +} diff --git a/service-orchestrator/src/main/java/models/Service.java b/service-orchestrator/src/main/java/models/Service.java new file mode 100644 index 0000000..f42bef0 --- /dev/null +++ b/service-orchestrator/src/main/java/models/Service.java @@ -0,0 +1,27 @@ +package models; + +import algorithms.Router; + +public class Service { + private final Router router; + private final String id; + private final String[] methods; + + public Service(String id, Router router, String[] methods) { + this.router = router; + this.id = id; + this.methods = methods; + } + + public Router getRouter() { + return router; + } + + public String getId() { + return id; + } + + public String[] getMethods() { + return methods; + } +} diff --git a/service-orchestrator/src/test/java/LBTester.java b/service-orchestrator/src/test/java/LBTester.java new file mode 100644 index 0000000..9cd6da5 --- /dev/null +++ b/service-orchestrator/src/test/java/LBTester.java @@ -0,0 +1,70 @@ +import algorithms.ConsistentHashing; +import algorithms.WeightedRoundRobin; +import models.Node; +import models.Request; +import models.Service; +import org.junit.Assert; +import org.junit.Test; + +public class LBTester { + @Test + public void LBDefaultBehaviour() { + LoadBalancer loadBalancer = new LoadBalancer(); + final var consistentHashing = new ConsistentHashing(point -> (long) Math.abs(point.hashCode()) % 100, 1); + final String profileServiceId = "profile", smsServiceId = "sms", emailServiceId = "email"; + + loadBalancer.register(new Service(profileServiceId, consistentHashing, new String[]{"addProfile", "deleteProfile", "updateProfile"})); + loadBalancer.register(new Service(smsServiceId, new WeightedRoundRobin(), new String[]{"sendSms", "addTemplate", "getSMSForUser"})); + loadBalancer.register(new Service(emailServiceId, new WeightedRoundRobin(), new String[]{"sendEmail", "addTemplate", "getSMSForUser"})); + + final Node pNode1 = new Node("51", "35.45.55.65", 2), pNode2 = new Node("22", "35.45.55.66", 3); + loadBalancer.addNode(profileServiceId, pNode1); + loadBalancer.addNode(profileServiceId, pNode2); + + final Node sNode1 = new Node("13", "35.45.55.67"), sNode2 = new Node("64", "35.45.55.68"); + loadBalancer.addNode(smsServiceId, sNode1); + loadBalancer.addNode(smsServiceId, sNode2); + + final Node eNode1 = new Node("node-35", "35.45.55.69", 2), eNode2 = new Node("node-76", "35.45.55.70"); + loadBalancer.addNode(emailServiceId, eNode1); + loadBalancer.addNode(emailServiceId, eNode2); + + var profileNode1 = loadBalancer.getHandler(new Request("r-123", profileServiceId, "addProfile")); + var profileNode2 = loadBalancer.getHandler(new Request("r-244", profileServiceId, "addProfile")); + var profileNode3 = loadBalancer.getHandler(new Request("r-659", profileServiceId, "addProfile")); + var profileNode4 = loadBalancer.getHandler(new Request("r-73", profileServiceId, "addProfile")); + Assert.assertEquals(pNode1, profileNode1); + Assert.assertEquals(pNode1, profileNode2); + Assert.assertEquals(pNode2, profileNode3); + Assert.assertEquals(pNode1, profileNode4); + + loadBalancer.removeNode(profileServiceId, pNode1.getId()); + + profileNode1 = loadBalancer.getHandler(new Request("r-123", profileServiceId, "addProfile")); + profileNode2 = loadBalancer.getHandler(new Request("r-244", profileServiceId, "addProfile")); + profileNode3 = loadBalancer.getHandler(new Request("r-659", profileServiceId, "addProfile")); + profileNode4 = loadBalancer.getHandler(new Request("r-73", profileServiceId, "addProfile")); + Assert.assertEquals(pNode2, profileNode1); + Assert.assertEquals(pNode2, profileNode2); + Assert.assertEquals(pNode2, profileNode3); + Assert.assertEquals(pNode2, profileNode4); + + final var smsNode1 = loadBalancer.getHandler(new Request("r-124", smsServiceId, "addTemplate")); + final var smsNode2 = loadBalancer.getHandler(new Request("r-1214", smsServiceId, "addTemplate")); + final var smsNode3 = loadBalancer.getHandler(new Request("r-4", smsServiceId, "addTemplate")); + + Assert.assertEquals(sNode1, smsNode1); + Assert.assertEquals(sNode2, smsNode2); + Assert.assertEquals(sNode1, smsNode3); + + final var emailNode1 = loadBalancer.getHandler(new Request("r-1232", emailServiceId, "addTemplate")); + final var emailNode2 = loadBalancer.getHandler(new Request("r-4134", emailServiceId, "addTemplate")); + final var emailNode3 = loadBalancer.getHandler(new Request("r-23432", emailServiceId, "addTemplate")); + final var emailNode4 = loadBalancer.getHandler(new Request("r-5345", emailServiceId, "addTemplate")); + + Assert.assertEquals(eNode1, emailNode1); + Assert.assertEquals(eNode1, emailNode2); + Assert.assertEquals(eNode2, emailNode3); + Assert.assertEquals(eNode1, emailNode4); + } +} diff --git a/service-orchestrator/src/test/java/RouterTester.java b/service-orchestrator/src/test/java/RouterTester.java new file mode 100644 index 0000000..3c6e91e --- /dev/null +++ b/service-orchestrator/src/test/java/RouterTester.java @@ -0,0 +1,140 @@ +import algorithms.ConsistentHashing; +import algorithms.Router; +import algorithms.WeightedRoundRobin; +import models.Node; +import models.Request; +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +public class RouterTester { + String ipAddress = "127.0.0.1", serviceId = "service", method = "method"; + + @Test + public void defaultRoundRobin() { + final Router router = new WeightedRoundRobin(); + final Node node1 = newNode("node-1"), node2 = newNode("node-2"), node3 = newNode("node-3"); + router.addNode(node1); + router.addNode(node2); + router.addNode(node3); + + Assert.assertEquals(node1, router.getAssignedNode(newRequest("r-123"))); + Assert.assertEquals(node2, router.getAssignedNode(newRequest("r-124"))); + Assert.assertEquals(node3, router.getAssignedNode(newRequest("r-125"))); + + router.removeNode(node1); + + Assert.assertEquals(node2, router.getAssignedNode(newRequest("r-125"))); + Assert.assertEquals(node3, router.getAssignedNode(newRequest("r-126"))); + Assert.assertEquals(node2, router.getAssignedNode(newRequest("r-127"))); + Assert.assertEquals(node3, router.getAssignedNode(newRequest("r-128"))); + + final Node node4 = new Node("node-4", ipAddress, 2); + router.addNode(node4); + + Assert.assertEquals(node4, router.getAssignedNode(newRequest("r-129"))); + Assert.assertEquals(node4, router.getAssignedNode(newRequest("r-130"))); + Assert.assertEquals(node2, router.getAssignedNode(newRequest("r-131"))); + } + + @Test + public void defaultConsistentHashing() { + final List hashes = new ArrayList<>(); + hashes.add(1L); + hashes.add(11L); + hashes.add(21L); + hashes.add(31L); + final Function hashFunction = id -> { + if (id.contains("000000")) { + return hashes.remove(0); + } else { + return Long.parseLong(id); + } + }; + final Router router = new ConsistentHashing(hashFunction, 1); + final Node node1 = newNode("1000000"), node2 = newNode("2000000"), node3 = newNode("3000000"); + router.addNode(node1); + router.addNode(node2); + router.addNode(node3); + + Assert.assertEquals(node1, router.getAssignedNode(newRequest("35"))); + Assert.assertEquals(node2, router.getAssignedNode(newRequest("5"))); + Assert.assertEquals(node3, router.getAssignedNode(newRequest("15"))); + + router.removeNode(node1); + + Assert.assertEquals(node2, router.getAssignedNode(newRequest("22"))); + Assert.assertEquals(node3, router.getAssignedNode(newRequest("12"))); + Assert.assertEquals(node2, router.getAssignedNode(newRequest("23"))); + Assert.assertEquals(node3, router.getAssignedNode(newRequest("13"))); + + final Node node4 = newNode("4000000"); + router.addNode(node4); + + Assert.assertEquals(node4, router.getAssignedNode(newRequest("25"))); + } + + @Test(expected = IllegalArgumentException.class) + public void consistentHashingConstruction() { + new ConsistentHashing(Long::valueOf, 0); + } + + @Test + public void consistentHashingWithWeights() { + final List hashes = new ArrayList<>(); + hashes.add(1L); // remaining is node 1 + hashes.add(21L); // 12 to 21 is for node 1 + hashes.add(11L); // 2 to 11 is for node 2 + hashes.add(41L); // 32 to 41 is for node 2 + hashes.add(31L); // 22 to 31 is for node 3 + hashes.add(51L); // 42 to 51 is for node 3 --> 10 points + final Function hashFunction = id -> { + //range should be (0, 60) + if (id.contains("000000")) { + return hashes.remove(0); + } else { + return Long.parseLong(id); + } + }; + final Router router = new ConsistentHashing(hashFunction, 2); + final Node node1 = newNode("1000000"), node2 = newNode("2000000"), node3 = newNode("3000000"); + router.addNode(node1); + router.addNode(node2); + router.addNode(node3); + + Assert.assertEquals(node1, router.getAssignedNode(newRequest("55"))); + Assert.assertEquals(node1, router.getAssignedNode(newRequest("15"))); + Assert.assertEquals(node2, router.getAssignedNode(newRequest("8"))); + Assert.assertEquals(node2, router.getAssignedNode(newRequest("33"))); + Assert.assertEquals(node3, router.getAssignedNode(newRequest("28"))); + Assert.assertEquals(node3, router.getAssignedNode(newRequest("47"))); + + router.removeNode(node1); + // remaining is node 2 + // 12 to 21 is now for node 3 + Assert.assertEquals(node2, router.getAssignedNode(newRequest("58"))); + Assert.assertEquals(node3, router.getAssignedNode(newRequest("12"))); + Assert.assertEquals(node3, router.getAssignedNode(newRequest("23"))); + Assert.assertEquals(node2, router.getAssignedNode(newRequest("54"))); + + final Node node4 = newNode("4000000"); + hashes.add(6L); // 0 to 6 is for node 4, 52 to remaining is for node 4 + hashes.add(26L); // 12 to 26 is for node 4 + router.addNode(node4); + + Assert.assertEquals(node4, router.getAssignedNode(newRequest("15"))); + Assert.assertEquals(node4, router.getAssignedNode(newRequest("59"))); + Assert.assertEquals(node4, router.getAssignedNode(newRequest("5"))); + } + + private Request newRequest(String s) { + return new Request(s, serviceId, method); + } + + private Node newNode(String s) { + return new Node(s, ipAddress); + } +} diff --git a/service-orchestrator/target/classes/LoadBalancer.class b/service-orchestrator/target/classes/LoadBalancer.class new file mode 100644 index 0000000000000000000000000000000000000000..0cef8c9ad0d0b06fc6502161bf40a55ededa3df1 GIT binary patch literal 1704 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 literal 0 HcmV?d00001 diff --git a/service-orchestrator/target/classes/algorithms/Router.class b/service-orchestrator/target/classes/algorithms/Router.class new file mode 100644 index 0000000000000000000000000000000000000000..25a199790b4824fde6711bb1fadaf93d628801f4 GIT binary patch literal 222 zcmY+8u?~Vj5JcwyIRz8-3v8v4(t^sy%4mX#{Rta{02jG~kF)Rt{3zoQh>6Y4ZeC_~ z-k;|Szz%Z`kHcr+B27;+C5GkZSed0%VSh)#mBDMMk{u0vk&)gjm1i}Lj`hzG)1xJ& zqu?*X1YBg>By`A2^T3Z~tEt%*q7w`&QD(Bq^}SNz08E7*g{7#o@O&#M(JljnWf}vv U!(hV!j7~;%!3|-UnrUJ71;r9J`2YX_ literal 0 HcmV?d00001 diff --git a/service-orchestrator/target/classes/algorithms/WeightedRoundRobin.class b/service-orchestrator/target/classes/algorithms/WeightedRoundRobin.class new file mode 100644 index 0000000000000000000000000000000000000000..f6c18e95e9be8e8f6ff5d9520885303a3fccff57 GIT binary patch literal 1600 zcmaJ=U2hvz5IuLj&P|+6+-#}cmZV8(>)P(RE>I{mAtm&SfNMk&rE;Ix*ehpS+gsM_ zBKQmZ3B(JpRBEXL5=G>VzrsI2f^ufp`9?f=X6N3SIcLsX|MAbyzXG_1Pcs<76^%DD zFmPoIS-d6YZH=qq-xtg0L7BZN?2O5hSHx$ynpyl-yj@2HzPu%Jt z@;lX<-;d@M##a4y&y5D5r!afydZE^3U8i5I)6Fk3d(`dsnZFh&sAYv*a}b7JFOvJE z_@>KOv9!+An=Dqy*8HAVA9OdoaLwK5 z@WieKO}De|hQ7qZLo?d+*?+d?cG^McN1NS#^}gq~HzTjL5)67Y8-9-{+HJLj7Kbh# z7$e3MJrpJmTrggZ+~%Wu?&BCt<3oi^=yii9UeYPs_M&7=Z`gZwxA%(o<-nt&r9(Z( z_en%nVUi}R7;3vysIf%2)nE`dy*s`LkUu)uW!ayF(|BFs_}(s;!qEL%7HVM%wuKYO z5h1?cr@e)fIHmECg`2o#;kKAOau)93u7!`mg^urAxP?z}S7GLVl~$PAch3z(UbsUo zhF;qYgRhc>P9B_CVRCN*AH30rjH_i_Hu47|o4C5yi)2)!**(VNVn*tFA(9w5apV6OZEs{9L# z#tWpL!z|OJ8ILel&!L|($G{A24r$sf`y8X4B9e_+qMqTYmGojf$4HUi8?=J+EvrrX znesE}#=YO^%u8DsHE`eA{sE(PXS?$9DU&oC=9!o$N%5%|)Q*dDyv@ghOmh?g%&~$^ zhk){R4(ADZfu|>7ykdGvBdal?MJSEJU$FoecZq#Z1R>@eBsNB3<;F9l&6Ws3H8O{J zofgc5Pcilb&|SC>!v9M%11wF?1YAIsEUvf5>ilF4nal0$xW=p#Z5}$z>R+c zryf9}EkXiu;=qqWytPf5ejGUL?(FQ&ym@ct_n&V+0Nlk&8al3|k-}1nRzYr-+-&J9 z>L{gg70WuxXor?}fT#$hyq$-QM%V52 z_;BBAd%**NrNYU$lOAhBKz-C{xB}U#*LK(Y+na9pnX~EBV^%wL$KP38eyV^8u=Np8zheFNi| zmfH;jqc9ANA!8sbt(&+dFghfLj@t%iaY11GuyIidI_?;pgL?ubMkSIY_Q-c+-zJZU z5jv^rP==`W0uy1(zG0$Ko zMlSP|RcZDYD4%FVbVfqwRG8X`KE$a7&f*+BQjXzZb@y2C%&EcWPes;_BhQ{1%-M;N z-?L8H2+J;hgr@Eme<5S7{BPpv>!cLi?QgiIoJat&$KrV)0 zQOB%-f)*~ai-PY4=OjZ40zG;T~xOi&Xs>1I&9>ynZuTVq3#9T`eQH({4nTs{6-w9 zUgt!>9>{2<+VYYE*{+Nq`not(Jr$n`99REx{)5z-0@iu|UJ4ZJswc09-Ik2*f>ubx zt@qnO*bE}2`xLa|2Q?5V)w^Vc1OHY&4do!_hMA6x^>*cI?XR_f!{^4&A8fW)={NF? z{xE9G3#DD1RPj(B!NEMX9po_Qz}6v;If24&Z*KE*u$$JSY{vp-wbsNgZ=2=SO}JWv zm8)fvOeJOuf))P`44VUZn`5?_&2+Sy1d?QPQ2C2!*FGwZ30_X^{g#l$L`fTcuQFUf&ans=T_HmAwu TndIe}WNVsSnMqzr_FMf1SHyLY literal 0 HcmV?d00001 diff --git a/service-orchestrator/target/classes/models/Service.class b/service-orchestrator/target/classes/models/Service.class new file mode 100644 index 0000000000000000000000000000000000000000..df903f7234d3129514bf33d5b3d2645a5fcaa176 GIT binary patch literal 813 zcmZuu%TC)s6g^`*P7_RkJj%1>(M{sEc*%l_4H6QP0f|)EuyT@7GRTP~$KktLsZk{s zd_X@6amERP!ffu`Id{(EYW_XmKL9wwmoi-Jlu*DYT|VowTf|-w`vPv7Of#7Z)WRqp zB&o`Vqe*a`gU14v>I+oE+vqL|;%Gbw+F7c`15Sz~nGKWvgv=Y$-2I`(DmxK4ZoVHI zybL0(j(~HT^rgT`sK)YgI_k>wXVi^}RKuhf#hoZs`n~|2Y^a!MH5`!@PlC2g?^I7R zZfPL1`BnmrX3G#jpujih+_%oTVdFB#7tc#m!;n~@+)k!xPo62A(OdW*YBdkbIPg%! z2M;BbJuJZ!sQjOl*3eiKhcWiwf%sZ!s() zK;8cf;eVUK^1set``>49eiOH_%KDM2po+sZC8|`YP-9&q>d*P~sqsdo+)w14PGIGo z_O|I+M`Pg}@YJsg{vTMs*vzfm99+w#z4?QMv|x;NB0BbKvi+9aFp@X0X&^gq$(u&< IR-SMB3HacXvH$=8 literal 0 HcmV?d00001 diff --git a/service-orchestrator/target/test-classes/LBTester.class b/service-orchestrator/target/test-classes/LBTester.class new file mode 100644 index 0000000000000000000000000000000000000000..54f716f1c91e9b4c11320ad46f188852f70ff5e3 GIT binary patch literal 3884 zcmb_eTXfu372Q`JkM79wOMbL!^QcoNapKq$dpvexr)gs+CJy7IWLy_h0+l_otwb}L zXf%fKXrZ(P3WXL51quby@+yJ0JnRrR;r%Ktl*cM-@zFn6eDKLy`~l}mnpoqcS$r@b zbI;k@_uO;tc}Oq*`@$svL-><|{aBP?DUif^QX{yiAcoT%E=P&OlTntj#PJS}cXFIj zpx|AC{%#rXQP7MSNjxTUL$?-*wFL8XC zui|SmzAob%4Do$d!SePq)O78bW{8bBIg_C=V-?J!<@~Ja zP8qW{AzH?nGwf-@wS*pmF>lc-F({eL=#*LVOm{>OM-Q6wM%ng8%|&C$a>_1)YCA@5 z)Ub`hoJnd!CKJ(VQ}ditC|P8o;2kzfi&kNQVN=Gi7aZ5}7W1X#*!t(Rsj28X^OkL9 zO?SzfGbeK5@RP=pk)(4Ll3C9sJH%5<`BH?_VCD_WjsWrEQ3|?`p+1u*!7e3*P8)*< z{Unq^Mj!~ptRTKXh#Gnm;!$7*__dNW%@dIGjw)cd9ynr zP{#4Ey0++81&_SSQrhRtamtRk*04HyMRHW!ireT0q7}ff;aa&Se@7k1D|xO_oHV^f zCs%5WHb$Jk)3g>AJu`RQDHrH()+&%oC9{yr3TNu*fGIOy zw8;>iuwZ)G$?Uk}-cb^5kr2E?qB@BgDMbb;P32jaO7{+=d(-LO!L&I3yM*^s2}JNg z72m`63GYi);X{JIO!y!bMufkIoGVbV_oUKQ_}&UWG#JH)s1hpoc-%Z)rploUc6<8z zQ{sRX73c8-AL$>UgXqa%noPyqo}rYC3o2Ief{Kf{q~eFd9e*nkazoeF_lv-7KkIkMm=R5_!!mk;& zMJqXYG7_!i5K?dpUL+5VV$nXsu(>Pbls(H%&X)^wqLY&2p+No95!!tn!i1QvBhjavl+cuB(|}U*#jdCo&bH7$*!{uC^&s ze7@!FS0i0vS@^K`e_~!eK!y6@uu;g_BE-#?Lkug8vJ`cTGQ2j!4wR^YJen$oFC02u zHf(CQD!0SLMvkszpWLpcqs0rPwUX!RSBT=8d1uKV^^}K2dPN2ANqT8cP)n@c#H8UZ zQW+bwbb737V%<|QC8?N~47+R|n-^(35-mM{h|2hlikI+;iaC%&Th?q_g(c^dnauce zn&zM}Z_Ig)OVwGIU(;m#ff^^{IH$@*x{!%N!F0!LaWCZ3OVR9v_JWBT+P*p$(yOdq^I>r0wf9D^C$v0f*mlJ7#dR~<^_pJ0f*Xe8zILOwAucqzTa)$p zGB%1;rqzGCN!z@Fn}+Fp+Ss9O5lW(4<2v`5Tu)S(H*1}~RmG=n)wT(0UALCd6`z{W z>%_W>?K4`JCTr>nb}VD(uo^&g>Sd@tfObWoh5+h`K#h7_SI?uDmU^+sdPAT!(h>%f zT*mJI>ydJBq+E5Rtouj$q4jB8YYjsa$)jldBT#byr6Q1~bG=D;p3s}c!gWmq;BOt! z5|^#D1W-BxwFb~&1ZoSQp$OEjD|(Bttmv&`sngp6t(}&&(!NABFN&I@9_?tL&S;{N zZ-xa8kDwLj&_*8_?RWv#<5lQ*4L9I#*udyJh$XOzHRC4Mi4Hb|E$j$xW+%|ea@fjF zVHl%!fbDb?Z_Zl^Xs$Nq&kktEZ%!(VWmB#u|v%gB^#ktt4p_fLj6LVE_kmUnpFWR$&k+_KVfB#_)&!HH2i?iM&sxCmXjRD0%lJKhOSwKp Nx)^shaq=j@`7Z=6$9w<) literal 0 HcmV?d00001 diff --git a/service-orchestrator/target/test-classes/META-INF/service-orchestrator.kotlin_module b/service-orchestrator/target/test-classes/META-INF/service-orchestrator.kotlin_module new file mode 100644 index 0000000000000000000000000000000000000000..a49347afef10a9b5f95305e1058ba36adec7d6dd GIT binary patch literal 16 RcmZQzU|?ooU|@t|0RRA102TlM literal 0 HcmV?d00001 diff --git a/service-orchestrator/target/test-classes/RouterTester.class b/service-orchestrator/target/test-classes/RouterTester.class new file mode 100644 index 0000000000000000000000000000000000000000..23e90919a0210b29b294f8c7a4037138e9f70b34 GIT binary patch literal 5329 zcmbtX33yaj75?vJCi9p~7A9dC2y3Av6GC>jASF#%5+!-;#@7{avIp>~x z&i~*0-lP9HcOJkh{M(0K9Mf=o8hUV}hS$jUCJlYEz1fS``k=}A*ZDYpy@p#fyrBqh z#H||MBt>tQgSYtbR=iEaZ5rM#$G2;EhlY1*c$XJtthdc~L{uO0AGqRlp0gCVT;-fx<@G%V^*YF7q_h`6R!zVSI(r}-KPigqH zhWj;~)^H{OfwNij>TIVe02l7oa8ARxhRe)Q9U+T*PBk7vB6d< zq>SW<88JEr1PX_Z^iW)O)|oLgy>J3VA_wUTg8Jd>uXk>ZmpO&>7!=$UAm;@Ch>yx)v*p`@iFP#)H!gYl%9 z9vV*7*@iUn#JF&SKyi4OooK4An@v{U=*%}cZL>hIClgDX!^RFXWip_)SS+5_(`G!z zc*?@@y-M0J!EV00%OBp#RSQ^!b`v>$Tmx^Tx(bMes z%GdB>0p+=2^r;bN9XROfq6@G9^W(t1MU{7xCj| z*h%6wG_G1v&;JHLw&NOZ-eIC2J8+F3eNr}0VTGMtgVa&@BMDMV4+n9GptVNih23fL z;;Vjq4PPfglC=$uO@3rB;>S1exEJ5_<6HQ)GehcF8ZCZ&C#SyB5w3FD)dEEd4YR8> z%l&-U(OF}+P0WIH*u?CY=lRo?OzMXu(*5`zp77#HKfaHrNX;wb@ig(MC%UWyC{Ht5V8A9& zSyh%|7F*;oUOH%`+fpfWFlIPP3vxt#B);}Vfm6cfq3*pB1#_CInHw-wK`Exc}HZAyD1cy z26?OhNnk!#r8qVLfo>3EVwD&osdcI3qij3^wB@5>|z@Qzr_NVZ2M&bg1r z)O|Sq>~m_19vg@nsU^0diu%cEn_#$|F9E9hi=VWYytJ*FPRDb*9*y>zX`|JPe+ev| zl%I?3M_~S?^{|Lw>*X780e`&0_=LQ>td`do+i85=dd!fZt_j^NI~oY4I!@Qh;aT2D>TV?`A7(6Uldv;^}M zfpk4z8RwJMjdCU;zz?Ew%tr;5Vg{CDCYmuDSMft(JLbSZC8C@?!Y`a#u@EO%!+UWV z&SMEbAucC}mf;yx;UB2B62IO~u9J8T&*7EOS%6YJi+)5{gqe661MC&h>r?a}VbRW` z6?@q8V2}|NQh&%Qo0%;etLtW*5S8Otz_Kk2*Z9t2(HIuH&{tD|%hbVAb+9ZyIL8># zHIC5E{$N$37-prn;&H3`1?2oAR`uPC?m?^i9glubm&{wy*Wy_c1-mO$4T-W1sz9Qw?LxtoieLhS+m(sQ5SG3sKv=GJm12^GFd4fspN9%;&J8Aj zd%{*E+B+0qK`Btlmxupx0v`Mx(s-4WvzQBc^4Ai2YgzxztowG}3cCq8gP=Ofqvj@< zJlyu;b^`Y!gzhMT_%Pyl5(yq#`|$!&q5vZV5`-iU+C@@#a(gEU9s)1%qQQy9CtdMNq3bsfvz*?z5^wp zf-!7!;Q?HIs^%l z{*UFFxLh-rTlu1vgx5=-+v*ewgEhoWGO5lf>IyPH!~V8^c}y E1sT@l$p8QV literal 0 HcmV?d00001