Skip to content

feat: add Timeout pattern (#2845) - #3598

Open
ylcn91 wants to merge 1 commit into
iluwatar:masterfrom
ylcn91:feat/timeout
Open

feat: add Timeout pattern (#2845)#3598
ylcn91 wants to merge 1 commit into
iluwatar:masterfrom
ylcn91:feat/timeout

Conversation

@ylcn91

@ylcn91 ylcn91 commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Adds the Timeout pattern as a new timeout module.

  • Problem: without a time limit a single slow dependency can hold threads and user requests hostage until the whole system stalls.
  • Solution: every downstream call runs under a per-service TimeoutPolicy. When the limit is exceeded the call is cancelled with an interrupt, the event is logged and counted, and a fallback answer is returned.
  • Key components:
    • TimeoutPolicy (record) and TimeoutRegistry: per-service configurable limits with a default.
    • TimeoutExecutor: enforces the limit (Future.get(timeout)), cancels the overrunning call, records the event in TimeoutMetrics, invokes the fallback. Service failures are surfaced as ServiceCallException, not as timeouts.
    • ProductCatalogService (fast) and RecommendationService (slow, interruptible): simulated dependencies.
    • App: catalog answers within its 500 ms limit; recommendations exceed their 100 ms limit, get cancelled and replaced by popular items; timeout counters are printed. Logging traces every step.
    • README.md: intent, real-world example, sequence diagram, code walkthrough, applicability, trade-offs, related patterns (including how this differs from the existing fallback module, where the time limit is only one of several triggers). PlantUML class diagram under etc/.
  • Tests: 13 JUnit 5 tests covering result within limit, fallback plus cancellation on overrun (interruption verified), failure propagation, per-service limits and counters, registry and policy validation, plus AppTest.
  • Module registered in the parent pom.xml. ./mvnw clean verify -pl timeout passes locally on JDK 21 and inside an eclipse-temurin:21 container.

Fixes #2845

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown

PR Summary

Introduces a new timeout module implementing per-service timeouts with TimeoutPolicy, TimeoutRegistry, and TimeoutExecutor. Includes a demo App wiring two simulated services (ProductCatalogService and RecommendationService), timeout metrics, and tests. Adds documentation (README and UML), module POM, and parent pom.xml update to register the module.

Changes

File Summary
pom.xml Updated parent pom.xml to include a new module timeout, enabling Maven builds for the Timeout pattern.
timeout/README.md Added README detailing the Timeout pattern, design, example, sequence, and usage guidance, including how it differs from the Fallback pattern.
timeout/etc/timeout.urm.puml Introduced PlantUML diagram representing core classes (TimeoutPolicy, TimeoutRegistry, TimeoutExecutor, TimeoutMetrics, App, services) and their relationships.
timeout/pom.xml New module POM declaring dependencies (SLF4J, Logback, JUnit) and build setup for the timeout module.
timeout/src/main/java/com/iluwatar/timeout/App.java Main demo wiring TimeoutRegistry, policies, and TimeoutExecutor to fetch products and recommendations with fallbacks and metric reporting.
timeout/src/main/java/com/ iluwatar/timeout/ProductCatalogService.java Simulated fast catalog service with configurable latency; returns product list.
timeout/src/main/java/com/iluwatar/timeout/RecommendationService.java Slow, interruptible recommendation service; computes recommendations or throws on interrupt.
timeout/src/main/java/com/iluwatar/timeout/ServiceCallException.java Custom runtime exception to surface non-timeout service failures from downstream calls.
timeout/src/main/java/com/iluwatar/timeout/TimeoutExecutor.java Executes downstream calls under timeout, cancels overdue tasks, updates metrics, and returns fallbacks or exceptions.
timeout/src/main/java/com/iluwatar/timeout/TimeoutMetrics.java Thread-safe per-service timeout counters with snapshot view for observability.
timeout/src/main/java/com/iluwatar/timeout/TimeoutPolicy.java Immutable policy describing per-service timeout with validation and millisecond factory.
timeout/src/main/java/com/iluwatar/timeout/TimeoutRegistry.java Holds per-service policies plus a default timeout; provides policy lookup.
timeout/src/test/java/com/iluwatar/timeout/AppTest.java JUnit tests verifying App behaviors: keeps within limits, shows fallbacks for overrun, and basic instantiation.
timeout/src/test/java/com/iluwatar/timeout/RecommendationServiceTest.java Tests for normal latency and interruption behavior of RecommendationService.
timeout/src/test/java/com/iluwatar/timeout/TimeoutExecutorTest.java Comprehensive tests for successful execution, timeout cancellation, failure propagation, per-service rules, and close behavior.
timeout/src/test/java/com/iluwatar/timeout/TimeoutPolicyTest.java Tests for policy creation, validation, and rejection of invalid inputs.
timeout/src/test/java/com/iluwatar/timeout/TimeoutRegistryTest.java Tests for registration, default policy, and policy replacement behavior.

autogenerated by presubmit.ai

@ylcn91 ylcn91 mentioned this pull request Sep 3, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 Pull request needs attention.

Review Summary

Commits Considered (1)
Files Processed (16)
  • pom.xml (1 hunk)
  • timeout/README.md (1 hunk)
  • timeout/etc/timeout.urm.puml (1 hunk)
  • timeout/pom.xml (1 hunk)
  • timeout/src/main/java/com/iluwatar/timeout/App.java (1 hunk)
  • timeout/src/main/java/com/iluwatar/timeout/ProductCatalogService.java (1 hunk)
  • timeout/src/main/java/com/iluwatar/timeout/RecommendationService.java (1 hunk)
  • timeout/src/main/java/com/iluwatar/timeout/ServiceCallException.java (1 hunk)
  • timeout/src/main/java/com/iluwatar/timeout/TimeoutExecutor.java (1 hunk)
  • timeout/src/main/java/com/iluwatar/timeout/TimeoutMetrics.java (1 hunk)
  • timeout/src/main/java/com/iluwatar/timeout/TimeoutPolicy.java (1 hunk)
  • timeout/src/main/java/com/iluwatar/timeout/TimeoutRegistry.java (1 hunk)
  • timeout/src/test/java/com/iluwatar/timeout/AppTest.java (1 hunk)
  • timeout/src/test/java/com/iluwatar/timeout/TimeoutExecutorTest.java (1 hunk)
  • timeout/src/test/java/com/iluwatar/timeout/TimeoutPolicyTest.java (1 hunk)
  • timeout/src/test/java/com/iluwatar/timeout/TimeoutRegistryTest.java (1 hunk)
Actionable Comments (7)
  • timeout/src/main/java/com/iluwatar/timeout/App.java [61-61]

    possible bug: "Logger name should match Lombok's @slf4j field"

  • timeout/src/main/java/com/iluwatar/timeout/App.java [67-67]

    readability: "Use the Lombok 'log' logger"

  • timeout/src/main/java/com/iluwatar/timeout/App.java [73-73]

    readability: "Use the Lombok 'log' logger"

  • timeout/src/main/java/com/iluwatar/timeout/App.java [81-81]

    readability: "Use the Lombok 'log' logger for final status"

  • timeout/src/main/java/com/iluwatar/timeout/ProductCatalogService.java [59-59]

    possible bug: "Logger field name mismatch in ProductCatalogService"

  • timeout/src/main/java/com/iluwatar/timeout/RecommendationService.java [60-66]

    possible bug: "Logger field name mismatch in RecommendationService"

  • timeout/src/main/java/com/iluwatar/timeout/TimeoutExecutor.java [82-82]

    possible bug: "Logger name mismatch in TimeoutExecutor"

Skipped Comments (1)
  • timeout/pom.xml [46-50]

    enhancement: "Add junit-jupiter-api for tests"

new TimeoutRegistry(Duration.ofMillis(300))
.register(TimeoutPolicy.of(ProductCatalogService.NAME, 500))
.register(TimeoutPolicy.of(RecommendationService.NAME, 100));
LOGGER.info("Configured per-service limits: catalog 500 ms, recommendations 100 ms");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logger instance name coming from Lombok's @slf4j is 'log', not 'LOGGER'. This will cause a compilation error. Use 'log' consistently.

var recommendations = new RecommendationService(Duration.ofMillis(400));

try (var executor = new TimeoutExecutor()) {
LOGGER.info("Calling {}", ProductCatalogService.NAME);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace the logger with the Lombok-generated field 'log'.

registry.policyFor(ProductCatalogService.NAME), catalog::fetchProducts, List::of);
LOGGER.info("Products: {}", products);

LOGGER.info("Calling {}", RecommendationService.NAME);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace the logger with the Lombok-generated field 'log'.

() -> POPULAR_ITEMS);
LOGGER.info("Recommendations shown to alice: {}", suggested);

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace the logger with the Lombok-generated field 'log'.

* @throws InterruptedException if the call is cancelled while waiting for the simulated backend
*/
public List<String> fetchProducts() throws InterruptedException {
LOGGER.info("{}: fetching products, expected latency {} ms", NAME, latency.toMillis());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace the logger with the Lombok-generated 'log' instance.

Comment on lines +60 to +66
LOGGER.info(
"{}: computing recommendations for {}, expected latency {} ms",
NAME,
customer,
latency.toMillis());
try {
Thread.sleep(latency);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace all usages of the Lombok-generated logger with the 'log' field. The multi-line log call needs to reference 'log'.

var future = executor.submit(call);
try {
var result = future.get(limitMillis, TimeUnit.MILLISECONDS);
LOGGER.info("{} responded within its {} ms limit", serviceName, limitMillis);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace the logger with the Lombok-generated 'log' instance.

@ylcn91

ylcn91 commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Note on the automated review comments: LOGGER is the Lombok logger field name configured for this repository in lombok.config (lombok.log.fieldName = LOGGER), the same name every other module uses, so the code compiles as is. Local ./mvnw clean verify -pl timeout passes on JDK 21, also inside an eclipse-temurin:21 container.

@codecov

codecov Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 83.91%. Comparing base (41625d8) to head (1b50cb4).

Additional details and impacted files
@@             Coverage Diff              @@
##             master    #3598      +/-   ##
============================================
+ Coverage     83.79%   83.91%   +0.11%     
- Complexity     4277     4311      +34     
============================================
  Files          1121     1129       +8     
  Lines         15144    15243      +99     
  Branches        723      726       +3     
============================================
+ Hits          12690    12791     +101     
+ Misses         2159     2157       -2     
  Partials        295      295              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ylcn91

ylcn91 commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up on the Codecov note: added a test for an interrupted caller in TimeoutExecutorTest and a new RecommendationServiceTest (normal reply and interrupted computation). 16 tests, no uncovered lines outside App. Tests only, no production change. ./mvnw clean verify -pl timeout passes locally on JDK 21 and in an eclipse-temurin:21 container.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 Pull request needs attention.

Review Summary

Commits Considered (1)
Files Processed (17)
  • pom.xml (1 hunk)
  • timeout/README.md (1 hunk)
  • timeout/etc/timeout.urm.puml (1 hunk)
  • timeout/pom.xml (1 hunk)
  • timeout/src/main/java/com/iluwatar/timeout/App.java (1 hunk)
  • timeout/src/main/java/com/iluwatar/timeout/ProductCatalogService.java (1 hunk)
  • timeout/src/main/java/com/iluwatar/timeout/RecommendationService.java (1 hunk)
  • timeout/src/main/java/com/iluwatar/timeout/ServiceCallException.java (1 hunk)
  • timeout/src/main/java/com/iluwatar/timeout/TimeoutExecutor.java (1 hunk)
  • timeout/src/main/java/com/iluwatar/timeout/TimeoutMetrics.java (1 hunk)
  • timeout/src/main/java/com/iluwatar/timeout/TimeoutPolicy.java (1 hunk)
  • timeout/src/main/java/com/iluwatar/timeout/TimeoutRegistry.java (1 hunk)
  • timeout/src/test/java/com/iluwatar/timeout/AppTest.java (1 hunk)
  • timeout/src/test/java/com/iluwatar/timeout/RecommendationServiceTest.java (1 hunk)
  • timeout/src/test/java/com/iluwatar/timeout/TimeoutExecutorTest.java (1 hunk)
  • timeout/src/test/java/com/iluwatar/timeout/TimeoutPolicyTest.java (1 hunk)
  • timeout/src/test/java/com/iluwatar/timeout/TimeoutRegistryTest.java (1 hunk)
Actionable Comments (3)
  • timeout/src/main/java/com/iluwatar/timeout/App.java [61-61]

    readability: "Replace Lombok-generated logger usage with 'log'."

  • timeout/src/main/java/com/iluwatar/timeout/ProductCatalogService.java [59-59]

    readability: "Use Lombok 'log' instead of 'LOGGER' in logging."

  • timeout/src/main/java/com/iluwatar/timeout/RecommendationService.java [60-64]

    readability: "Replace Lombok-generated logger usage with 'log' in multi-line logging."

Skipped Comments (0)

new TimeoutRegistry(Duration.ofMillis(300))
.register(TimeoutPolicy.of(ProductCatalogService.NAME, 500))
.register(TimeoutPolicy.of(RecommendationService.NAME, 100));
LOGGER.info("Configured per-service limits: catalog 500 ms, recommendations 100 ms");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This project uses Lombok's @slf4j which provides a 'log' field. The code currently references 'LOGGER', which will fail to compile. Replace LOGGER.info(...) with log.info(...) (and similarly for other logging calls in this file).

* @throws InterruptedException if the call is cancelled while waiting for the simulated backend
*/
public List<String> fetchProducts() throws InterruptedException {
LOGGER.info("{}: fetching products, expected latency {} ms", NAME, latency.toMillis());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace the LOGGER.info call with log.info since the class uses @slf4j. The current line will not compile as LOGGER is undefined.

Comment on lines +60 to +64
LOGGER.info(
"{}: computing recommendations for {}, expected latency {} ms",
NAME,
customer,
latency.toMillis());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace the multi-line LOGGER.info(...) logging with a single log.info(...) block using the same message and arguments. The Lombok @slf4j field is named 'log'.

@ylcn91

ylcn91 commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Coverage follow-up: extracted the two demo calls in App into package-private helpers (loadProducts, loadRecommendations) so both the normal and the fallback paths are exercised by AppTest, and added shouldBeInstantiable. Demo output unchanged. JaCoCo now reports 100% instruction, branch and line coverage. Verified locally on JDK 21 and in an eclipse-temurin:21 container; the packaged jar runs end to end.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

Review Summary

Commits Considered (1)
Files Processed (17)
  • pom.xml (1 hunk)
  • timeout/README.md (1 hunk)
  • timeout/etc/timeout.urm.puml (1 hunk)
  • timeout/pom.xml (1 hunk)
  • timeout/src/main/java/com/iluwatar/timeout/App.java (1 hunk)
  • timeout/src/main/java/com/iluwatar/timeout/ProductCatalogService.java (1 hunk)
  • timeout/src/main/java/com/iluwatar/timeout/RecommendationService.java (1 hunk)
  • timeout/src/main/java/com/iluwatar/timeout/ServiceCallException.java (1 hunk)
  • timeout/src/main/java/com/iluwatar/timeout/TimeoutExecutor.java (1 hunk)
  • timeout/src/main/java/com/iluwatar/timeout/TimeoutMetrics.java (1 hunk)
  • timeout/src/main/java/com/iluwatar/timeout/TimeoutPolicy.java (1 hunk)
  • timeout/src/main/java/com/iluwatar/timeout/TimeoutRegistry.java (1 hunk)
  • timeout/src/test/java/com/iluwatar/timeout/AppTest.java (1 hunk)
  • timeout/src/test/java/com/iluwatar/timeout/RecommendationServiceTest.java (1 hunk)
  • timeout/src/test/java/com/iluwatar/timeout/TimeoutExecutorTest.java (1 hunk)
  • timeout/src/test/java/com/iluwatar/timeout/TimeoutPolicyTest.java (1 hunk)
  • timeout/src/test/java/com/iluwatar/timeout/TimeoutRegistryTest.java (1 hunk)
Actionable Comments (0)
Skipped Comments (8)
  • timeout/src/main/java/com/iluwatar/timeout/App.java [61-61]

    best_practice: "Use Lombok's generated logger"

  • timeout/src/main/java/com/iluwatar/timeout/App.java [67-67]

    best_practice: "Use Lombok's generated logger"

  • timeout/src/main/java/com/iluwatar/timeout/App.java [73-73]

    best_practice: "Use Lombok's generated logger"

  • timeout/src/main/java/com/iluwatar/timeout/App.java [75-75]

    best_practice: "Use Lombok's generated logger"

  • timeout/src/main/java/com/iluwatar/timeout/ProductCatalogService.java [59-59]

    best_practice: "Use Lombok's generated logger"

  • timeout/src/main/java/com/iluwatar/timeout/RecommendationService.java [60-66]

    best_practice: "Use Lombok's generated logger"

  • timeout/src/main/java/com/iluwatar/timeout/TimeoutExecutor.java [82-82]

    best_practice: "Use Lombok's generated logger"

  • timeout/src/main/java/com/iluwatar/timeout/TimeoutExecutor.java [88-90]

    best_practice: "Use Lombok's generated logger"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Timeout pattern

1 participant