+ + getName() : String
+ + getMaxConcurrentCalls() : int
+ + getMaxQueueSize() : int
+ + getActiveCalls() : int
+ + getQueuedCalls() : int
+ + getRejectedCalls() : long
+ + shutdown() : void
+ + close() : void
+ }
+ class BulkheadFullException {
+ - bulkheadName : String
+ + BulkheadFullException(bulkheadName : String)
+ + getBulkheadName() : String
+ }
+ class PaymentService {
+ - latency : Duration
+ + PaymentService(latency : Duration)
+ + call(request : String) : String
+ }
+ class InventoryService {
+ + InventoryService()
+ + call(request : String) : String
+ }
+ class App {
+ - PAYMENT_LATENCY : Duration {static}
+ - WAIT_TIMEOUT : Duration {static}
+ + App()
+ + main(args : String[]) : void {static}
+ }
+}
+Bulkhead ..|> AutoCloseable
+BulkheadFullException --|> RuntimeException
+PaymentService ..|> RemoteService
+InventoryService ..|> RemoteService
+Bulkhead ..> BulkheadFullException
+App ..> Bulkhead
+App ..> PaymentService
+App ..> InventoryService
+App ..> BulkheadFullException
+@enduml
diff --git a/microservices-bulkhead/pom.xml b/microservices-bulkhead/pom.xml
new file mode 100644
index 000000000000..a261642c597d
--- /dev/null
+++ b/microservices-bulkhead/pom.xml
@@ -0,0 +1,70 @@
+
+
+
+ 4.0.0
+
+ com.iluwatar
+ java-design-patterns
+ 1.26.0-SNAPSHOT
+
+ microservices-bulkhead
+
+
+ org.slf4j
+ slf4j-api
+
+
+ ch.qos.logback
+ logback-classic
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-assembly-plugin
+
+
+
+
+
+ com.iluwatar.bulkhead.App
+
+
+
+
+
+
+
+
+
diff --git a/microservices-bulkhead/src/main/java/com/iluwatar/bulkhead/App.java b/microservices-bulkhead/src/main/java/com/iluwatar/bulkhead/App.java
new file mode 100644
index 000000000000..3074a2b32370
--- /dev/null
+++ b/microservices-bulkhead/src/main/java/com/iluwatar/bulkhead/App.java
@@ -0,0 +1,134 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka SeppÀlÀ
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.bulkhead;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * The Bulkhead pattern partitions the resources of a service so that a failure or an overload in
+ * one dependency cannot sink the whole service, just like the watertight compartments in a ship's
+ * hull keep a single leak from flooding the entire vessel.
+ *
+ * In this example an order service calls two downstream dependencies: a payment provider that
+ * has become very slow and an inventory system that is perfectly healthy. The demo first sends both
+ * kinds of calls through one shared thread pool. The slow payment calls fill every thread and the
+ * queue, so a request for the healthy inventory system is rejected although nothing is wrong with
+ * it. The demo then gives each dependency its own {@link Bulkhead}. The payment compartment still
+ * saturates and rejects the excess calls fast, but the inventory compartment keeps answering
+ * immediately because the payment calls can no longer consume its threads.
+ */
+@Slf4j
+public class App {
+
+ private static final Duration PAYMENT_LATENCY = Duration.ofMillis(300);
+ private static final Duration WAIT_TIMEOUT = Duration.ofSeconds(5);
+
+ /**
+ * Program entry point.
+ *
+ * @param args command line arguments, not used
+ */
+ public static void main(String[] args) {
+ var payment = new PaymentService(PAYMENT_LATENCY);
+ var inventory = new InventoryService();
+
+ LOGGER.info("--- Scenario 1: one shared thread pool for every downstream call ---");
+ try (var sharedPool = new Bulkhead("shared-pool", 2, 2)) {
+ var paymentFutures = flood(sharedPool, payment, "order", 4);
+ callInventory(sharedPool, inventory, "order-5");
+ awaitAll(paymentFutures);
+ }
+
+ LOGGER.info("--- Scenario 2: a dedicated bulkhead for each downstream dependency ---");
+ try (var paymentBulkhead = new Bulkhead("payment", 2, 2);
+ var inventoryBulkhead = new Bulkhead("inventory", 2, 2)) {
+ var paymentFutures = flood(paymentBulkhead, payment, "order", 10);
+ for (var i = 1; i <= 3; i++) {
+ callInventory(inventoryBulkhead, inventory, "order-" + i);
+ }
+ awaitAll(paymentFutures);
+ LOGGER.info(
+ "Bulkhead '{}' rejected {} of 10 calls, bulkhead '{}' rejected {} of 3 calls",
+ paymentBulkhead.getName(),
+ paymentBulkhead.getRejectedCalls(),
+ inventoryBulkhead.getName(),
+ inventoryBulkhead.getRejectedCalls());
+ }
+ }
+
+ static List> flood(
+ Bulkhead bulkhead, RemoteService service, String requestPrefix, int calls) {
+ var accepted = new ArrayList>();
+ for (var i = 1; i <= calls; i++) {
+ var request = requestPrefix + "-" + i;
+ try {
+ accepted.add(bulkhead.submit(() -> service.call(request)));
+ } catch (BulkheadFullException e) {
+ LOGGER.info("Request '{}' rejected immediately: {}", request, e.getMessage());
+ }
+ }
+ return accepted;
+ }
+
+ static void callInventory(Bulkhead bulkhead, RemoteService inventory, String request) {
+ try {
+ var response = bulkhead.submit(() -> inventory.call(request));
+ LOGGER.info(
+ "Inventory response: {}", response.get(WAIT_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS));
+ } catch (BulkheadFullException e) {
+ LOGGER.error(
+ "Inventory check for '{}' rejected although the inventory system is healthy: {}",
+ request,
+ e.getMessage());
+ } catch (ExecutionException | TimeoutException e) {
+ LOGGER.error("Inventory check for '{}' failed", request, e);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ LOGGER.warn(
+ "Inventory check for '{}' was interrupted while waiting for the response", request);
+ }
+ }
+
+ static void awaitAll(List> futures) {
+ for (var future : futures) {
+ try {
+ LOGGER.info(
+ "Payment response: {}", future.get(WAIT_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS));
+ } catch (ExecutionException | TimeoutException e) {
+ LOGGER.error("Payment call failed", e);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return;
+ }
+ }
+ }
+}
diff --git a/microservices-bulkhead/src/main/java/com/iluwatar/bulkhead/Bulkhead.java b/microservices-bulkhead/src/main/java/com/iluwatar/bulkhead/Bulkhead.java
new file mode 100644
index 000000000000..9b04849c053f
--- /dev/null
+++ b/microservices-bulkhead/src/main/java/com/iluwatar/bulkhead/Bulkhead.java
@@ -0,0 +1,157 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka SeppÀlÀ
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.bulkhead;
+
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Future;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.SynchronousQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * Isolates the calls made to one downstream dependency inside a dedicated, bounded thread pool.
+ *
+ * The pool has a fixed number of worker threads and a bounded waiting queue. When both are full
+ * the bulkhead does not block the caller and it cannot borrow threads from anywhere else: the call
+ * fails fast with a {@link BulkheadFullException}. Each dependency gets its own instance, so a slow
+ * or hanging dependency can only exhaust its own compartment while the rest of the system keeps
+ * serving requests.
+ */
+@Slf4j
+public class Bulkhead implements AutoCloseable {
+
+ @Getter private final String name;
+ @Getter private final int maxConcurrentCalls;
+ @Getter private final int maxQueueSize;
+ private final ThreadPoolExecutor executor;
+ private final AtomicLong rejectedCalls = new AtomicLong();
+
+ /**
+ * Creates a bulkhead with its own thread pool.
+ *
+ * @param name name of the compartment, used in logs and worker thread names
+ * @param maxConcurrentCalls number of calls that may run at the same time
+ * @param maxQueueSize number of calls that may wait for a free thread; zero disables queueing
+ */
+ public Bulkhead(String name, int maxConcurrentCalls, int maxQueueSize) {
+ if (maxConcurrentCalls < 1) {
+ throw new IllegalArgumentException("maxConcurrentCalls must be at least 1");
+ }
+ if (maxQueueSize < 0) {
+ throw new IllegalArgumentException("maxQueueSize must not be negative");
+ }
+ this.name = name;
+ this.maxConcurrentCalls = maxConcurrentCalls;
+ this.maxQueueSize = maxQueueSize;
+ BlockingQueue queue =
+ maxQueueSize == 0 ? new SynchronousQueue<>() : new ArrayBlockingQueue<>(maxQueueSize);
+ var threadCounter = new AtomicInteger();
+ this.executor =
+ new ThreadPoolExecutor(
+ maxConcurrentCalls,
+ maxConcurrentCalls,
+ 0L,
+ TimeUnit.MILLISECONDS,
+ queue,
+ runnable ->
+ new Thread(runnable, "bulkhead-" + name + "-" + threadCounter.incrementAndGet()),
+ new ThreadPoolExecutor.AbortPolicy());
+ }
+
+ /**
+ * Submits a call to this compartment.
+ *
+ * @param task the call to execute
+ * @param type of the result
+ * @return a future that completes with the result of the call
+ * @throws BulkheadFullException if every thread is busy and the queue is full
+ * @throws IllegalStateException if the bulkhead has been shut down
+ */
+ public Future submit(Callable task) {
+ if (executor.isShutdown()) {
+ throw new IllegalStateException("Bulkhead '" + name + "' is shut down");
+ }
+ try {
+ var future = executor.submit(task);
+ LOGGER.debug(
+ "Bulkhead '{}' accepted call ({} active, {} queued)",
+ name,
+ executor.getActiveCount(),
+ executor.getQueue().size());
+ return future;
+ } catch (RejectedExecutionException e) {
+ if (executor.isShutdown()) {
+ throw new IllegalStateException("Bulkhead '" + name + "' is shut down");
+ }
+ rejectedCalls.incrementAndGet();
+ LOGGER.warn(
+ "Bulkhead '{}' is full ({} active, {} queued), rejecting call",
+ name,
+ executor.getActiveCount(),
+ executor.getQueue().size());
+ throw new BulkheadFullException(name);
+ }
+ }
+
+ /** Number of calls currently running. */
+ public int getActiveCalls() {
+ return executor.getActiveCount();
+ }
+
+ /** Number of calls waiting for a free thread. */
+ public int getQueuedCalls() {
+ return executor.getQueue().size();
+ }
+
+ /** Number of calls rejected since the bulkhead was created. */
+ public long getRejectedCalls() {
+ return rejectedCalls.get();
+ }
+
+ /**
+ * Stops the compartment, interrupting calls that are still running and cancelling the calls that
+ * were still waiting in the queue, so that callers blocked on their future are released instead
+ * of waiting for a result that will never be produced.
+ */
+ public void shutdown() {
+ for (var pending : executor.shutdownNow()) {
+ if (pending instanceof Future> future) {
+ future.cancel(false);
+ }
+ }
+ }
+
+ @Override
+ public void close() {
+ shutdown();
+ }
+}
diff --git a/microservices-bulkhead/src/main/java/com/iluwatar/bulkhead/BulkheadFullException.java b/microservices-bulkhead/src/main/java/com/iluwatar/bulkhead/BulkheadFullException.java
new file mode 100644
index 000000000000..de4c0bb516bb
--- /dev/null
+++ b/microservices-bulkhead/src/main/java/com/iluwatar/bulkhead/BulkheadFullException.java
@@ -0,0 +1,46 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka SeppÀlÀ
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.bulkhead;
+
+/**
+ * Thrown by a {@link Bulkhead} when a call cannot be accepted because every worker thread is busy
+ * and the waiting queue is full. The caller receives this exception immediately instead of
+ * blocking, which is the fail-fast behaviour the pattern relies on: the caller can degrade
+ * gracefully while the overloaded dependency keeps consuming only the capacity reserved for it.
+ */
+public class BulkheadFullException extends RuntimeException {
+
+ private final String bulkheadName;
+
+ public BulkheadFullException(String bulkheadName) {
+ super("Bulkhead '" + bulkheadName + "' is full, call rejected");
+ this.bulkheadName = bulkheadName;
+ }
+
+ /** Name of the bulkhead that rejected the call. */
+ public String getBulkheadName() {
+ return bulkheadName;
+ }
+}
diff --git a/microservices-bulkhead/src/main/java/com/iluwatar/bulkhead/InventoryService.java b/microservices-bulkhead/src/main/java/com/iluwatar/bulkhead/InventoryService.java
new file mode 100644
index 000000000000..a1648738a613
--- /dev/null
+++ b/microservices-bulkhead/src/main/java/com/iluwatar/bulkhead/InventoryService.java
@@ -0,0 +1,41 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka SeppÀlÀ
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.bulkhead;
+
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * Simulates a healthy inventory system that answers immediately. It represents the dependency that
+ * should keep working even while another dependency is overloaded.
+ */
+@Slf4j
+public class InventoryService implements RemoteService {
+
+ @Override
+ public String call(String request) {
+ LOGGER.info("Inventory system received '{}'", request);
+ return "Inventory reserved for " + request;
+ }
+}
diff --git a/microservices-bulkhead/src/main/java/com/iluwatar/bulkhead/PaymentService.java b/microservices-bulkhead/src/main/java/com/iluwatar/bulkhead/PaymentService.java
new file mode 100644
index 000000000000..e4c1599a4a3d
--- /dev/null
+++ b/microservices-bulkhead/src/main/java/com/iluwatar/bulkhead/PaymentService.java
@@ -0,0 +1,60 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka SeppÀlÀ
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.bulkhead;
+
+import java.time.Duration;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * Simulates a payment provider that has become slow. Every call takes the configured latency, so a
+ * burst of payment requests keeps the threads that serve it busy for a long time. Without a
+ * bulkhead these calls would also occupy the threads needed by healthy dependencies.
+ */
+@Slf4j
+public class PaymentService implements RemoteService {
+
+ private final Duration latency;
+
+ /**
+ * Creates a payment service.
+ *
+ * @param latency time every call takes to complete
+ */
+ public PaymentService(Duration latency) {
+ this.latency = latency;
+ }
+
+ @Override
+ public String call(String request) {
+ LOGGER.info("Payment provider received '{}', it will take {} ms", request, latency.toMillis());
+ try {
+ Thread.sleep(latency);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException("Payment for '" + request + "' was interrupted", e);
+ }
+ return "Payment approved for " + request;
+ }
+}
diff --git a/microservices-bulkhead/src/main/java/com/iluwatar/bulkhead/RemoteService.java b/microservices-bulkhead/src/main/java/com/iluwatar/bulkhead/RemoteService.java
new file mode 100644
index 000000000000..be4f21941415
--- /dev/null
+++ b/microservices-bulkhead/src/main/java/com/iluwatar/bulkhead/RemoteService.java
@@ -0,0 +1,42 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka SeppÀlÀ
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.bulkhead;
+
+/**
+ * A remote dependency that a service calls over the network, such as a payment provider or an
+ * inventory system. Every call is routed through a {@link Bulkhead} so that the caller never lets
+ * one dependency monopolise its threads.
+ */
+@FunctionalInterface
+public interface RemoteService {
+
+ /**
+ * Performs the remote call.
+ *
+ * @param request identifier of the request, used in the response and in the logs
+ * @return the response of the dependency
+ */
+ String call(String request);
+}
diff --git a/microservices-bulkhead/src/test/java/com/iluwatar/bulkhead/AppTest.java b/microservices-bulkhead/src/test/java/com/iluwatar/bulkhead/AppTest.java
new file mode 100644
index 000000000000..35c85bf9c77d
--- /dev/null
+++ b/microservices-bulkhead/src/test/java/com/iluwatar/bulkhead/AppTest.java
@@ -0,0 +1,151 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka SeppÀlÀ
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.bulkhead;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+class AppTest {
+
+ private static final long TIMEOUT_SECONDS = 5;
+
+ @AfterEach
+ void clearInterruptFlag() {
+ Thread.interrupted();
+ }
+
+ @Test
+ void shouldLaunchApp() {
+ assertDoesNotThrow(() -> App.main(new String[] {}));
+ }
+
+ @Test
+ void shouldBeInstantiable() {
+ assertNotNull(new App(), "App should be instantiable");
+ }
+
+ @Test
+ void callInventoryShouldReportFailureOfTheRemoteCall() {
+ try (var bulkhead = new Bulkhead("inventory", 1, 1)) {
+ RemoteService failing =
+ request -> {
+ throw new IllegalStateException("inventory system down");
+ };
+
+ assertDoesNotThrow(() -> App.callInventory(bulkhead, failing, "order-1"));
+ }
+ }
+
+ @Test
+ void callInventoryShouldReportRejectionWhenBulkheadIsFull() throws Exception {
+ var started = new CountDownLatch(1);
+ var gate = new CountDownLatch(1);
+ try (var bulkhead = new Bulkhead("inventory", 1, 0)) {
+ var blocking = bulkhead.submit(blockOn(started, gate));
+ assertTrue(started.await(TIMEOUT_SECONDS, TimeUnit.SECONDS));
+
+ assertDoesNotThrow(() -> App.callInventory(bulkhead, new InventoryService(), "order-1"));
+
+ gate.countDown();
+ blocking.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ }
+ }
+
+ @Test
+ void callInventoryShouldKeepInterruptFlagWhenWaitingIsInterrupted() throws Exception {
+ var started = new CountDownLatch(1);
+ var gate = new CountDownLatch(1);
+ try (var bulkhead = new Bulkhead("inventory", 1, 1)) {
+ RemoteService slow =
+ request -> {
+ started.countDown();
+ awaitQuietly(gate);
+ return "late";
+ };
+ Thread.currentThread().interrupt();
+
+ assertDoesNotThrow(() -> App.callInventory(bulkhead, slow, "order-1"));
+
+ assertTrue(Thread.interrupted());
+ gate.countDown();
+ assertTrue(started.await(TIMEOUT_SECONDS, TimeUnit.SECONDS));
+ }
+ }
+
+ @Test
+ void awaitAllShouldReportFailedCalls() {
+ try (var bulkhead = new Bulkhead("payment", 1, 1)) {
+ Future failed =
+ bulkhead.submit(
+ () -> {
+ throw new IllegalStateException("payment provider down");
+ });
+
+ assertDoesNotThrow(() -> App.awaitAll(List.of(failed)));
+ }
+ }
+
+ @Test
+ void awaitAllShouldStopAndKeepInterruptFlagWhenWaitingIsInterrupted() throws Exception {
+ var started = new CountDownLatch(1);
+ var gate = new CountDownLatch(1);
+ try (var bulkhead = new Bulkhead("payment", 1, 1)) {
+ var blocking = bulkhead.submit(blockOn(started, gate));
+ assertTrue(started.await(TIMEOUT_SECONDS, TimeUnit.SECONDS));
+ Thread.currentThread().interrupt();
+
+ assertDoesNotThrow(() -> App.awaitAll(List.of(blocking)));
+
+ assertTrue(Thread.interrupted());
+ gate.countDown();
+ blocking.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ }
+ }
+
+ private static Callable blockOn(CountDownLatch started, CountDownLatch gate) {
+ return () -> {
+ started.countDown();
+ awaitQuietly(gate);
+ return "done";
+ };
+ }
+
+ private static void awaitQuietly(CountDownLatch gate) {
+ try {
+ gate.await(TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+}
diff --git a/microservices-bulkhead/src/test/java/com/iluwatar/bulkhead/BulkheadTest.java b/microservices-bulkhead/src/test/java/com/iluwatar/bulkhead/BulkheadTest.java
new file mode 100644
index 000000000000..33df93981fae
--- /dev/null
+++ b/microservices-bulkhead/src/test/java/com/iluwatar/bulkhead/BulkheadTest.java
@@ -0,0 +1,175 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka SeppÀlÀ
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.bulkhead;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import org.junit.jupiter.api.Test;
+
+class BulkheadTest {
+
+ private static final long TIMEOUT_SECONDS = 5;
+
+ @Test
+ void shouldExecuteCallWhenCapacityIsAvailable() throws Exception {
+ try (var bulkhead = new Bulkhead("test", 1, 1)) {
+ var future = bulkhead.submit(() -> "ok");
+
+ assertEquals("ok", future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS));
+ assertEquals(0, bulkhead.getRejectedCalls());
+ }
+ }
+
+ @Test
+ void shouldRejectCallWhenThreadsAndQueueAreFull() throws Exception {
+ var started = new CountDownLatch(1);
+ var gate = new CountDownLatch(1);
+ try (var bulkhead = new Bulkhead("payment", 1, 1)) {
+ var running = bulkhead.submit(blockOn(started, gate));
+ assertTrue(started.await(TIMEOUT_SECONDS, TimeUnit.SECONDS));
+ var queued = bulkhead.submit(() -> "queued");
+
+ var exception =
+ assertThrows(BulkheadFullException.class, () -> bulkhead.submit(() -> "rejected"));
+
+ assertEquals("payment", exception.getBulkheadName());
+ assertEquals("Bulkhead 'payment' is full, call rejected", exception.getMessage());
+ assertEquals(1, bulkhead.getActiveCalls());
+ assertEquals(1, bulkhead.getQueuedCalls());
+ assertEquals(1, bulkhead.getRejectedCalls());
+
+ gate.countDown();
+ assertEquals("running", running.get(TIMEOUT_SECONDS, TimeUnit.SECONDS));
+ assertEquals("queued", queued.get(TIMEOUT_SECONDS, TimeUnit.SECONDS));
+ }
+ }
+
+ @Test
+ void shouldAcceptCallsAgainAfterCapacityIsReleased() throws Exception {
+ var started = new CountDownLatch(1);
+ var gate = new CountDownLatch(1);
+ try (var bulkhead = new Bulkhead("payment", 1, 1)) {
+ var running = bulkhead.submit(blockOn(started, gate));
+ assertTrue(started.await(TIMEOUT_SECONDS, TimeUnit.SECONDS));
+ var queued = bulkhead.submit(() -> "queued");
+ assertThrows(BulkheadFullException.class, () -> bulkhead.submit(() -> "rejected"));
+
+ gate.countDown();
+ assertEquals("running", running.get(TIMEOUT_SECONDS, TimeUnit.SECONDS));
+ assertEquals("queued", queued.get(TIMEOUT_SECONDS, TimeUnit.SECONDS));
+
+ // The queue is empty again, so the next call is accepted even if the worker thread has not
+ // yet returned to polling the queue. A bulkhead without a queue would race here, because the
+ // hand-off to the single thread only succeeds once that thread is idle.
+ var afterRelease = bulkhead.submit(() -> "accepted");
+ assertEquals("accepted", afterRelease.get(TIMEOUT_SECONDS, TimeUnit.SECONDS));
+ assertEquals(1, bulkhead.getRejectedCalls());
+ }
+ }
+
+ @Test
+ void shouldPropagateFailureOfTheCallThroughTheFuture() {
+ try (var bulkhead = new Bulkhead("test", 1, 1)) {
+ var future =
+ bulkhead.submit(
+ () -> {
+ throw new IllegalStateException("downstream failure");
+ });
+
+ var exception =
+ assertThrows(
+ ExecutionException.class, () -> future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS));
+
+ assertInstanceOf(IllegalStateException.class, exception.getCause());
+ assertEquals(0, bulkhead.getRejectedCalls());
+ }
+ }
+
+ @Test
+ void shouldRunCallsOnThreadsNamedAfterTheBulkhead() throws Exception {
+ try (var bulkhead = new Bulkhead("payment", 1, 1)) {
+ var threadName = bulkhead.submit(() -> Thread.currentThread().getName());
+
+ assertTrue(threadName.get(TIMEOUT_SECONDS, TimeUnit.SECONDS).startsWith("bulkhead-payment-"));
+ }
+ }
+
+ @Test
+ void shouldRejectSubmissionAfterShutdown() {
+ var bulkhead = new Bulkhead("test", 1, 1);
+ bulkhead.shutdown();
+
+ assertThrows(IllegalStateException.class, () -> bulkhead.submit(() -> "late"));
+ }
+
+ @Test
+ void shouldCancelQueuedCallsOnShutdown() throws Exception {
+ var started = new CountDownLatch(1);
+ var gate = new CountDownLatch(1);
+ Future queued;
+ try (var bulkhead = new Bulkhead("payment", 1, 1)) {
+ bulkhead.submit(blockOn(started, gate));
+ assertTrue(started.await(TIMEOUT_SECONDS, TimeUnit.SECONDS));
+ queued = bulkhead.submit(() -> "queued");
+ }
+
+ // Without the cancellation the queued task would simply be dropped and this call would block
+ // until the timeout expires, because nobody ever completes its future.
+ assertTrue(queued.isCancelled());
+ assertThrows(CancellationException.class, () -> queued.get(1, TimeUnit.SECONDS));
+ }
+
+ @Test
+ void shouldRejectInvalidConfiguration() {
+ assertThrows(IllegalArgumentException.class, () -> new Bulkhead("test", 0, 1));
+ assertThrows(IllegalArgumentException.class, () -> new Bulkhead("test", 1, -1));
+ }
+
+ @Test
+ void shouldExposeConfiguration() {
+ try (var bulkhead = new Bulkhead("inventory", 3, 4)) {
+ assertEquals("inventory", bulkhead.getName());
+ assertEquals(3, bulkhead.getMaxConcurrentCalls());
+ assertEquals(4, bulkhead.getMaxQueueSize());
+ }
+ }
+
+ private static Callable blockOn(CountDownLatch started, CountDownLatch gate) {
+ return () -> {
+ started.countDown();
+ gate.await(TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ return "running";
+ };
+ }
+}
diff --git a/microservices-bulkhead/src/test/java/com/iluwatar/bulkhead/InventoryServiceTest.java b/microservices-bulkhead/src/test/java/com/iluwatar/bulkhead/InventoryServiceTest.java
new file mode 100644
index 000000000000..96d4e2a38732
--- /dev/null
+++ b/microservices-bulkhead/src/test/java/com/iluwatar/bulkhead/InventoryServiceTest.java
@@ -0,0 +1,39 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka SeppÀlÀ
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.bulkhead;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+
+class InventoryServiceTest {
+
+ @Test
+ void shouldReserveInventoryImmediately() {
+ var service = new InventoryService();
+
+ assertEquals("Inventory reserved for order-1", service.call("order-1"));
+ }
+}
diff --git a/microservices-bulkhead/src/test/java/com/iluwatar/bulkhead/PaymentServiceTest.java b/microservices-bulkhead/src/test/java/com/iluwatar/bulkhead/PaymentServiceTest.java
new file mode 100644
index 000000000000..5ff96742c0a7
--- /dev/null
+++ b/microservices-bulkhead/src/test/java/com/iluwatar/bulkhead/PaymentServiceTest.java
@@ -0,0 +1,58 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka SeppÀlÀ
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.bulkhead;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.time.Duration;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+class PaymentServiceTest {
+
+ @AfterEach
+ void clearInterruptFlag() {
+ Thread.interrupted();
+ }
+
+ @Test
+ void shouldApprovePaymentAfterLatency() {
+ var service = new PaymentService(Duration.ofMillis(10));
+
+ assertEquals("Payment approved for order-1", service.call("order-1"));
+ }
+
+ @Test
+ void shouldFailAndKeepInterruptFlagWhenInterrupted() {
+ var service = new PaymentService(Duration.ofSeconds(10));
+ Thread.currentThread().interrupt();
+
+ assertThrows(IllegalStateException.class, () -> service.call("order-1"));
+
+ assertTrue(Thread.currentThread().isInterrupted());
+ }
+}
diff --git a/microservices-load-shedding/README.md b/microservices-load-shedding/README.md
new file mode 100644
index 000000000000..ae2ef09e1ad2
--- /dev/null
+++ b/microservices-load-shedding/README.md
@@ -0,0 +1,237 @@
+---
+title: "Load Shedding Pattern in Java: Protecting Microservices from Overload"
+shortTitle: Load Shedding
+description: "Learn the Load Shedding pattern in Java: reject excess requests at the door, keep critical traffic flowing and stop overload from turning into an outage. Includes a runnable example, class diagram and trade-offs."
+category: Resilience
+language: en
+tag:
+ - Fault tolerance
+ - Microservices
+ - Performance
+ - Resource management
+ - Scalability
+---
+
+## Also known as
+
+* Overload protection
+* Admission control
+* Graceful degradation under load
+
+## Intent of Load Shedding Design Pattern
+
+Keep a service responsive when it receives more work than it can handle by measuring its own load and rejecting excess requests immediately, before they consume threads, memory or connections. The requests that are accepted are served with normal latency, the rest fail fast so callers can retry or degrade gracefully.
+
+## Detailed Explanation of Load Shedding Pattern with Real-World Examples
+
+Real-world example
+
+> A power grid that is asked for more electricity than it can generate does not try to serve everybody a little worse. It disconnects selected neighbourhoods in a controlled way, keeps hospitals and traffic lights powered, and reconnects the rest when generation catches up. Without this deliberate "load shedding" the frequency would drop and the whole grid would collapse.
+
+In plain words
+
+> When a service is at capacity, say no to new requests right away instead of letting them queue up and slow everything down. Drop the least important work first.
+
+Google's Site Reliability Engineering book says
+
+> A server that is overloaded should degrade gracefully... it is better to reject some requests quickly than to accept all of them and serve every request slowly or not at all.
+
+Flowchart
+
+```mermaid
+flowchart TD
+ A[Incoming request] --> B{In-flight below limit
for this priority?}
+ B -- yes --> C[Admit: in-flight + 1]
+ C --> D[Process request]
+ D --> E[Release: in-flight - 1]
+ E --> F[ACCEPTED response]
+ B -- no --> G[Count as shed]
+ G --> H[REJECTED response
fail fast, retry later]
+```
+
+
+
+## Programmatic Example of Load Shedding Pattern in Java
+
+Our example is an order service that can process five requests at the same time. When a slow payment provider makes orders pile up inside the service, new requests are shed according to their priority: best-effort work is dropped first, regular traffic is dropped when the service is almost full, and one slot is always kept free for critical requests such as checkout.
+
+Every request carries a `Priority`. It is the only piece of information the shedder needs.
+
+```java
+public enum Priority {
+ CRITICAL,
+ NORMAL,
+ LOW
+}
+
+public record Request(String id, Priority priority, String description) {}
+```
+
+The `LoadShedder` is the admission controller. It knows the hard capacity of the service and a limit per priority: low priority requests are shed early, normal requests may not touch the reserve kept for critical ones, and critical requests may use the whole capacity. The admission check is lock-free: the in-flight count is updated with an atomic accumulator whose function refuses to increment past the limit, so concurrent callers can never push the in-flight count above the capacity. A request that cannot be admitted gets a `LoadShedException` immediately instead of a place in a queue.
+
+```java
+public class LoadShedder {
+
+ private final int maxInFlight;
+ private final Map limits = new EnumMap<>(Priority.class);
+ private final AtomicInteger inFlight = new AtomicInteger();
+ private final LongAdder accepted = new LongAdder();
+ private final Map shed = new EnumMap<>(Priority.class);
+
+ public LoadShedder(int maxInFlight, int lowPriorityLimit, int criticalReserve) {
+ // validation omitted
+ this.maxInFlight = maxInFlight;
+ limits.put(Priority.CRITICAL, maxInFlight);
+ limits.put(Priority.NORMAL, maxInFlight - criticalReserve);
+ limits.put(Priority.LOW, lowPriorityLimit);
+ for (var priority : Priority.values()) {
+ shed.put(priority, new LongAdder());
+ }
+ }
+
+ public int acquire(Request request) {
+ var priority = request.priority();
+ var limit = limits.get(priority);
+ // The accumulator is a pure function, so it is safe for the atomic to re-apply it under
+ // contention: the count is only incremented while it is below the limit for this priority.
+ var previous =
+ inFlight.getAndAccumulate(
+ 1, (current, increment) -> current >= limit ? current : current + increment);
+ if (previous >= limit) {
+ // Fail fast: the caller gets an immediate rejection instead of waiting in a queue.
+ shed.get(priority).increment();
+ throw new LoadShedException(request, previous, limit);
+ }
+ accepted.increment();
+ return previous + 1;
+ }
+
+ public void release() {
+ // Clamped at zero so that an unmatched release cannot hand out capacity the service lacks.
+ inFlight.updateAndGet(current -> Math.max(0, current - 1));
+ }
+}
+```
+
+`ShedGuardedService` puts the shedder in front of the real business logic, which is a plain `RequestHandler` function. Shed requests are answered with a `REJECTED` response right away and never reach the handler. Admitted requests always release their slot when they finish, even when the handler throws.
+
+```java
+@Slf4j
+public class ShedGuardedService {
+
+ private final String name;
+ private final LoadShedder shedder;
+ private final RequestHandler handler;
+
+ public Response handle(Request request) {
+ int inFlight;
+ try {
+ // The count is taken from the admission itself: reading it back afterwards could report a
+ // value that belongs to a concurrent request.
+ inFlight = shedder.acquire(request);
+ } catch (LoadShedException e) {
+ LOGGER.warn("[{}] shed {} ({}): {}", name, request.id(), request.priority(), e.getMessage());
+ return Response.rejected(request, e.getMessage());
+ }
+ LOGGER.info("[{}] admitted {} ({}), {}/{} in flight", name, request.id(), request.priority(),
+ inFlight, shedder.getMaxInFlight());
+ try {
+ return Response.accepted(request, handler.handle(request));
+ } finally {
+ shedder.release();
+ }
+ }
+}
+```
+
+The `App` drives three phases. Under light load every request is admitted. Then the payment provider becomes slow, four orders get stuck inside the service and three probes are sent: the low priority one is shed because the service is past the low priority limit, the normal one is shed because only the critical reserve is left, and the critical checkout is admitted into that reserve. When the provider recovers the stuck orders complete and new requests are admitted again.
+
+```java
+var shedder = new LoadShedder(CAPACITY, LOW_PRIORITY_LIMIT, CRITICAL_RESERVE);
+var orderService = new ShedGuardedService("order-service", shedder, paymentProvider);
+
+// Phase 2: four orders are stuck behind a slow payment provider
+report(orderService.handle(new Request("p1", Priority.LOW, "prefetch recommendations")));
+report(orderService.handle(new Request("p2", Priority.NORMAL, "view cart")));
+var checkout = executor.submit(
+ () -> orderService.handle(new Request("p3", Priority.CRITICAL, "checkout payment")));
+```
+
+Running the program produces output similar to this:
+
+```
+Order service capacity: 5 in flight, low priority shed at 3, 1 slot reserved for critical requests
+--- Phase 1: light load, every request is admitted ---
+[order-service] admitted r1 (LOW), 1/5 in flight
+r1 -> ACCEPTED: processed prefetch recommendations
+[order-service] admitted r2 (NORMAL), 1/5 in flight
+r2 -> ACCEPTED: processed view cart
+--- Phase 2: payment provider slows down, orders pile up ---
+[order-service] admitted order-1 (NORMAL), 1/5 in flight
+[order-service] admitted order-2 (NORMAL), 2/5 in flight
+[order-service] admitted order-3 (NORMAL), 3/5 in flight
+[order-service] admitted order-4 (NORMAL), 4/5 in flight
+4 of 5 slots busy, probing with every priority
+[order-service] shed p1 (LOW): Request p1 shed: 4 requests in flight, limit for LOW priority is 3
+p1 -> REJECTED: Request p1 shed: 4 requests in flight, limit for LOW priority is 3
+[order-service] shed p2 (NORMAL): Request p2 shed: 4 requests in flight, limit for NORMAL priority is 4
+p2 -> REJECTED: Request p2 shed: 4 requests in flight, limit for NORMAL priority is 4
+[order-service] admitted p3 (CRITICAL), 5/5 in flight
+--- Phase 3: payment provider recovers, load drops ---
+order-1 -> ACCEPTED: processed place order
+order-2 -> ACCEPTED: processed place order
+order-3 -> ACCEPTED: processed place order
+order-4 -> ACCEPTED: processed place order
+p3 -> ACCEPTED: processed checkout payment
+[order-service] admitted r3 (LOW), 1/5 in flight
+r3 -> ACCEPTED: processed prefetch recommendations
+Summary: accepted=8, shed 2 requests in total: low=1, normal=1, critical=0
+```
+
+## When to Use the Load Shedding Pattern in Java
+
+* A service has a known capacity (threads, connections, CPU) and traffic can exceed it, for example during marketing campaigns, retry storms or when a downstream dependency slows down.
+* Latency matters more than throughput: it is better to answer some callers quickly with an error than to answer everybody late.
+* Requests differ in importance and you want to protect critical flows (checkout, health checks, control plane traffic) at the expense of best-effort work.
+* Callers are able to retry with backoff or to degrade gracefully when they receive a rejection.
+
+## Real-World Applications of Load Shedding Pattern in Java
+
+* Google's frontends shed load based on per-request criticality and measured CPU utilisation, as described in the Site Reliability Engineering book.
+* Netflix's [concurrency-limits](https://github.com/Netflix/concurrency-limits) library rejects requests once the measured concurrency limit of a Java service is reached.
+* Envoy and Istio provide admission control filters that reject requests when success rate or concurrency thresholds are exceeded.
+* Resilience4j's `Bulkhead` and Hystrix's semaphore isolation reject calls when the configured number of concurrent calls is in flight.
+* Netty and Tomcat reject connections when their accept queues are full rather than growing without bound.
+
+## Benefits and Trade-offs of Load Shedding Pattern
+
+Benefits:
+
+* Keeps latency predictable for the requests that are admitted instead of degrading every request.
+* Prevents an overloaded service from exhausting memory, threads or connection pools and crashing.
+* Stops overload from cascading: callers get an immediate answer and can fail over, degrade or back off.
+* Priority-aware shedding protects the business-critical flows first.
+
+Trade-offs:
+
+* Some requests are deliberately rejected, so callers must be prepared to handle a rejection.
+* Choosing capacity limits and priority thresholds requires measurement; limits that are too low waste capacity and limits that are too high do not protect the service.
+* A static in-flight limit does not follow changes in hardware or in the cost of individual requests. Adaptive variants measure latency or CPU instead.
+* Rejected callers that retry immediately can turn shedding into a retry storm, so shedding is usually combined with exponential backoff on the client side.
+
+## Related Java Design Patterns
+
+* [Rate Limiting](../rate-limiting-pattern): limits how many requests a client may send in a time window; load shedding instead reacts to the actual load of the server, whoever the client is.
+* [Throttling](../throttling): slows callers down to a configured rate; load shedding rejects excess work outright once capacity is reached.
+* [Backpressure](../backpressure): asks producers to slow down; load shedding is the last line of defence when producers cannot or do not slow down.
+* [Queue-Based Load Leveling](../queue-based-load-leveling): buffers bursts in a queue; load shedding bounds that queue and drops what does not fit so that latency stays low.
+* [Circuit Breaker](../circuit-breaker): protects a caller from a failing dependency; load shedding protects a service from its callers.
+* [Fallback](../fallback): a natural companion, callers can answer a shed request with a cached or simplified response.
+
+## References and Credits
+
+* [Load Shedding pattern (microservices.io)](https://microservices.io/patterns/reliability/load-shedding.html)
+* [Site Reliability Engineering, chapter Handling Overload (Google)](https://sre.google/sre-book/handling-overload/)
+* [Release It! Design and Deploy Production-Ready Software (Michael T. Nygard)](https://www.amazon.com/gp/product/1680502395)
+* [Using load shedding to avoid overload (Amazon Builders' Library)](https://aws.amazon.com/builders-library/using-load-shedding-to-avoid-overload/)
+* [Netflix concurrency-limits](https://github.com/Netflix/concurrency-limits)
diff --git a/microservices-load-shedding/etc/microservices-load-shedding.urm.png b/microservices-load-shedding/etc/microservices-load-shedding.urm.png
new file mode 100644
index 000000000000..106ac68422b4
Binary files /dev/null and b/microservices-load-shedding/etc/microservices-load-shedding.urm.png differ
diff --git a/microservices-load-shedding/etc/microservices-load-shedding.urm.puml b/microservices-load-shedding/etc/microservices-load-shedding.urm.puml
new file mode 100644
index 000000000000..4d13515d2761
--- /dev/null
+++ b/microservices-load-shedding/etc/microservices-load-shedding.urm.puml
@@ -0,0 +1,90 @@
+@startuml
+package com.iluwatar.loadshedding {
+ enum Priority {
+ + CRITICAL {static}
+ + NORMAL {static}
+ + LOW {static}
+ + valueOf(name : String) : Priority {static}
+ + values() : Priority[] {static}
+ }
+ class Request {
+ + Request(id : String, priority : Priority, description : String)
+ + id() : String
+ + priority() : Priority
+ + description() : String
+ }
+ class Response {
+ + Response(requestId : String, status : Status, message : String)
+ + requestId() : String
+ + status() : Status
+ + message() : String
+ + accepted(request : Request, result : String) : Response {static}
+ + rejected(request : Request, reason : String) : Response {static}
+ }
+ enum Status {
+ + ACCEPTED {static}
+ + REJECTED {static}
+ + valueOf(name : String) : Status {static}
+ + values() : Status[] {static}
+ }
+ class LoadShedException {
+ - requestId : String
+ - priority : Priority
+ + LoadShedException(request : Request, inFlight : int, limit : int)
+ + getRequestId() : String
+ + getPriority() : Priority
+ }
+ class LoadShedder {
+ - maxInFlight : int
+ - limits : Map
+ - inFlight : AtomicInteger
+ - accepted : LongAdder
+ - shed : Map
+ + LoadShedder(maxInFlight : int, lowPriorityLimit : int, criticalReserve : int)
+ + acquire(request : Request) : int
+ + release() : void
+ + getMaxInFlight() : int
+ + getInFlight() : int
+ + getAccepted() : long
+ + getShed(priority : Priority) : long
+ + getTotalShed() : long
+ }
+ interface RequestHandler {
+ + handle(request : Request) : String {abstract}
+ }
+ class ShedGuardedService {
+ - name : String
+ - shedder : LoadShedder
+ - handler : RequestHandler
+ + ShedGuardedService(name : String, shedder : LoadShedder, handler : RequestHandler)
+ + handle(request : Request) : Response
+ }
+ class App {
+ + App()
+ + main(args : String[]) : void {static}
+ ~ simulatedPaymentProvider(paymentSlow : AtomicBoolean, paymentRecovered : CountDownLatch, entered : Semaphore, recoveryTimeout : Duration) : RequestHandler {static}
+ ~ awaitEntered(entered : Semaphore, count : int, timeout : Duration) : void {static}
+ ~ result(future : Future, timeout : Duration) : Response {static}
+ ~ shutdown(executor : ExecutorService, timeout : Duration) : void {static}
+ ~ report(response : Response) : void {static}
+ }
+}
+Response +-- Status
+Request --> Priority
+Response --> Status
+Response ..> Request
+LoadShedException --> Priority
+LoadShedException ..> Request
+LoadShedder ..> Priority
+LoadShedder ..> Request
+LoadShedder ..> LoadShedException
+RequestHandler ..> Request
+ShedGuardedService --> LoadShedder
+ShedGuardedService --> RequestHandler
+ShedGuardedService ..> Request
+ShedGuardedService ..> Response
+ShedGuardedService ..> LoadShedException
+App ..> LoadShedder
+App ..> ShedGuardedService
+App ..> RequestHandler
+@enduml
diff --git a/microservices-load-shedding/pom.xml b/microservices-load-shedding/pom.xml
new file mode 100644
index 000000000000..33582143a0e9
--- /dev/null
+++ b/microservices-load-shedding/pom.xml
@@ -0,0 +1,70 @@
+
+
+
+ 4.0.0
+
+ com.iluwatar
+ java-design-patterns
+ 1.26.0-SNAPSHOT
+
+ microservices-load-shedding
+
+
+ org.slf4j
+ slf4j-api
+
+
+ ch.qos.logback
+ logback-classic
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-assembly-plugin
+
+
+
+
+
+ com.iluwatar.loadshedding.App
+
+
+
+
+
+
+
+
+
diff --git a/microservices-load-shedding/src/main/java/com/iluwatar/loadshedding/App.java b/microservices-load-shedding/src/main/java/com/iluwatar/loadshedding/App.java
new file mode 100644
index 000000000000..f0ca4f95a2a1
--- /dev/null
+++ b/microservices-load-shedding/src/main/java/com/iluwatar/loadshedding/App.java
@@ -0,0 +1,194 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka SeppÀlÀ
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.loadshedding;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * Load Shedding is a resilience pattern for services that receive more work than they can handle.
+ * Instead of accepting every request and slowly drowning, the service measures its own load and
+ * proactively rejects excess requests at the door, so that the requests it does accept are served
+ * with normal latency and the process never runs out of threads, memory or connections.
+ *
+ * The key ingredients demonstrated here are:
+ *
+ *
+ * - a capacity limit expressed as the number of requests in flight ({@link LoadShedder}),
+ *
- fail-fast rejection: shed requests receive an immediate {@link Response.Status#REJECTED}
+ * answer instead of waiting in a queue ({@link ShedGuardedService}),
+ *
- priority-aware shedding: low priority work is dropped first and a small reserve is kept for
+ * critical requests ({@link Priority}),
+ *
- metrics that make the shedding decisions observable.
+ *
+ *
+ * The demo runs an order service with capacity for five concurrent requests. Phase one shows
+ * normal operation. In phase two the payment provider becomes slow, four orders get stuck inside
+ * the service and probes of every priority show which of them are shed. In phase three the provider
+ * recovers, the stuck orders complete and new requests are admitted again.
+ */
+@Slf4j
+public class App {
+
+ private static final int CAPACITY = 5;
+ private static final int LOW_PRIORITY_LIMIT = 3;
+ private static final int CRITICAL_RESERVE = 1;
+ private static final int STUCK_ORDERS = 4;
+ private static final Duration WAIT = Duration.ofSeconds(5);
+
+ /**
+ * Program entry point.
+ *
+ * @param args command line arguments, not used
+ * @throws InterruptedException if the demo is interrupted while waiting for the workers
+ */
+ public static void main(String[] args) throws InterruptedException {
+ var shedder = new LoadShedder(CAPACITY, LOW_PRIORITY_LIMIT, CRITICAL_RESERVE);
+ var paymentSlow = new AtomicBoolean(false);
+ var paymentRecovered = new CountDownLatch(1);
+ var entered = new Semaphore(0);
+ var orderService =
+ new ShedGuardedService(
+ "order-service",
+ shedder,
+ simulatedPaymentProvider(paymentSlow, paymentRecovered, entered, WAIT));
+ var executor = Executors.newCachedThreadPool();
+ try {
+ LOGGER.info(
+ "Order service capacity: {} in flight, low priority shed at {}, {} slot reserved for"
+ + " critical requests",
+ CAPACITY,
+ LOW_PRIORITY_LIMIT,
+ CRITICAL_RESERVE);
+
+ LOGGER.info("--- Phase 1: light load, every request is admitted ---");
+ report(orderService.handle(new Request("r1", Priority.LOW, "prefetch recommendations")));
+ report(orderService.handle(new Request("r2", Priority.NORMAL, "view cart")));
+
+ LOGGER.info("--- Phase 2: payment provider slows down, orders pile up ---");
+ paymentSlow.set(true);
+ List> stuckOrders = new ArrayList<>();
+ for (var i = 1; i <= STUCK_ORDERS; i++) {
+ var order = new Request("order-" + i, Priority.NORMAL, "place order");
+ stuckOrders.add(executor.submit(() -> orderService.handle(order)));
+ }
+ awaitEntered(entered, STUCK_ORDERS, WAIT);
+ LOGGER.info(
+ "{} of {} slots busy, probing with every priority", shedder.getInFlight(), CAPACITY);
+ report(orderService.handle(new Request("p1", Priority.LOW, "prefetch recommendations")));
+ report(orderService.handle(new Request("p2", Priority.NORMAL, "view cart")));
+ var checkout =
+ executor.submit(
+ () -> orderService.handle(new Request("p3", Priority.CRITICAL, "checkout payment")));
+ awaitEntered(entered, 1, WAIT);
+
+ LOGGER.info("--- Phase 3: payment provider recovers, load drops ---");
+ paymentSlow.set(false);
+ paymentRecovered.countDown();
+ for (var order : stuckOrders) {
+ report(result(order, WAIT));
+ }
+ report(result(checkout, WAIT));
+ report(orderService.handle(new Request("r3", Priority.LOW, "prefetch recommendations")));
+
+ LOGGER.info(
+ "Summary: accepted={}, shed {} requests in total: low={}, normal={}, critical={}",
+ shedder.getAccepted(),
+ shedder.getTotalShed(),
+ shedder.getShed(Priority.LOW),
+ shedder.getShed(Priority.NORMAL),
+ shedder.getShed(Priority.CRITICAL));
+ } finally {
+ shutdown(executor, WAIT);
+ }
+ }
+
+ /**
+ * Business logic of the order service. While the payment provider is slow every admitted request
+ * blocks until the provider recovers, which is exactly the situation in which requests pile up
+ * and load shedding becomes necessary. The semaphore tells the demo how many requests are stuck.
+ */
+ static RequestHandler simulatedPaymentProvider(
+ AtomicBoolean paymentSlow,
+ CountDownLatch paymentRecovered,
+ Semaphore entered,
+ Duration recoveryTimeout) {
+ return request -> {
+ if (paymentSlow.get()) {
+ // Signal the demo that one more request is now stuck behind the slow provider.
+ entered.release();
+ try {
+ if (!paymentRecovered.await(recoveryTimeout.toMillis(), TimeUnit.MILLISECONDS)) {
+ throw new IllegalStateException("payment provider never recovered");
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException("interrupted while processing " + request.id(), e);
+ }
+ }
+ return "processed " + request.description();
+ };
+ }
+
+ /** Waits until the given number of requests are stuck inside the service. */
+ static void awaitEntered(Semaphore entered, int count, Duration timeout)
+ throws InterruptedException {
+ if (!entered.tryAcquire(count, timeout.toMillis(), TimeUnit.MILLISECONDS)) {
+ throw new IllegalStateException("requests did not enter the service in time");
+ }
+ }
+
+ /** Collects the response of a request that was handled on a worker thread. */
+ static Response result(Future future, Duration timeout) throws InterruptedException {
+ try {
+ return future.get(timeout.toMillis(), TimeUnit.MILLISECONDS);
+ } catch (ExecutionException | TimeoutException e) {
+ throw new IllegalStateException("worker failed", e);
+ }
+ }
+
+ /** Stops the worker pool, forcing the shutdown if workers do not finish within the timeout. */
+ static void shutdown(ExecutorService executor, Duration timeout) throws InterruptedException {
+ executor.shutdown();
+ if (!executor.awaitTermination(timeout.toMillis(), TimeUnit.MILLISECONDS)) {
+ executor.shutdownNow();
+ }
+ }
+
+ static void report(Response response) {
+ LOGGER.info("{} -> {}: {}", response.requestId(), response.status(), response.message());
+ }
+}
diff --git a/microservices-load-shedding/src/main/java/com/iluwatar/loadshedding/LoadShedException.java b/microservices-load-shedding/src/main/java/com/iluwatar/loadshedding/LoadShedException.java
new file mode 100644
index 000000000000..4015a509c37c
--- /dev/null
+++ b/microservices-load-shedding/src/main/java/com/iluwatar/loadshedding/LoadShedException.java
@@ -0,0 +1,57 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka SeppÀlÀ
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.loadshedding;
+
+import lombok.Getter;
+
+/**
+ * Thrown by {@link LoadShedder#acquire(Request)} when a request must be shed. It is the in-process
+ * equivalent of an HTTP 503 "Service Unavailable" response: the service is healthy but has no spare
+ * capacity for this request right now, and the caller should back off and retry later.
+ */
+@Getter
+public class LoadShedException extends RuntimeException {
+
+ private static final long serialVersionUID = 1L;
+
+ private final String requestId;
+ private final Priority priority;
+
+ /**
+ * Creates the exception for a shed request.
+ *
+ * @param request the request that was shed
+ * @param inFlight number of requests being processed at the moment of the decision
+ * @param limit admission limit that applies to the request's priority
+ */
+ public LoadShedException(Request request, int inFlight, int limit) {
+ super(
+ String.format(
+ "Request %s shed: %d requests in flight, limit for %s priority is %d",
+ request.id(), inFlight, request.priority(), limit));
+ this.requestId = request.id();
+ this.priority = request.priority();
+ }
+}
diff --git a/microservices-load-shedding/src/main/java/com/iluwatar/loadshedding/LoadShedder.java b/microservices-load-shedding/src/main/java/com/iluwatar/loadshedding/LoadShedder.java
new file mode 100644
index 000000000000..4cf4aad0e5f3
--- /dev/null
+++ b/microservices-load-shedding/src/main/java/com/iluwatar/loadshedding/LoadShedder.java
@@ -0,0 +1,146 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka SeppÀlÀ
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.loadshedding;
+
+import java.util.EnumMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.LongAdder;
+
+/**
+ * Admission controller that decides whether a request may enter the service. The decision is based
+ * on how many requests are already in flight and on the priority of the new request:
+ *
+ *
+ * - {@link Priority#LOW} requests are admitted only while the in-flight count is below the low
+ * priority limit, so they are the first to be shed when load builds up.
+ *
- {@link Priority#NORMAL} requests are admitted while the in-flight count is below the hard
+ * capacity minus the reserve kept for critical work.
+ *
- {@link Priority#CRITICAL} requests may use the full capacity, including the reserve.
+ *
+ *
+ * Requests that cannot be admitted are rejected immediately with a {@link LoadShedException}
+ * rather than queued. Rejecting quickly costs almost nothing, whereas letting an unbounded queue
+ * grow would increase latency for every request and eventually exhaust memory or threads.
+ *
+ *
The class is thread-safe. Admission is lock-free: the in-flight count is updated with an
+ * atomic accumulator whose function refuses to increment past the limit, so concurrent callers can
+ * never push the in-flight count above the configured capacity.
+ */
+public class LoadShedder {
+
+ private final int maxInFlight;
+ private final Map limits = new EnumMap<>(Priority.class);
+ private final AtomicInteger inFlight = new AtomicInteger();
+ private final LongAdder accepted = new LongAdder();
+ private final Map shed = new EnumMap<>(Priority.class);
+
+ /**
+ * Creates a load shedder.
+ *
+ * @param maxInFlight hard capacity: the maximum number of requests processed concurrently
+ * @param lowPriorityLimit in-flight count at which low priority requests start being shed
+ * @param criticalReserve part of the capacity that only critical requests may use
+ */
+ public LoadShedder(int maxInFlight, int lowPriorityLimit, int criticalReserve) {
+ if (maxInFlight <= 0) {
+ throw new IllegalArgumentException("maxInFlight must be positive");
+ }
+ if (criticalReserve < 0 || criticalReserve >= maxInFlight) {
+ throw new IllegalArgumentException("criticalReserve must be between 0 and maxInFlight - 1");
+ }
+ var normalLimit = maxInFlight - criticalReserve;
+ if (lowPriorityLimit <= 0 || lowPriorityLimit > normalLimit) {
+ throw new IllegalArgumentException(
+ "lowPriorityLimit must be between 1 and maxInFlight - criticalReserve");
+ }
+ this.maxInFlight = maxInFlight;
+ limits.put(Priority.CRITICAL, maxInFlight);
+ limits.put(Priority.NORMAL, normalLimit);
+ limits.put(Priority.LOW, lowPriorityLimit);
+ for (var priority : Priority.values()) {
+ shed.put(priority, new LongAdder());
+ }
+ }
+
+ /**
+ * Tries to admit the request. On success the in-flight count is incremented and the caller must
+ * invoke {@link #release()} once the work is done.
+ *
+ * @param request the request asking for admission
+ * @return the in-flight count including this request, as observed at the moment of admission
+ * @throws LoadShedException if the service has no spare capacity for this priority
+ */
+ public int acquire(Request request) {
+ var priority = request.priority();
+ var limit = limits.get(priority);
+ // The accumulator is a pure function, so it is safe for the atomic to re-apply it under
+ // contention: the count is only incremented while it is below the limit for this priority.
+ var previous =
+ inFlight.getAndAccumulate(
+ 1, (current, increment) -> current >= limit ? current : current + increment);
+ if (previous >= limit) {
+ // Fail fast: the caller gets an immediate rejection instead of waiting in a queue.
+ shed.get(priority).increment();
+ throw new LoadShedException(request, previous, limit);
+ }
+ accepted.increment();
+ return previous + 1;
+ }
+
+ /**
+ * Signals that a previously admitted request has finished, freeing one slot of capacity. The
+ * count is clamped at zero so that an unmatched release cannot make it negative and hand out more
+ * capacity than the service has.
+ */
+ public void release() {
+ inFlight.updateAndGet(current -> Math.max(0, current - 1));
+ }
+
+ /** Hard capacity of the service. */
+ public int getMaxInFlight() {
+ return maxInFlight;
+ }
+
+ /** Number of requests currently being processed. */
+ public int getInFlight() {
+ return inFlight.get();
+ }
+
+ /** Total number of requests admitted since creation. */
+ public long getAccepted() {
+ return accepted.sum();
+ }
+
+ /** Number of requests of the given priority that were shed since creation. */
+ public long getShed(Priority priority) {
+ return shed.get(priority).sum();
+ }
+
+ /** Total number of requests shed since creation, across all priorities. */
+ public long getTotalShed() {
+ return shed.values().stream().mapToLong(LongAdder::sum).sum();
+ }
+}
diff --git a/microservices-load-shedding/src/main/java/com/iluwatar/loadshedding/Priority.java b/microservices-load-shedding/src/main/java/com/iluwatar/loadshedding/Priority.java
new file mode 100644
index 000000000000..a2baca4d5a53
--- /dev/null
+++ b/microservices-load-shedding/src/main/java/com/iluwatar/loadshedding/Priority.java
@@ -0,0 +1,39 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka SeppÀlÀ
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.loadshedding;
+
+/**
+ * Importance of a request from the point of view of the service that receives it. When the service
+ * approaches its capacity the {@link LoadShedder} sheds the least important requests first, so that
+ * the work that matters most keeps flowing while excess load is rejected.
+ */
+public enum Priority {
+ /** Must be served whenever physically possible, for example checkout or health probes. */
+ CRITICAL,
+ /** Regular user traffic. */
+ NORMAL,
+ /** Best-effort work such as prefetching, analytics or background refreshes. */
+ LOW
+}
diff --git a/microservices-load-shedding/src/main/java/com/iluwatar/loadshedding/Request.java b/microservices-load-shedding/src/main/java/com/iluwatar/loadshedding/Request.java
new file mode 100644
index 000000000000..7ff2681ce21f
--- /dev/null
+++ b/microservices-load-shedding/src/main/java/com/iluwatar/loadshedding/Request.java
@@ -0,0 +1,36 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka SeppÀlÀ
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.loadshedding;
+
+/**
+ * An incoming unit of work. Besides its identity it carries the {@link Priority} the caller
+ * assigned to it, which is the only thing the {@link LoadShedder} needs to decide whether the
+ * request may enter the service.
+ *
+ * @param id unique identifier used in logs and responses
+ * @param priority importance of the request
+ * @param description human readable summary of the work
+ */
+public record Request(String id, Priority priority, String description) {}
diff --git a/microservices-load-shedding/src/main/java/com/iluwatar/loadshedding/RequestHandler.java b/microservices-load-shedding/src/main/java/com/iluwatar/loadshedding/RequestHandler.java
new file mode 100644
index 000000000000..588475ae09b3
--- /dev/null
+++ b/microservices-load-shedding/src/main/java/com/iluwatar/loadshedding/RequestHandler.java
@@ -0,0 +1,41 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka SeppÀlÀ
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.loadshedding;
+
+/**
+ * The actual work a service performs for an admitted request. Kept as a functional interface so
+ * that the admission control in {@link ShedGuardedService} stays independent of the business logic.
+ */
+@FunctionalInterface
+public interface RequestHandler {
+
+ /**
+ * Processes the request.
+ *
+ * @param request an admitted request
+ * @return result to return to the caller
+ */
+ String handle(Request request);
+}
diff --git a/microservices-load-shedding/src/main/java/com/iluwatar/loadshedding/Response.java b/microservices-load-shedding/src/main/java/com/iluwatar/loadshedding/Response.java
new file mode 100644
index 000000000000..494defd14a30
--- /dev/null
+++ b/microservices-load-shedding/src/main/java/com/iluwatar/loadshedding/Response.java
@@ -0,0 +1,55 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka SeppÀlÀ
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.loadshedding;
+
+/**
+ * Outcome returned to the caller. A shed request gets a {@link Status#REJECTED} response
+ * immediately instead of waiting in a queue, which is the fail-fast behaviour that keeps the
+ * service responsive under overload.
+ *
+ * @param requestId identifier of the request this response belongs to
+ * @param status whether the request was processed or shed
+ * @param message result of the work, or the reason the request was shed
+ */
+public record Response(String requestId, Status status, String message) {
+
+ /** Result of admission control. */
+ public enum Status {
+ /** The request was admitted and processed. */
+ ACCEPTED,
+ /** The request was shed because the service is at capacity. Callers may retry later. */
+ REJECTED
+ }
+
+ /** Creates the response for a request that was processed. */
+ public static Response accepted(Request request, String result) {
+ return new Response(request.id(), Status.ACCEPTED, result);
+ }
+
+ /** Creates the fast-failure response for a request that was shed. */
+ public static Response rejected(Request request, String reason) {
+ return new Response(request.id(), Status.REJECTED, reason);
+ }
+}
diff --git a/microservices-load-shedding/src/main/java/com/iluwatar/loadshedding/ShedGuardedService.java b/microservices-load-shedding/src/main/java/com/iluwatar/loadshedding/ShedGuardedService.java
new file mode 100644
index 000000000000..d43315d498db
--- /dev/null
+++ b/microservices-load-shedding/src/main/java/com/iluwatar/loadshedding/ShedGuardedService.java
@@ -0,0 +1,84 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka SeppÀlÀ
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.loadshedding;
+
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * A service whose entry point is protected by a {@link LoadShedder}. Every request first asks the
+ * shedder for admission; requests that are shed receive a {@link Response.Status#REJECTED} response
+ * right away, admitted requests are handed to the {@link RequestHandler} and always release their
+ * capacity slot afterwards, even when the handler fails.
+ */
+@Slf4j
+public class ShedGuardedService {
+
+ private final String name;
+ private final LoadShedder shedder;
+ private final RequestHandler handler;
+
+ /**
+ * Creates a guarded service.
+ *
+ * @param name service name used in log output
+ * @param shedder admission controller protecting the service
+ * @param handler business logic executed for admitted requests
+ */
+ public ShedGuardedService(String name, LoadShedder shedder, RequestHandler handler) {
+ this.name = name;
+ this.shedder = shedder;
+ this.handler = handler;
+ }
+
+ /**
+ * Handles the request if capacity allows, otherwise fails fast.
+ *
+ * @param request incoming request
+ * @return the handler result, or a rejection when the request was shed
+ */
+ public Response handle(Request request) {
+ int inFlight;
+ try {
+ // The count is taken from the admission itself: reading it back afterwards could report a
+ // value that belongs to a concurrent request.
+ inFlight = shedder.acquire(request);
+ } catch (LoadShedException e) {
+ LOGGER.warn("[{}] shed {} ({}): {}", name, request.id(), request.priority(), e.getMessage());
+ return Response.rejected(request, e.getMessage());
+ }
+ LOGGER.info(
+ "[{}] admitted {} ({}), {}/{} in flight",
+ name,
+ request.id(),
+ request.priority(),
+ inFlight,
+ shedder.getMaxInFlight());
+ try {
+ return Response.accepted(request, handler.handle(request));
+ } finally {
+ shedder.release();
+ }
+ }
+}
diff --git a/microservices-load-shedding/src/test/java/com/iluwatar/loadshedding/AppTest.java b/microservices-load-shedding/src/test/java/com/iluwatar/loadshedding/AppTest.java
new file mode 100644
index 000000000000..3a3b6a8f9da6
--- /dev/null
+++ b/microservices-load-shedding/src/test/java/com/iluwatar/loadshedding/AppTest.java
@@ -0,0 +1,177 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka SeppÀlÀ
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.loadshedding;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.time.Duration;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+class AppTest {
+
+ private static final Duration SHORT = Duration.ofMillis(10);
+ private static final Duration GENEROUS = Duration.ofSeconds(1);
+ private static final Request REQUEST = new Request("r", Priority.NORMAL, "place order");
+
+ @AfterEach
+ void clearInterruptFlag() {
+ Thread.interrupted();
+ }
+
+ @Test
+ void shouldLaunchApp() {
+ assertDoesNotThrow(() -> App.main(new String[] {}));
+ }
+
+ @Test
+ void shouldBeInstantiable() {
+ assertNotNull(new App(), "App should be instantiable");
+ }
+
+ @Test
+ void paymentProviderProcessesImmediatelyWhenHealthy() {
+ var entered = new Semaphore(0);
+ var handler =
+ App.simulatedPaymentProvider(
+ new AtomicBoolean(false), new CountDownLatch(1), entered, GENEROUS);
+ assertEquals("processed place order", handler.handle(REQUEST));
+ assertEquals(0, entered.availablePermits());
+ }
+
+ @Test
+ void paymentProviderResumesOnceRecovered() {
+ var entered = new Semaphore(0);
+ var recovered = new CountDownLatch(0);
+ var handler =
+ App.simulatedPaymentProvider(new AtomicBoolean(true), recovered, entered, GENEROUS);
+ assertEquals("processed place order", handler.handle(REQUEST));
+ assertEquals(1, entered.availablePermits());
+ }
+
+ @Test
+ void paymentProviderFailsWhenRecoveryTimesOut() {
+ var handler =
+ App.simulatedPaymentProvider(
+ new AtomicBoolean(true), new CountDownLatch(1), new Semaphore(0), SHORT);
+ var exception = assertThrows(IllegalStateException.class, () -> handler.handle(REQUEST));
+ assertEquals("payment provider never recovered", exception.getMessage());
+ }
+
+ @Test
+ void paymentProviderRestoresInterruptFlagWhenInterruptedWhileSlow() {
+ var entered = new Semaphore(0);
+ var handler =
+ App.simulatedPaymentProvider(
+ new AtomicBoolean(true), new CountDownLatch(1), entered, GENEROUS);
+ Thread.currentThread().interrupt();
+ var exception = assertThrows(IllegalStateException.class, () -> handler.handle(REQUEST));
+ assertInstanceOf(InterruptedException.class, exception.getCause());
+ assertTrue(Thread.interrupted());
+ assertEquals(1, entered.availablePermits());
+ }
+
+ @Test
+ void awaitEnteredReturnsOncePermitsAreAvailable() {
+ var entered = new Semaphore(2);
+ assertDoesNotThrow(() -> App.awaitEntered(entered, 2, GENEROUS));
+ assertEquals(0, entered.availablePermits());
+ }
+
+ @Test
+ void awaitEnteredFailsWhenNobodyEnters() {
+ var entered = new Semaphore(0);
+ var exception =
+ assertThrows(IllegalStateException.class, () -> App.awaitEntered(entered, 1, SHORT));
+ assertEquals("requests did not enter the service in time", exception.getMessage());
+ }
+
+ @Test
+ void resultReturnsCompletedResponse() throws InterruptedException {
+ var response = Response.accepted(REQUEST, "done");
+ assertEquals(response, App.result(CompletableFuture.completedFuture(response), GENEROUS));
+ }
+
+ @Test
+ void resultWrapsFailedWorker() {
+ var failed = CompletableFuture.failedFuture(new RuntimeException("boom"));
+ var exception = assertThrows(IllegalStateException.class, () -> App.result(failed, GENEROUS));
+ assertEquals("worker failed", exception.getMessage());
+ assertInstanceOf(RuntimeException.class, exception.getCause().getCause());
+ }
+
+ @Test
+ void resultWrapsWorkerThatNeverFinishes() {
+ var pending = new CompletableFuture();
+ var exception = assertThrows(IllegalStateException.class, () -> App.result(pending, SHORT));
+ assertEquals("worker failed", exception.getMessage());
+ assertInstanceOf(TimeoutException.class, exception.getCause());
+ }
+
+ @Test
+ void shutdownWaitsForIdleExecutor() throws InterruptedException {
+ var executor = Executors.newSingleThreadExecutor();
+ App.shutdown(executor, GENEROUS);
+ assertTrue(executor.isTerminated());
+ }
+
+ @Test
+ void shutdownForcesStopWhenWorkersIgnoreTheTimeout() throws InterruptedException {
+ var executor = Executors.newSingleThreadExecutor();
+ var started = new CountDownLatch(1);
+ var interrupted = new CountDownLatch(1);
+ var release = new CountDownLatch(1);
+ executor.execute(
+ () -> {
+ started.countDown();
+ while (release.getCount() > 0) {
+ try {
+ release.await();
+ } catch (InterruptedException e) {
+ // A stubborn worker that keeps going despite the interrupt.
+ interrupted.countDown();
+ }
+ }
+ });
+ assertTrue(started.await(1, TimeUnit.SECONDS));
+ App.shutdown(executor, SHORT);
+ assertTrue(executor.isShutdown());
+ assertTrue(interrupted.await(1, TimeUnit.SECONDS));
+ release.countDown();
+ assertTrue(executor.awaitTermination(1, TimeUnit.SECONDS));
+ }
+}
diff --git a/microservices-load-shedding/src/test/java/com/iluwatar/loadshedding/LoadShedderTest.java b/microservices-load-shedding/src/test/java/com/iluwatar/loadshedding/LoadShedderTest.java
new file mode 100644
index 000000000000..ffd9f4493091
--- /dev/null
+++ b/microservices-load-shedding/src/test/java/com/iluwatar/loadshedding/LoadShedderTest.java
@@ -0,0 +1,201 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka SeppÀlÀ
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.loadshedding;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.junit.jupiter.api.Test;
+
+class LoadShedderTest {
+
+ private static final int CAPACITY = 5;
+ private static final int LOW_LIMIT = 3;
+ private static final int RESERVE = 1;
+
+ private final LoadShedder shedder = new LoadShedder(CAPACITY, LOW_LIMIT, RESERVE);
+
+ private static Request request(Priority priority) {
+ return new Request("req-" + priority, priority, "test");
+ }
+
+ private void fill(int count) {
+ for (var i = 0; i < count; i++) {
+ shedder.acquire(request(Priority.CRITICAL));
+ }
+ }
+
+ @Test
+ void admitsEveryPriorityBelowLowPriorityLimit() {
+ fill(LOW_LIMIT - 1);
+ assertDoesNotThrow(() -> shedder.acquire(request(Priority.LOW)));
+ assertEquals(LOW_LIMIT, shedder.getInFlight());
+ assertEquals(LOW_LIMIT, shedder.getAccepted());
+ assertEquals(0, shedder.getTotalShed());
+ }
+
+ @Test
+ void shedsLowPriorityFirst() {
+ fill(LOW_LIMIT);
+ assertThrows(LoadShedException.class, () -> shedder.acquire(request(Priority.LOW)));
+ assertDoesNotThrow(() -> shedder.acquire(request(Priority.NORMAL)));
+ assertEquals(LOW_LIMIT + 1, shedder.getInFlight());
+ assertEquals(1, shedder.getShed(Priority.LOW));
+ assertEquals(0, shedder.getShed(Priority.NORMAL));
+ }
+
+ @Test
+ void keepsReserveForCriticalRequests() {
+ fill(CAPACITY - RESERVE);
+ assertThrows(LoadShedException.class, () -> shedder.acquire(request(Priority.NORMAL)));
+ assertDoesNotThrow(() -> shedder.acquire(request(Priority.CRITICAL)));
+ assertEquals(CAPACITY, shedder.getInFlight());
+ assertEquals(1, shedder.getShed(Priority.NORMAL));
+ assertEquals(0, shedder.getShed(Priority.CRITICAL));
+ }
+
+ @Test
+ void shedsCriticalRequestsAtHardCapacity() {
+ fill(CAPACITY);
+ assertThrows(LoadShedException.class, () -> shedder.acquire(request(Priority.CRITICAL)));
+ assertEquals(CAPACITY, shedder.getInFlight());
+ assertEquals(1, shedder.getShed(Priority.CRITICAL));
+ }
+
+ @Test
+ void releaseFreesCapacity() {
+ fill(LOW_LIMIT);
+ assertThrows(LoadShedException.class, () -> shedder.acquire(request(Priority.LOW)));
+ shedder.release();
+ assertDoesNotThrow(() -> shedder.acquire(request(Priority.LOW)));
+ assertEquals(LOW_LIMIT, shedder.getInFlight());
+ }
+
+ @Test
+ void unmatchedReleaseDoesNotDriveInFlightNegative() {
+ shedder.release();
+ assertEquals(0, shedder.getInFlight());
+ fill(CAPACITY);
+ assertThrows(LoadShedException.class, () -> shedder.acquire(request(Priority.CRITICAL)));
+ }
+
+ @Test
+ void acquireReturnsInFlightCountIncludingTheAdmittedRequest() {
+ assertEquals(1, shedder.acquire(request(Priority.NORMAL)));
+ assertEquals(2, shedder.acquire(request(Priority.NORMAL)));
+ }
+
+ @Test
+ void countsShedRequestsPerPriority() {
+ fill(CAPACITY);
+ assertThrows(LoadShedException.class, () -> shedder.acquire(request(Priority.LOW)));
+ assertThrows(LoadShedException.class, () -> shedder.acquire(request(Priority.LOW)));
+ assertThrows(LoadShedException.class, () -> shedder.acquire(request(Priority.NORMAL)));
+ assertThrows(LoadShedException.class, () -> shedder.acquire(request(Priority.CRITICAL)));
+ assertEquals(CAPACITY, shedder.getAccepted());
+ assertEquals(2, shedder.getShed(Priority.LOW));
+ assertEquals(1, shedder.getShed(Priority.NORMAL));
+ assertEquals(1, shedder.getShed(Priority.CRITICAL));
+ assertEquals(4, shedder.getTotalShed());
+ }
+
+ @Test
+ void exceptionDescribesTheDecision() {
+ fill(LOW_LIMIT);
+ var request = new Request("low-42", Priority.LOW, "test");
+ var exception = assertThrows(LoadShedException.class, () -> shedder.acquire(request));
+ assertEquals("low-42", exception.getRequestId());
+ assertEquals(Priority.LOW, exception.getPriority());
+ assertTrue(exception.getMessage().contains("low-42"));
+ assertTrue(exception.getMessage().contains("LOW"));
+ }
+
+ @Test
+ void neverExceedsCapacityUnderConcurrentAdmission() throws InterruptedException {
+ var callers = 50;
+ var start = new CountDownLatch(1);
+ var done = new CountDownLatch(callers);
+ var admitted = new AtomicInteger();
+ var executor = Executors.newFixedThreadPool(callers);
+ try {
+ for (var i = 0; i < callers; i++) {
+ executor.execute(
+ () -> {
+ try {
+ start.await();
+ shedder.acquire(request(Priority.CRITICAL));
+ admitted.incrementAndGet();
+ } catch (LoadShedException expected) {
+ // shed
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ } finally {
+ done.countDown();
+ }
+ });
+ }
+ start.countDown();
+ assertTrue(done.await(10, TimeUnit.SECONDS));
+ } finally {
+ executor.shutdownNow();
+ }
+ assertEquals(CAPACITY, admitted.get());
+ assertEquals(CAPACITY, shedder.getInFlight());
+ assertEquals(callers - CAPACITY, shedder.getShed(Priority.CRITICAL));
+ }
+
+ @Test
+ void rejectsInvalidConfiguration() {
+ // maxInFlight must be positive
+ assertThrows(IllegalArgumentException.class, () -> new LoadShedder(0, 1, 0));
+ // criticalReserve must be between 0 and maxInFlight - 1
+ assertThrows(IllegalArgumentException.class, () -> new LoadShedder(5, 3, -1));
+ assertThrows(IllegalArgumentException.class, () -> new LoadShedder(5, 3, 5));
+ // lowPriorityLimit must be between 1 and maxInFlight - criticalReserve
+ assertThrows(IllegalArgumentException.class, () -> new LoadShedder(5, 0, 1));
+ assertThrows(IllegalArgumentException.class, () -> new LoadShedder(5, 5, 1));
+ }
+
+ @Test
+ void acceptsBoundaryConfiguration() {
+ assertDoesNotThrow(() -> new LoadShedder(5, 4, 1));
+ var noReserve = new LoadShedder(5, 5, 0);
+ fillWith(noReserve, Priority.LOW, 5);
+ assertEquals(5, noReserve.getInFlight());
+ assertThrows(LoadShedException.class, () -> noReserve.acquire(request(Priority.CRITICAL)));
+ }
+
+ private static void fillWith(LoadShedder target, Priority priority, int count) {
+ for (var i = 0; i < count; i++) {
+ target.acquire(request(priority));
+ }
+ }
+}
diff --git a/microservices-load-shedding/src/test/java/com/iluwatar/loadshedding/ShedGuardedServiceTest.java b/microservices-load-shedding/src/test/java/com/iluwatar/loadshedding/ShedGuardedServiceTest.java
new file mode 100644
index 000000000000..87c847196517
--- /dev/null
+++ b/microservices-load-shedding/src/test/java/com/iluwatar/loadshedding/ShedGuardedServiceTest.java
@@ -0,0 +1,80 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka SeppÀlÀ
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.iluwatar.loadshedding;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.junit.jupiter.api.Test;
+
+class ShedGuardedServiceTest {
+
+ private final LoadShedder shedder = new LoadShedder(2, 1, 0);
+
+ @Test
+ void returnsHandlerResultWhenAdmitted() {
+ var service = new ShedGuardedService("svc", shedder, request -> "done " + request.id());
+ var response = service.handle(new Request("a", Priority.NORMAL, "work"));
+ assertEquals(new Response("a", Response.Status.ACCEPTED, "done a"), response);
+ assertEquals(0, shedder.getInFlight());
+ }
+
+ @Test
+ void returnsRejectionWithoutInvokingHandlerWhenShed() {
+ var handlerCalled = new AtomicBoolean();
+ var service =
+ new ShedGuardedService(
+ "svc",
+ shedder,
+ request -> {
+ handlerCalled.set(true);
+ return "unexpected";
+ });
+ shedder.acquire(new Request("occupied", Priority.NORMAL, "work"));
+ var response = service.handle(new Request("b", Priority.LOW, "work"));
+ assertEquals(Response.Status.REJECTED, response.status());
+ assertEquals("b", response.requestId());
+ assertFalse(handlerCalled.get());
+ assertEquals(1, shedder.getInFlight());
+ assertEquals(1, shedder.getShed(Priority.LOW));
+ }
+
+ @Test
+ void releasesCapacityWhenHandlerFails() {
+ var service =
+ new ShedGuardedService(
+ "svc",
+ shedder,
+ request -> {
+ throw new IllegalStateException("boom");
+ });
+ assertThrows(
+ IllegalStateException.class,
+ () -> service.handle(new Request("c", Priority.NORMAL, "work")));
+ assertEquals(0, shedder.getInFlight());
+ }
+}
diff --git a/pom.xml b/pom.xml
index a71630d289d3..17cca3d15482 100644
--- a/pom.xml
+++ b/pom.xml
@@ -260,6 +260,8 @@
rate-limiting-pattern
fallback
onion-architecture
+ microservices-bulkhead
+ microservices-load-shedding