The building blocks are a {@link TimeoutPolicy} per service, a {@link TimeoutRegistry} that + * makes the limits configurable in one place, and a {@link TimeoutExecutor} that enforces them, + * cancels calls that overrun, and counts timeouts in {@link TimeoutMetrics}. + * + *
The demo wires two services with different limits. The product catalog answers well within its
+ * 500 ms budget and returns real data. The recommendation engine needs 400 ms but is only allowed
+ * 100 ms, so its call is cancelled and the customer sees popular items instead. The timeout
+ * counters are printed at the end.
+ */
+@Slf4j
+public class App {
+
+ private static final List Such failures are deliberately not masked by the fallback: a timeout means "too slow", while
+ * an exception from the service means "broken", and the two deserve different handling.
+ */
+public class ServiceCallException extends RuntimeException {
+
+ /**
+ * Creates the exception.
+ *
+ * @param serviceName name of the service whose call failed
+ * @param cause the failure raised by the service
+ */
+ public ServiceCallException(String serviceName, Throwable cause) {
+ super("Call to " + serviceName + " failed", cause);
+ }
+}
diff --git a/timeout/src/main/java/com/iluwatar/timeout/TimeoutExecutor.java b/timeout/src/main/java/com/iluwatar/timeout/TimeoutExecutor.java
new file mode 100644
index 000000000000..33f248b1aff9
--- /dev/null
+++ b/timeout/src/main/java/com/iluwatar/timeout/TimeoutExecutor.java
@@ -0,0 +1,113 @@
+/*
+ * 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.timeout;
+
+import java.util.Objects;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Supplier;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * Runs downstream calls under the time limit declared by their {@link TimeoutPolicy}.
+ *
+ * The call is executed on a separate thread while the caller waits for at most the configured
+ * duration. When the limit is exceeded the call is cancelled with an interrupt, the event is logged
+ * and counted in {@link TimeoutMetrics}, and the supplied fallback provides the answer instead.
+ * Failures raised by the service itself are not treated as timeouts; they surface as {@link
+ * ServiceCallException}.
+ */
+@Slf4j
+public class TimeoutExecutor implements AutoCloseable {
+
+ private final ExecutorService executor;
+ private final TimeoutMetrics metrics = new TimeoutMetrics();
+
+ /** Creates an executor that runs every call on its own virtual thread. */
+ public TimeoutExecutor() {
+ this(Executors.newVirtualThreadPerTaskExecutor());
+ }
+
+ /**
+ * Creates an executor backed by the given thread pool.
+ *
+ * @param executor pool used to run the calls
+ */
+ public TimeoutExecutor(ExecutorService executor) {
+ this.executor = Objects.requireNonNull(executor, "executor");
+ }
+
+ /**
+ * Executes a call within the limit of its policy.
+ *
+ * @param policy limit that applies to the call
+ * @param call the downstream invocation
+ * @param fallback answer to use when the call does not complete in time
+ * @param Each service gets its own policy so that a fast catalog lookup and a slow recommendation
+ * engine can be governed by different limits.
+ *
+ * @param serviceName name of the downstream service the policy applies to
+ * @param timeout maximum time the caller is willing to wait for a response
+ */
+public record TimeoutPolicy(String serviceName, Duration timeout) {
+
+ /** Validates that the policy names a service and carries a positive limit. */
+ public TimeoutPolicy {
+ Objects.requireNonNull(serviceName, "serviceName");
+ Objects.requireNonNull(timeout, "timeout");
+ if (serviceName.isBlank()) {
+ throw new IllegalArgumentException("serviceName must not be blank");
+ }
+ if (timeout.isZero() || timeout.isNegative()) {
+ throw new IllegalArgumentException("timeout must be positive");
+ }
+ }
+
+ /**
+ * Convenience factory for millisecond based limits.
+ *
+ * @param serviceName name of the downstream service
+ * @param millis limit in milliseconds
+ * @return the policy
+ */
+ public static TimeoutPolicy of(String serviceName, long millis) {
+ return new TimeoutPolicy(serviceName, Duration.ofMillis(millis));
+ }
+}
diff --git a/timeout/src/main/java/com/iluwatar/timeout/TimeoutRegistry.java b/timeout/src/main/java/com/iluwatar/timeout/TimeoutRegistry.java
new file mode 100644
index 000000000000..67b712406983
--- /dev/null
+++ b/timeout/src/main/java/com/iluwatar/timeout/TimeoutRegistry.java
@@ -0,0 +1,72 @@
+/*
+ * 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.timeout;
+
+import java.time.Duration;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Holds the {@link TimeoutPolicy} configured for each downstream service.
+ *
+ * Services that have no explicit policy fall back to a default limit, so callers never have to
+ * hard code a duration next to the call site.
+ */
+public class TimeoutRegistry {
+
+ private final Map