Skip to content

Commit 4d79669

Browse files
committed
refactor: drop timeout registry and merge the demo services
TimeoutRegistry was configuration scaffolding rather than part of the pattern, so App now builds its TimeoutPolicy values directly. ProductCatalogService and RecommendationService only differed in name, latency and payload, so they collapse into a single parameterised DownstreamService and App's two near-identical helpers into one call helper. README, PlantUML diagram and tests follow; the sample output block is taken from a real run of the demo.
1 parent 9124ada commit 4d79669

9 files changed

Lines changed: 115 additions & 397 deletions

File tree

‎timeout/README.md‎

Lines changed: 35 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -87,32 +87,7 @@ public record TimeoutPolicy(String serviceName, Duration timeout) {
8787
}
8888
```
8989

90-
2. **Keep the limits configurable in one place**
91-
92-
A `TimeoutRegistry` stores the policies and hands out a default for services nobody configured explicitly.
93-
94-
```java
95-
public class TimeoutRegistry {
96-
97-
private final Map<String, TimeoutPolicy> policies = new ConcurrentHashMap<>();
98-
private final Duration defaultTimeout;
99-
100-
public TimeoutRegistry(Duration defaultTimeout) {
101-
this.defaultTimeout = Objects.requireNonNull(defaultTimeout, "defaultTimeout");
102-
}
103-
104-
public TimeoutRegistry register(TimeoutPolicy policy) {
105-
policies.put(policy.serviceName(), policy);
106-
return this;
107-
}
108-
109-
public TimeoutPolicy policyFor(String serviceName) {
110-
return policies.getOrDefault(serviceName, new TimeoutPolicy(serviceName, defaultTimeout));
111-
}
112-
}
113-
```
114-
115-
3. **Enforce the limit**
90+
2. **Enforce the limit**
11691

11792
`TimeoutExecutor` runs the call on a worker thread and waits for at most the configured duration. On a timeout it cancels the worker with an interrupt, logs the event, counts it in `TimeoutMetrics`, and returns the fallback. A failure raised by the service, or a call the pool refuses to accept, is not a timeout and is rethrown as `ServiceCallException`.
11893

@@ -160,64 +135,52 @@ public class TimeoutExecutor implements AutoCloseable {
160135
}
161136
```
162137

163-
4. **Make the slow service cooperate with cancellation**
138+
3. **Make the service cooperate with cancellation**
164139

165-
The simulated `RecommendationService` sleeps interruptibly, so the interrupt sent by the executor actually stops the work instead of leaving it running in the background.
140+
The simulated `DownstreamService` sleeps interruptibly, so the interrupt sent by the executor actually stops the work instead of leaving it running in the background. Its name, latency and payload are constructor arguments, so the same class plays both the fast catalog and the slow recommendation engine.
166141

167142
```java
168-
public List<String> recommendationsFor(String customer) throws InterruptedException {
169-
LOGGER.info(
170-
"{}: computing recommendations for {}, expected latency {} ms",
171-
NAME,
172-
customer,
173-
latency.toMillis());
143+
public List<String> fetch() throws InterruptedException {
144+
LOGGER.info("{}: responding, expected latency {} ms", name, latency.toMillis());
174145
try {
175146
Thread.sleep(latency);
176147
} catch (InterruptedException e) {
177-
LOGGER.info("{}: interrupted, abandoning the computation for {}", NAME, customer);
148+
LOGGER.info("{}: interrupted, abandoning the call", name);
178149
throw e;
179150
}
180-
return List.of("Mechanical keyboard", "USB-C dock");
151+
return items;
181152
}
182153
```
183154

184-
5. **Wire it together**
155+
4. **Wire it together**
185156

186-
`App` registers a 500 ms limit for the catalog and a 100 ms limit for recommendations. Each call lives in a small helper that pairs the policy with the call and its fallback; `main` runs both. The catalog answers in time; the recommendation engine needs 400 ms, so the customer sees popular items instead and the timeout counter shows one event.
157+
`App` gives the catalog a 500 ms limit and recommendations a 100 ms one, then runs both calls through a small helper that pairs the policy with the call and its fallback. The catalog answers in time; the recommendation engine needs 400 ms, so the customer sees popular items instead and the timeout counter shows one event.
187158

188159
```java
189-
static List<String> loadProducts(
190-
TimeoutExecutor executor, TimeoutRegistry registry, ProductCatalogService catalog) {
191-
return executor.execute(
192-
registry.policyFor(ProductCatalogService.NAME), catalog::fetchProducts, List::of);
193-
}
194-
195-
static List<String> loadRecommendations(
160+
static List<String> call(
196161
TimeoutExecutor executor,
197-
TimeoutRegistry registry,
198-
RecommendationService recommendations,
199-
String customer) {
200-
return executor.execute(
201-
registry.policyFor(RecommendationService.NAME),
202-
() -> recommendations.recommendationsFor(customer),
203-
() -> POPULAR_ITEMS);
162+
TimeoutPolicy policy,
163+
DownstreamService service,
164+
List<String> fallback) {
165+
return executor.execute(policy, service::fetch, () -> fallback);
204166
}
205167
```
206168

207169
```java
208-
var registry =
209-
new TimeoutRegistry(Duration.ofMillis(300))
210-
.register(TimeoutPolicy.of(ProductCatalogService.NAME, 500))
211-
.register(TimeoutPolicy.of(RecommendationService.NAME, 100));
212-
213-
var catalog = new ProductCatalogService(Duration.ofMillis(50));
214-
var recommendations = new RecommendationService(Duration.ofMillis(400));
170+
var catalog =
171+
new DownstreamService(
172+
"product-catalog", Duration.ofMillis(50), List.of("Laptop", "Headphones", "Monitor"));
173+
var recommendations =
174+
new DownstreamService(
175+
"recommendations", Duration.ofMillis(400), List.of("Mechanical keyboard", "USB-C dock"));
176+
var catalogPolicy = TimeoutPolicy.of(catalog.name(), 500);
177+
var recommendationPolicy = TimeoutPolicy.of(recommendations.name(), 100);
215178

216179
try (var executor = new TimeoutExecutor()) {
217-
var products = loadProducts(executor, registry, catalog);
180+
var products = call(executor, catalogPolicy, catalog, List.of());
218181
LOGGER.info("Products: {}", products);
219182

220-
var suggested = loadRecommendations(executor, registry, recommendations, "alice");
183+
var suggested = call(executor, recommendationPolicy, recommendations, POPULAR_ITEMS);
221184
LOGGER.info("Recommendations shown to alice: {}", suggested);
222185

223186
LOGGER.info("Timeouts per service: {}", executor.metrics().snapshot());
@@ -229,13 +192,13 @@ Running the application produces output along these lines:
229192
```
230193
Configured per-service limits: catalog 500 ms, recommendations 100 ms
231194
Calling product-catalog
232-
product-catalog: fetching products, expected latency 50 ms
195+
product-catalog: responding, expected latency 50 ms
233196
product-catalog responded within its 500 ms limit
234197
Products: [Laptop, Headphones, Monitor]
235198
Calling recommendations
236-
recommendations: computing recommendations for alice, expected latency 400 ms
199+
recommendations: responding, expected latency 400 ms
200+
recommendations: interrupted, abandoning the call
237201
recommendations exceeded its 100 ms limit; call cancelled, using fallback
238-
recommendations: interrupted, abandoning the computation for alice
239202
Recommendations shown to alice: [Wireless mouse, Webcam]
240203
Timeouts per service: {recommendations=1}
241204
```
@@ -252,12 +215,6 @@ classDiagram
252215
+Duration timeout
253216
+of(serviceName, millis)$ TimeoutPolicy
254217
}
255-
class TimeoutRegistry {
256-
-Map~String, TimeoutPolicy~ policies
257-
-Duration defaultTimeout
258-
+register(policy) TimeoutRegistry
259-
+policyFor(serviceName) TimeoutPolicy
260-
}
261218
class TimeoutMetrics {
262219
-ConcurrentMap~String, AtomicInteger~ timeouts
263220
+recordTimeout(serviceName) void
@@ -274,34 +231,27 @@ classDiagram
274231
class ServiceCallException {
275232
+ServiceCallException(serviceName, cause)
276233
}
277-
class ProductCatalogService {
278-
+String NAME$
279-
-Duration latency
280-
+fetchProducts() List~String~
281-
}
282-
class RecommendationService {
283-
+String NAME$
234+
class DownstreamService {
235+
-String name
284236
-Duration latency
285-
+recommendationsFor(customer) List~String~
237+
-List~String~ items
238+
+name() String
239+
+fetch() List~String~
286240
}
287241
class App {
288242
-List~String~ POPULAR_ITEMS$
289243
+main(args)$ void
290-
~loadProducts(executor, registry, catalog)$ List~String~
291-
~loadRecommendations(executor, registry, recommendations, customer)$ List~String~
244+
~call(executor, policy, service, fallback)$ List~String~
292245
}
293246
294247
ServiceCallException --|> RuntimeException
295-
TimeoutRegistry --> "*" TimeoutPolicy : holds
296248
TimeoutExecutor --> TimeoutMetrics : counts timeouts in
297249
TimeoutExecutor ..> TimeoutPolicy : reads the limit from
298250
TimeoutExecutor ..> ServiceCallException : throws
299-
App ..> TimeoutRegistry : configures
300-
App ..> TimeoutPolicy : registers
251+
App ..> TimeoutPolicy : configures
301252
App ..> TimeoutExecutor : calls through
302253
App ..> TimeoutMetrics : reports
303-
App ..> ProductCatalogService : calls
304-
App ..> RecommendationService : calls
254+
App ..> DownstreamService : calls
305255
```
306256

307257
The same diagram is kept as PlantUML in [timeout.urm.puml](./etc/timeout.urm.puml).

‎timeout/etc/timeout.urm.puml‎

Lines changed: 8 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,6 @@ package com.iluwatar.timeout {
88
+ serviceName() : String
99
+ timeout() : Duration
1010
}
11-
class TimeoutRegistry {
12-
- policies : Map<String, TimeoutPolicy>
13-
- defaultTimeout : Duration
14-
+ TimeoutRegistry(defaultTimeout : Duration)
15-
+ register(policy : TimeoutPolicy) : TimeoutRegistry
16-
+ policyFor(serviceName : String) : TimeoutPolicy
17-
}
1811
class TimeoutMetrics {
1912
- timeouts : ConcurrentMap<String, AtomicInteger>
2013
+ TimeoutMetrics()
@@ -34,34 +27,26 @@ package com.iluwatar.timeout {
3427
class ServiceCallException {
3528
+ ServiceCallException(serviceName : String, cause : Throwable)
3629
}
37-
class ProductCatalogService {
38-
+ NAME : String {static}
39-
- latency : Duration
40-
+ ProductCatalogService(latency : Duration)
41-
+ fetchProducts() : List<String>
42-
}
43-
class RecommendationService {
44-
+ NAME : String {static}
30+
class DownstreamService {
31+
- name : String
4532
- latency : Duration
46-
+ RecommendationService(latency : Duration)
47-
+ recommendationsFor(customer : String) : List<String>
33+
- items : List<String>
34+
+ DownstreamService(name : String, latency : Duration, items : List<String>)
35+
+ name() : String
36+
+ fetch() : List<String>
4837
}
4938
class App {
5039
- POPULAR_ITEMS : List<String> {static}
5140
+ main(args : String[]) : void
52-
~ loadProducts(executor : TimeoutExecutor, registry : TimeoutRegistry, catalog : ProductCatalogService) : List<String> {static}
53-
~ loadRecommendations(executor : TimeoutExecutor, registry : TimeoutRegistry, recommendations : RecommendationService, customer : String) : List<String> {static}
41+
~ call(executor : TimeoutExecutor, policy : TimeoutPolicy, service : DownstreamService, fallback : List<String>) : List<String> {static}
5442
}
5543
}
5644
ServiceCallException --|> RuntimeException
5745
TimeoutExecutor --> TimeoutMetrics
5846
TimeoutExecutor ..> TimeoutPolicy
5947
TimeoutExecutor ..> ServiceCallException
60-
TimeoutRegistry --> "*" TimeoutPolicy
61-
App ..> TimeoutRegistry
6248
App ..> TimeoutPolicy
6349
App ..> TimeoutExecutor
6450
App ..> TimeoutMetrics
65-
App ..> ProductCatalogService
66-
App ..> RecommendationService
51+
App ..> DownstreamService
6752
@enduml

‎timeout/src/main/java/com/iluwatar/timeout/App.java‎

Lines changed: 26 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,8 @@
3434
* system stalls. With a limit the caller abandons the slow call, records the event and continues
3535
* with a fallback, keeping latency predictable and failures contained.
3636
*
37-
* <p>The building blocks are a {@link TimeoutPolicy} per service, a {@link TimeoutRegistry} that
38-
* makes the limits configurable in one place, and a {@link TimeoutExecutor} that enforces them,
39-
* cancels calls that overrun, and counts timeouts in {@link TimeoutMetrics}.
37+
* <p>The building blocks are a {@link TimeoutPolicy} per service and a {@link TimeoutExecutor} that
38+
* enforces them, cancels calls that overrun, and counts timeouts in {@link TimeoutMetrics}.
4039
*
4140
* <p>The demo wires two services with different limits. The product catalog answers well within its
4241
* 500 ms budget and returns real data. The recommendation engine needs 400 ms but is only allowed
@@ -54,60 +53,45 @@ public class App {
5453
* @param args command line arguments, not used
5554
*/
5655
public static void main(String[] args) {
57-
var registry =
58-
new TimeoutRegistry(Duration.ofMillis(300))
59-
.register(TimeoutPolicy.of(ProductCatalogService.NAME, 500))
60-
.register(TimeoutPolicy.of(RecommendationService.NAME, 100));
56+
var catalog =
57+
new DownstreamService(
58+
"product-catalog", Duration.ofMillis(50), List.of("Laptop", "Headphones", "Monitor"));
59+
var recommendations =
60+
new DownstreamService(
61+
"recommendations",
62+
Duration.ofMillis(400),
63+
List.of("Mechanical keyboard", "USB-C dock"));
64+
var catalogPolicy = TimeoutPolicy.of(catalog.name(), 500);
65+
var recommendationPolicy = TimeoutPolicy.of(recommendations.name(), 100);
6166
LOGGER.info("Configured per-service limits: catalog 500 ms, recommendations 100 ms");
6267

63-
var catalog = new ProductCatalogService(Duration.ofMillis(50));
64-
var recommendations = new RecommendationService(Duration.ofMillis(400));
65-
6668
try (var executor = new TimeoutExecutor()) {
67-
LOGGER.info("Calling {}", ProductCatalogService.NAME);
68-
var products = loadProducts(executor, registry, catalog);
69+
LOGGER.info("Calling {}", catalog.name());
70+
var products = call(executor, catalogPolicy, catalog, List.of());
6971
LOGGER.info("Products: {}", products);
7072

71-
LOGGER.info("Calling {}", RecommendationService.NAME);
72-
var suggested = loadRecommendations(executor, registry, recommendations, "alice");
73+
LOGGER.info("Calling {}", recommendations.name());
74+
var suggested = call(executor, recommendationPolicy, recommendations, POPULAR_ITEMS);
7375
LOGGER.info("Recommendations shown to alice: {}", suggested);
7476

7577
LOGGER.info("Timeouts per service: {}", executor.metrics().snapshot());
7678
}
7779
}
7880

7981
/**
80-
* Loads the catalog under its time limit, showing an empty catalog if the limit is exceeded.
81-
*
82-
* @param executor executor enforcing the limit
83-
* @param registry registry holding the catalog's policy
84-
* @param catalog the downstream catalog service
85-
* @return the products, or an empty list on timeout
86-
*/
87-
static List<String> loadProducts(
88-
TimeoutExecutor executor, TimeoutRegistry registry, ProductCatalogService catalog) {
89-
return executor.execute(
90-
registry.policyFor(ProductCatalogService.NAME), catalog::fetchProducts, List::of);
91-
}
92-
93-
/**
94-
* Loads personalised recommendations under their time limit, showing popular items instead if the
95-
* limit is exceeded.
82+
* Calls a service under its time limit, answering with the fallback if the limit is exceeded.
9683
*
9784
* @param executor executor enforcing the limit
98-
* @param registry registry holding the recommendation service's policy
99-
* @param recommendations the downstream recommendation service
100-
* @param customer customer to personalise for
101-
* @return the recommendations, or the popular items on timeout
85+
* @param policy limit that applies to the service
86+
* @param service the downstream service to call
87+
* @param fallback answer to use when the limit is exceeded
88+
* @return the response of the service, or the fallback on timeout
10289
*/
103-
static List<String> loadRecommendations(
90+
static List<String> call(
10491
TimeoutExecutor executor,
105-
TimeoutRegistry registry,
106-
RecommendationService recommendations,
107-
String customer) {
108-
return executor.execute(
109-
registry.policyFor(RecommendationService.NAME),
110-
() -> recommendations.recommendationsFor(customer),
111-
() -> POPULAR_ITEMS);
92+
TimeoutPolicy policy,
93+
DownstreamService service,
94+
List<String> fallback) {
95+
return executor.execute(policy, service::fetch, () -> fallback);
11296
}
11397
}

0 commit comments

Comments
 (0)