Skip to content

xDS retry now correctly re-selects endpoints on each retry attempt. - #6798

Merged
jrhee17 merged 4 commits into
line:mainfrom
jrhee17:bugfix/xds-retry
Jun 10, 2026
Merged

xDS retry now correctly re-selects endpoints on each retry attempt.#6798
jrhee17 merged 4 commits into
line:mainfrom
jrhee17:bugfix/xds-retry

Conversation

@jrhee17

@jrhee17 jrhee17 commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Motivation:

xDS retry was broken because RetryingClient was built as part of the upstream ClientDecoration inside FilterUtil.buildUpstreamFilter(). This placed the retry decorator after endpoint selection, so retries always reused the same endpoint instead of re-selecting. The fix separates retry from upstream filters and places it before the cluster decorator chain, so each retry attempt goes through endpoint selection again.

Additionally, the cluster-level preprocessor, decorator, and retry logic were scattered across ClusterFilter, RouteEntry, and RouterFilter, making the execution order hard to follow.

Modifications:

  • Separated retry decoration from upstream filter building into FilterUtil.buildRetryDecoration() so retry wraps the full cluster chain in RouteEntry:
    [retry] -> [cluster decorator] -> [upstream decorators] -> [delegate]
  • Added ClusterFilterFactory which consolidates cluster-level preprocessor and decorator logic into a single class with a clear execution order.
  • Replaced ClusterSnapshot.hasPreprocessor() / executePreprocessor() with typed, non-null preprocessor()HttpPreprocessor and rpcPreprocessor()RpcPreprocessor.
  • Refactored RouterFilter to accept a Function<ClusterSnapshot, Preprocessor> mapping, wired via ClusterSnapshot::preprocessor / ClusterSnapshot::rpcPreprocessor in RouterFilterFactory.
  • Updated GrpcServicesPreprocessor to use snapshot.preprocessor() instead of manual endpoint selection and TLS setup.
  • Moved SELECTED_ROUTE attribute key to XdsCommonUtil for cross-package accessibility.
  • Removed ClusterFilter, XdsCommonUtil.setTlsParams(), ClusterSnapshot.endpointGroup(), and ClusterSnapshot.sessionProtocol().

Result:

  • xDS retry now correctly re-selects endpoints on each retry attempt.
  • Cluster-level filter pipeline is consolidated in ClusterFilterFactory:
    [downstream preprocessors]
        -> [router preprocessor]
            -> [cluster preprocessor]
                -> [endpoint selection]
                    -> [retry decorator]
                        -> [cluster decorator]
                            -> [upstream decorators]
    
  • ClusterSnapshot.preprocessor() and ClusterSnapshot.rpcPreprocessor() provide typed, non-null accessors enabling direct use like WebClient.of(clusterSnapshot.preprocessor()).

@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Refactors xDS routing and preprocessing: adds ClusterFilterFactory for per-cluster preprocessing and TLS injection, exposes cluster preprocessors, pre-builds route client chains with optional retry decoration, migrates SELECTED_ROUTE to XdsCommonUtil, and updates tests to expect EmptyEndpointGroupException.

Changes

XDS Request Preprocessing Pipeline Refactoring

Layer / File(s) Summary
ClusterFilterFactory — per-cluster preprocessing core
xds/src/main/java/com/linecorp/armeria/xds/ClusterFilterFactory.java
New factory encapsulating endpoint-group validation, session-protocol resolution, delegate-client customization, and per-endpoint TLS injection for HTTP and RPC preprocessors.
ClusterSnapshot preprocessor exposure
xds/src/main/java/com/linecorp/armeria/xds/ClusterSnapshot.java
Stores HTTP/RPC preprocessor instances created by ClusterFilterFactory and exposes them via public preprocessor()/rpcPreprocessor() @UnstableApi accessors.
FilterUtil retry decoration separation
xds/src/main/java/com/linecorp/armeria/xds/FilterUtil.java
Removes RetryPolicy from buildUpstreamFilter signature; constructs upstream decoration from filters only and adds buildRetryDecoration to produce retry decoration separately.
RouteEntry pre-built client chains & RouteStream retry threading
xds/src/main/java/com/linecorp/armeria/xds/RouteEntry.java, xds/src/main/java/com/linecorp/armeria/xds/RouteStream.java
RouteEntry accepts optional retryDecoration, pre-builds httpClient()/rpcClient() chains wrapping delegates with ClusterFilterFactory.DECORATION, and removes per-request applyUpstreamFilter. RouteStream computes and passes retryDecoration from effectiveRetryPolicy.
RouterFilter simplification & factory binding
xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/RouterFilter.java, xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/RouterFilterFactory.java
RouterFilter selects the stored RouteEntry from context, applies response timeout, and delegates to the ClusterSnapshot-provided preprocessor (HTTP vs RPC). RouterFilterFactory constructs http/rpc filters with explicit boolean config.
GrpcServicesPreprocessor delegation
xds/src/main/java/com/linecorp/armeria/xds/GrpcServicesPreprocessor.java
Delegates preprocessing to snapshot.preprocessor().execute(...) instead of performing inline endpoint selection/TLS setup; unused imports removed.
SELECTED_ROUTE attribute migration & XdsCommonUtil cleanup
xds/src/main/java/com/linecorp/armeria/xds/internal/XdsCommonUtil.java, xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/XdsHttpPreprocessor.java, xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/XdsRpcPreprocessor.java, xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/XdsAttributeKeys.java
Introduces XdsCommonUtil.SELECTED_ROUTE, updates preprocessors to use it, removes XdsAttributeKeys.SELECTED_ROUTE, and removes setTlsParams from XdsCommonUtil (TLS logic moved to ClusterFilterFactory).
XdsEndpointGroup lifecycle refactoring
xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/XdsEndpointGroup.java
Adds of(XdsLoadBalancer) factory, removes of(ClusterSnapshot), stores nullable listenerRoot and per-instance selectionTimeoutMillis, and updates closeAsync()/selectionTimeoutMillis() behavior.
ClusterStream endpoint snapshot typing & error behavior
xds/src/main/java/com/linecorp/armeria/xds/ClusterStream.java
Changes endpoint snapshot stream from Optional<EndpointSnapshot> to EndpointSnapshot, updates LoadBalancerInput and EndpointSnapshotNode contracts, and emits an error when neither load_assignment nor eds_cluster_config is specified.
Test updates for new preprocessing behavior
it/xds-client/src/test/java/com/linecorp/armeria/xds/it/PreprocessorErrorTest.java, it/xds-client/src/test/java/com/linecorp/armeria/xds/it/RetryTest.java, xds/src/test/java/com/linecorp/armeria/xds/client/endpoint/RouteMetadataSubsetTest.java, xds/src/test/java/com/linecorp/armeria/xds/XdsTestUtil.java
Tests now assert EmptyEndpointGroupException for empty-selection scenarios; RetryTest adds a multi-endpoint bootstrap and retrySelectsDifferentEndpoints() to verify retries use multiple endpoints; XdsTestUtil.pollLoadBalancer simplified.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • line/armeria#6785: Related credential-injector and upstream filter/preprocessor wiring changes that overlap with this refactor.
  • line/armeria#6730: Changes to xDS TLS/ALPN plumbing that intersect with ClusterFilterFactory TLS handling.
  • line/armeria#6610: Earlier xDS stream/refactor work with related modifications to routing/preprocessing classes.

Suggested reviewers

  • trustin
  • ikhoon
  • minwoox

Poem

🐇 I nibble at routes and stitch the thread,
Clusters prep TLS before the request is fed.
Retry hops scatter across ports four,
Selected routes now live in one common store.
Hooray — the pipeline hums ahead!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.64% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main objective of the changeset: fixing xDS retry to correctly re-select endpoints on each retry attempt, which is the core motivation and result of all modifications.
Description check ✅ Passed The description clearly explains the motivation, modifications, and results of the changeset, with detailed explanations of the retry fix and architectural improvements across multiple files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@jrhee17 jrhee17 changed the title Add typed preprocessor accessors to ClusterSnapshot xDS retries correctly select a new endpoint on retry Jun 8, 2026
@jrhee17 jrhee17 added the defect label Jun 8, 2026
@jrhee17 jrhee17 added this to the 1.40.0 milestone Jun 8, 2026
@jrhee17 jrhee17 changed the title xDS retries correctly select a new endpoint on retry xDS retry now correctly re-selects endpoints on each retry attempt. Jun 8, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/XdsEndpointGroup.java`:
- Around line 90-92: The public factory method
XdsEndpointGroup.of(XdsLoadBalancer loadBalancer) must validate its parameter:
add an explicit Objects.requireNonNull(loadBalancer, "loadBalancer") at the
start of the of(...) method to fail fast with a clear message rather than
allowing later NPEs in the XdsEndpointGroup constructor; update imports if
needed and leave the constructor unchanged (refer to XdsEndpointGroup.of and the
XdsLoadBalancer parameter).

In `@xds/src/main/java/com/linecorp/armeria/xds/RouteEntry.java`:
- Around line 147-148: Add an explicit null-check at the start of the public
method matches(ClientRequestContext ctx) in class RouteEntry by calling
Objects.requireNonNull(ctx, "ctx") before using matcher.matches(ctx); also
ensure java.util.Objects is imported if not already so the method fails fast
with a clear NPE message rather than relying on matcher.matches to throw later.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e963e58a-98e0-4792-a92d-83dcacc99076

📥 Commits

Reviewing files that changed from the base of the PR and between 423d994 and f4e173b.

📒 Files selected for processing (16)
  • it/xds-client/src/test/java/com/linecorp/armeria/xds/it/PreprocessorErrorTest.java
  • it/xds-client/src/test/java/com/linecorp/armeria/xds/it/RetryTest.java
  • xds/src/main/java/com/linecorp/armeria/xds/ClusterFilterFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/ClusterSnapshot.java
  • xds/src/main/java/com/linecorp/armeria/xds/FilterUtil.java
  • xds/src/main/java/com/linecorp/armeria/xds/GrpcServicesPreprocessor.java
  • xds/src/main/java/com/linecorp/armeria/xds/RouteEntry.java
  • xds/src/main/java/com/linecorp/armeria/xds/RouteStream.java
  • xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/RouterFilter.java
  • xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/RouterFilterFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/XdsAttributeKeys.java
  • xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/XdsEndpointGroup.java
  • xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/XdsHttpPreprocessor.java
  • xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/XdsRpcPreprocessor.java
  • xds/src/main/java/com/linecorp/armeria/xds/internal/XdsCommonUtil.java
  • xds/src/test/java/com/linecorp/armeria/xds/client/endpoint/RouteMetadataSubsetTest.java
💤 Files with no reviewable changes (1)
  • xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/XdsAttributeKeys.java

Comment thread xds/src/main/java/com/linecorp/armeria/xds/RouteEntry.java
@jrhee17
jrhee17 marked this pull request as ready for review June 8, 2026 06:02
@codecov

codecov Bot commented Jun 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 0.00%. Comparing base (8150425) to head (281694f).
⚠️ Report is 482 commits behind head on main.

Additional details and impacted files
@@             Coverage Diff              @@
##               main   #6798       +/-   ##
============================================
- Coverage     74.46%       0   -74.47%     
============================================
  Files          1963       0     -1963     
  Lines         82437       0    -82437     
  Branches      10764       0    -10764     
============================================
- Hits          61385       0    -61385     
+ Misses        15918       0    -15918     
+ Partials       5134       0     -5134     

☔ 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.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@minwoox minwoox left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

👍 👍


final class RouterFilter<I extends Request, O extends Response> implements Preprocessor<I, O> {

private static final AttributeKey<RouteEntry> SELECTED_ROUTE = XdsCommonUtil.SELECTED_ROUTE;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: I think we can just inline it.

private <I extends Request, O extends Response> O execute(
PreClient<I, O> delegate, PreClientRequestContext ctx, I req) throws Exception {
if (endpointGroup == null) {
throw UnprocessedRequestException.of(new IllegalStateException(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can't we throw the exception in the ctor?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Modified s.t. we enforce either a load_assignment or eds_cluster_config is specified at 3bd9524

@ikhoon ikhoon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

👍👍

XdsCommonUtil.setTlsParams(ctx, endpoint);
ctx.setEndpointGroup(endpoint);
return delegate.execute(ctx, req);
return preprocessorMapper.apply(clusterSnapshot).execute(delegate, ctx, req);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Optional) If the preprocessor is just obtained from the clusterSnapshot, should we take a flag for simpilicy?

Preprocessor<...,...> preprocessor;
if (isRpc) {
   preprocessor = clusterSnapshot.rpcPreprocessor();
} else {
   preprocessor = clusterSnapshot.preprocessor();
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done with an unsafe cast

final ClusterFilterFactory factory = new ClusterFilterFactory(
clusterXdsResource, this.loadBalancer, transportSocket);
httpPreprocessor = factory.httpPreprocessor();
rpcPreprocessor = factory.rpcPreprocessor();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Question) Don't we need to update toString(), hashCode() and equals()?
Nit) Since hashCode() and equals() are usually changed together, it would be better to place them together.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

httpPreprocessor, rpcPreprocessor are derived fields - I'm not sure if it's necessary to add these to equality/hashcode checks.

Added httpPreprocessor, rpcPreprocessor to debugString

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
xds/src/test/java/com/linecorp/armeria/xds/XdsTestUtil.java (1)

2-5: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update the Java file header to the required LY Corporation form.

This touched Java file still uses LINE Corporation in the copyright header.

Suggested patch
- * Copyright 2025 LINE Corporation
+ * Copyright 2025 LY Corporation
...
- * LINE Corporation licenses this file to you under the Apache License,
+ * LY Corporation licenses this file to you under the Apache License,

As per coding guidelines, "For any Java changes in this PR: add/keep the required LY Corporation copyright header (don’t churn the year on every file)."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@xds/src/test/java/com/linecorp/armeria/xds/XdsTestUtil.java` around lines 2 -
5, The file header in XdsTestUtil.java still uses "LINE Corporation"; update the
top-of-file Java header to the required "LY Corporation" form while preserving
the existing year and license text (do not change the year). Locate the header
comment above the class XdsTestUtil and replace occurrences of "LINE
Corporation" with "LY Corporation" so the file complies with the project's
copyright header guideline.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@xds/src/test/java/com/linecorp/armeria/xds/XdsTestUtil.java`:
- Around line 2-5: The file header in XdsTestUtil.java still uses "LINE
Corporation"; update the top-of-file Java header to the required "LY
Corporation" form while preserving the existing year and license text (do not
change the year). Locate the header comment above the class XdsTestUtil and
replace occurrences of "LINE Corporation" with "LY Corporation" so the file
complies with the project's copyright header guideline.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: bfa520a6-9467-42e0-b50d-4e3684527efc

📥 Commits

Reviewing files that changed from the base of the PR and between 8f4419e and 3bd9524.

📒 Files selected for processing (5)
  • xds/src/main/java/com/linecorp/armeria/xds/ClusterFilterFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/ClusterSnapshot.java
  • xds/src/main/java/com/linecorp/armeria/xds/ClusterStream.java
  • xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/XdsEndpointGroup.java
  • xds/src/test/java/com/linecorp/armeria/xds/XdsTestUtil.java
💤 Files with no reviewable changes (1)
  • xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/XdsEndpointGroup.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • xds/src/main/java/com/linecorp/armeria/xds/ClusterSnapshot.java

@jrhee17
jrhee17 merged commit e0d43b3 into line:main Jun 10, 2026
16 of 17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants