Skip to content

feat: add Scatter-Gather pattern (#3577) - #3601

Open
ylcn91 wants to merge 1 commit into
iluwatar:masterfrom
ylcn91:feat/scatter-gather
Open

feat: add Scatter-Gather pattern (#3577)#3601
ylcn91 wants to merge 1 commit into
iluwatar:masterfrom
ylcn91:feat/scatter-gather

Conversation

@ylcn91

@ylcn91 ylcn91 commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Adds the Scatter-Gather pattern as a new scatter-gather module.

  • Problem: a client request often needs answers from several independent providers; calling them one after another adds up their latencies, and one slow or failing provider should not block the whole answer.
  • Solution: the same request is scattered to every provider concurrently, replies are gathered until each has arrived, failed or exceeded the timeout, and the successful replies are aggregated into one result. Partial results are first class.
  • Key components:
    • RateRequest, RateQuote, RateProvider: a travel site asking several hotel rate providers for the same stay.
    • InMemoryRateProvider (fast), DelayedRateProvider (slow, interrupt-safe), FailingRateProvider.
    • ScatterGather: scatter() (one CompletableFuture per provider with orTimeout), gather() (waits for all to settle, keeps successes, logs timeouts and failures), scatterGather() chaining both with an Aggregator; AutoCloseable.
    • Aggregator: functional reduction of the gathered replies, with a cheapestQuote() factory.
    • App: four providers (two fast, one slower than the 300 ms timeout, one failing); logs the scatter, gather and aggregate phases and the best offer.
    • README.md: intent, real-world example, sequence diagram, code walkthrough, applicability, trade-offs, related patterns including an explicit note on how Scatter-Gather differs from the existing Fan-Out/Fan-In module. PlantUML class diagram under etc/.
  • Tests: 13 JUnit 5 tests covering all providers replying, a slow provider dropped after the timeout while others are kept, a failing provider dropped, aggregation, empty result when nobody answers, executor shutdown, plus AppTest.
  • Module registered in the parent pom.xml. ./mvnw clean verify -pl scatter-gather passes locally on JDK 21 and inside an eclipse-temurin:21 container.

Note: PR #3590 also targets this issue. This implementation was written independently with a different domain and design; it includes the populated, logging App entry point requested in the review of #3590 and treats partial results and timeouts as the core of the pattern. Happy to align with whichever direction the maintainers prefer.

Fixes #3577

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown

Analyzing changes in this PR...

This might take a few minutes, please wait

📥 Commits

Analyzing changes from base (41625d8) to latest commit (c4563e9):

📁 Files being considered (17)

🔄 pom.xml (1 hunk)
➕ scatter-gather/README.md (1 hunk)
➕ scatter-gather/etc/scatter-gather.urm.puml (1 hunk)
➕ scatter-gather/pom.xml (1 hunk)
➕ scatter-gather/src/main/java/com/iluwatar/scattergather/Aggregator.java (1 hunk)
➕ scatter-gather/src/main/java/com/iluwatar/scattergather/App.java (1 hunk)
➕ scatter-gather/src/main/java/com/iluwatar/scattergather/DelayedRateProvider.java (1 hunk)
➕ scatter-gather/src/main/java/com/iluwatar/scattergather/FailingRateProvider.java (1 hunk)
➕ scatter-gather/src/main/java/com/iluwatar/scattergather/InMemoryRateProvider.java (1 hunk)
➕ scatter-gather/src/main/java/com/iluwatar/scattergather/RateProvider.java (1 hunk)
➕ scatter-gather/src/main/java/com/iluwatar/scattergather/RateQuote.java (1 hunk)
➕ scatter-gather/src/main/java/com/iluwatar/scattergather/RateRequest.java (1 hunk)
➕ scatter-gather/src/main/java/com/iluwatar/scattergather/ScatterGather.java (1 hunk)
➕ scatter-gather/src/test/java/com/iluwatar/scattergather/AggregatorTest.java (1 hunk)
➕ scatter-gather/src/test/java/com/iluwatar/scattergather/AppTest.java (1 hunk)
➕ scatter-gather/src/test/java/com/iluwatar/scattergather/RateProviderTest.java (1 hunk)
➕ scatter-gather/src/test/java/com/iluwatar/scattergather/ScatterGatherTest.java (1 hunk)


autogenerated by presubmit.ai

@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)
  • scatter-gather/README.md (1 hunk)
  • scatter-gather/etc/scatter-gather.urm.puml (1 hunk)
  • scatter-gather/pom.xml (1 hunk)
  • scatter-gather/src/main/java/com/iluwatar/scattergather/Aggregator.java (1 hunk)
  • scatter-gather/src/main/java/com/iluwatar/scattergather/App.java (1 hunk)
  • scatter-gather/src/main/java/com/iluwatar/scattergather/DelayedRateProvider.java (1 hunk)
  • scatter-gather/src/main/java/com/iluwatar/scattergather/FailingRateProvider.java (1 hunk)
  • scatter-gather/src/main/java/com/iluwatar/scattergather/InMemoryRateProvider.java (1 hunk)
  • scatter-gather/src/main/java/com/iluwatar/scattergather/RateProvider.java (1 hunk)
  • scatter-gather/src/main/java/com/iluwatar/scattergather/RateQuote.java (1 hunk)
  • scatter-gather/src/main/java/com/iluwatar/scattergather/RateRequest.java (1 hunk)
  • scatter-gather/src/main/java/com/iluwatar/scattergather/ScatterGather.java (1 hunk)
  • scatter-gather/src/test/java/com/iluwatar/scattergather/AggregatorTest.java (1 hunk)
  • scatter-gather/src/test/java/com/iluwatar/scattergather/AppTest.java (1 hunk)
  • scatter-gather/src/test/java/com/iluwatar/scattergather/RateProviderTest.java (1 hunk)
  • scatter-gather/src/test/java/com/iluwatar/scattergather/ScatterGatherTest.java (1 hunk)
Actionable Comments (6)
  • scatter-gather/src/main/java/com/iluwatar/scattergather/App.java [66-66]

    bug: "Logger instance naming mismatch with Lombok"

  • scatter-gather/src/main/java/com/iluwatar/scattergather/App.java [69-69]

    bug: "Logger usage mismatch in gather log line"

  • scatter-gather/src/main/java/com/iluwatar/scattergather/App.java [72-72]

    bug: "Logger usage mismatch in aggregate log line"

  • scatter-gather/src/main/java/com/iluwatar/scattergather/DelayedRateProvider.java [60-60]

    bug: "Incorrect duration handling in Delay provider"

  • scatter-gather/src/main/java/com/iluwatar/scattergather/InMemoryRateProvider.java [56-56]

    bug: "Logger naming inconsistency in InMemoryRateProvider"

  • scatter-gather/src/main/java/com/iluwatar/scattergather/ScatterGather.java [83-87]

    bug: "Logger naming mismatch in ScatterGather.scatter"

Skipped Comments (0)


try (var scatterGather =
new ScatterGather(Executors.newFixedThreadPool(providers.size()), Duration.ofMillis(300))) {
LOGGER.info("Scatter phase: broadcasting the same request to every provider");

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 'LOGGER' with 'log' to align with Lombok @slf4j; 'log' is the generated logger name. Using 'LOGGER' will fail to compile.

LOGGER.info("Scatter phase: broadcasting the same request to every provider");
var pending = scatterGather.scatter(request, providers);

LOGGER.info("Gather phase: collecting replies that arrive within the timeout");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Same logger name issue as above; consider using 'log'.

LOGGER.info("Gather phase: collecting replies that arrive within the timeout");
var quotes = scatterGather.gather(pending);

LOGGER.info("Aggregate phase: choosing the cheapest of {} quotes", quotes.size());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Same: replace with 'log' to align with Lombok-generated logger.

public RateQuote quote(RateRequest request) {
LOGGER.info("{} is slow and will need {} ms to answer", name(), delay.toMillis());
try {
Thread.sleep(delay);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thread.sleep expects a long value in milliseconds. Use delay.toMillis() to convert Duration to milliseconds.

@Override
public RateQuote quote(RateRequest request) {
var total = nightlyRate.multiply(BigDecimal.valueOf(request.nights()));
LOGGER.info("{} quotes {} for {} nights in {}", name, total, request.nights(), request.city());

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 usage mismatch; should be 'log.info(...)' to match Lombok @slf4j. This will fail compilation.

Comment on lines +83 to +87
LOGGER.info(
"Scattering request for {} nights in {} to {} providers",
request.nights(),
request.city(),
providers.size());

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 'LOGGER' with 'log' to align with Lombok @slf4j; use the generated logger field name. The multi-line snippet shows the exact log call that should be updated.

@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. Thread.sleep(Duration) is the Java 19+ overload, valid on the project's Java 21 baseline. Local ./mvnw clean verify -pl scatter-gather 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 (c4563e9).

Additional details and impacted files
@@             Coverage Diff              @@
##             master    #3601      +/-   ##
============================================
+ Coverage     83.79%   83.91%   +0.12%     
- Complexity     4277     4312      +35     
============================================
  Files          1121     1129       +8     
  Lines         15144    15240      +96     
  Branches        723      728       +5     
============================================
+ Hits          12690    12789      +99     
+ Misses         2159     2155       -4     
- Partials        295      296       +1     

☔ 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 force-pushed the feat/scatter-gather branch from 31d348c to 2df15af Compare September 3, 2026 09:52
@ylcn91

ylcn91 commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up on the Codecov note: added two ScatterGatherTest cases for close() (interrupted caller keeps the interrupt flag; a task that ignores interrupts does not block shutdown). 15 tests, ScatterGather fully covered; the remaining uncovered lines are the implicit App constructor and the demo's empty-result lambda. Tests only. ./mvnw clean verify -pl scatter-gather passes locally on JDK 21 and in an eclipse-temurin:21 container.

@ylcn91
ylcn91 force-pushed the feat/scatter-gather branch from 2df15af to c4563e9 Compare September 3, 2026 11:32
@ylcn91

ylcn91 commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Coverage follow-up: moved the result reporting in App into a package-private reportBestOffer helper so the empty-result branch is covered, 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.

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.

[Feature Request]: Add Scatter-Gather Design Pattern

1 participant