Skip to content

Conversation

@ikhoon
Copy link
Contributor

@ikhoon ikhoon commented Dec 3, 2025

Motivation:

As the initialize() method of AbstractEndpointSelector may access member fields of subclasses, initialize() couldn't be called in the constructor of AbstractEndpointSelector.

Although the requirement is stated in the Javadoc, many users overlooked calling initialize() in their implementations.

I propose adding a fallback logic that automatically initializes AbstractEndpointSelector if it hasn't been initialized by the time the first endpoint selection occurs, so users are protected from implementation mistakes.

Modifications:

  • Try to initialize AbstractEndpointSelector if it hasn't been initialized when select(...) method is called.
  • Breaking) A subclass of AbstractEndpointSelector should implement doSelectNow(ctx) instead of selectNow(ctx), which is now a final method.

Result:

Make AbstractEndpointSelector auto-initialize to avoid user-side implementation errors.

Motivation:

The `initialize()` method of `AbstractEndpointSelector` may access
member fields of subclasses, `initialize()` couldn't be called in the
constructor of `AbstractEndpointSelector`.

Although the requirement is stated in the Javadoc, many users overlooked
calling `initialize()` in their implementations.

I propose adding a fallback logic that automatically initializes
`AbstractEndpointSelector` if it hasn't been intialized by the time the
first enpoint selection occurs, so users are protected from
implmentation mistakes.

Modifications:

- Try to initialize `AbstractEndpointSelector` if it hasn't been
  initialized when `select(...)` method is called.

Result:

Make `AbstractEndpointSelector` auto-initialize to avoid user-side
implementation errors.
@ikhoon ikhoon added this to the 1.35.0 milestone Dec 3, 2025
@ikhoon ikhoon requested a review from jrhee17 as a code owner December 3, 2025 04:05
@coderabbitai
Copy link

coderabbitai bot commented Dec 3, 2025

Walkthrough

Refactors EndpointSelector initialization from eager (constructor) to thread-safe lazy initialization using an AtomicIntegerFieldUpdater; adds selectNow() public entry and protected doSelectNow() for immediate selection; removes initialize() calls from subclasses and updates tests to assert initialization before/after first selection.

Changes

Cohort / File(s) Summary
Core lazy initialization
core/.../AbstractEndpointSelector.java
Adds volatile int initialized and AtomicIntegerFieldUpdater plus tryInitialize(); moves listener registration to tryInitialize(); initialize() delegates to tryInitialize(); adds public final @nullable Endpoint selectNow(ClientRequestContext) and protected abstract Endpoint doSelectNow(ClientRequestContext); adds @VisibleForTesting boolean isInitialized().
Implementations (deferred init)
core/.../DefaultEndpointSelector.java
Removes constructor initialize() call; implements doSelectNow(ClientRequestContext) (renamed from selectNow) to perform immediate selection.
XDS implementation update
xds/.../XdsEndpointGroup.java
Removes constructor initialize() call in XdsEndpointGroupSelector; renames selectNow(...) override to doSelectNow(...).
Tests (adjusted for lazy init)
core/.../ClientRequestContextDelayedInitTest.java,
core/.../AbstractEndpointSelectorTest.java
Remove eager initialize() calls from test fixtures; rename test overrides from selectNow(...) to doSelectNow(...); add assertions verifying isInitialized() before and after first selection where applicable.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Pay special attention to the CAS logic in tryInitialize() and the correctness of initialized/updater usage.
  • Verify select() and new selectNow() both reliably trigger initialization and that listener registration cannot occur multiple times.
  • Confirm all subclasses and tests were updated to implement/override doSelectNow() and that no stray initialize() calls remain.

Poem

🐰 I waited in a burrow, patient and small,
Until the first select gave the clarion call.
A tiny CAS hop, one-time and bright,
Listeners register, everything's right.
Hooray for lazy init—quiet, but tall! 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the main change: adding automatic initialization to AbstractEndpointSelector.
Description check ✅ Passed The pull request description directly addresses the changeset by explaining the motivation for auto-initialization, the specific modifications made, and the expected result.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (4)
xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/XdsEndpointGroup.java (1)

254-262: Consider changing visibility to protected.

The doSelectNow method in AbstractEndpointSelector is declared as protected abstract. While Java allows widening visibility when overriding, keeping it protected aligns with the design intent where doSelectNow is an internal hook called by the public selectNow() method.

         @Override
         @Nullable
-        public Endpoint doSelectNow(ClientRequestContext ctx) {
+        protected Endpoint doSelectNow(ClientRequestContext ctx) {
             final XdsLoadBalancer loadBalancer = XdsEndpointGroup.this.loadBalancer;
             if (loadBalancer == null) {
                 return null;
             }
             return loadBalancer.selectNow(ctx);
         }
core/src/test/java/com/linecorp/armeria/client/endpoint/AbstractEndpointSelectorTest.java (1)

136-146: Consider using protected visibility for doSelectNow.

The parent class declares doSelectNow as protected abstract. For consistency, the override should also be protected.

     private static AbstractEndpointSelector newSelector(EndpointGroup endpointGroup) {
         return new AbstractEndpointSelector(endpointGroup) {

             @Nullable
             @Override
-            public Endpoint doSelectNow(ClientRequestContext ctx) {
+            protected Endpoint doSelectNow(ClientRequestContext ctx) {
                 final List<Endpoint> endpoints = endpointGroup.endpoints();
                 return endpoints.isEmpty() ? null : endpoints.get(0);
             }
         };
     }
core/src/main/java/com/linecorp/armeria/client/endpoint/DefaultEndpointSelector.java (1)

70-78: Consider changing visibility to protected.

The doSelectNow method in AbstractEndpointSelector is declared as protected abstract. For consistency and to reflect that this is an internal implementation hook, the override should use protected visibility.

     @Nullable
     @Override
-    public Endpoint doSelectNow(ClientRequestContext ctx) {
+    protected Endpoint doSelectNow(ClientRequestContext ctx) {
         final T loadBalancer = this.loadBalancer;
         if (loadBalancer == null) {
             return null;
         }
         return loadBalancer.pick(ctx);
     }
core/src/test/java/com/linecorp/armeria/client/ClientRequestContextDelayedInitTest.java (1)

94-102: Consider changing visibility to protected.

For consistency with the protected abstract declaration in AbstractEndpointSelector, the override should use protected visibility.

                 @Nullable
                 @Override
-                public Endpoint doSelectNow(ClientRequestContext ctx) {
+                protected Endpoint doSelectNow(ClientRequestContext ctx) {
                     if (counter.getAndIncrement() >= failAfter) {
                         throw cause;
                     }
                     return null;
                 }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 96c8414 and afa61ca.

📒 Files selected for processing (5)
  • core/src/main/java/com/linecorp/armeria/client/endpoint/AbstractEndpointSelector.java (3 hunks)
  • core/src/main/java/com/linecorp/armeria/client/endpoint/DefaultEndpointSelector.java (1 hunks)
  • core/src/test/java/com/linecorp/armeria/client/ClientRequestContextDelayedInitTest.java (1 hunks)
  • core/src/test/java/com/linecorp/armeria/client/endpoint/AbstractEndpointSelectorTest.java (3 hunks)
  • xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/XdsEndpointGroup.java (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.java

⚙️ CodeRabbit configuration file

**/*.java: - The primary coding conventions and style guide for this project are defined in site/src/pages/community/developer-guide.mdx. Please strictly adhere to this file as the ultimate source of truth for all style and convention-related feedback.

2. Specific check for @UnstableApi

  • Review all newly added public classes and methods to ensure they have the @UnstableApi annotation.
  • However, this annotation is NOT required under the following conditions:
    • If the class or method is located in a package containing .internal.
    • If a public method is part of a class that is already annotated with @UnstableApi.

Files:

  • core/src/main/java/com/linecorp/armeria/client/endpoint/DefaultEndpointSelector.java
  • core/src/test/java/com/linecorp/armeria/client/endpoint/AbstractEndpointSelectorTest.java
  • core/src/test/java/com/linecorp/armeria/client/ClientRequestContextDelayedInitTest.java
  • core/src/main/java/com/linecorp/armeria/client/endpoint/AbstractEndpointSelector.java
  • xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/XdsEndpointGroup.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (14)
  • GitHub Check: build-ubicloud-standard-16-jdk-21-snapshot-blockhound
  • GitHub Check: build-ubicloud-standard-16-jdk-17-min-java-11
  • GitHub Check: build-ubicloud-standard-16-jdk-17-min-java-17-coverage
  • GitHub Check: build-ubicloud-standard-16-jdk-17-leak
  • GitHub Check: build-ubicloud-standard-16-jdk-11
  • GitHub Check: build-windows-latest-jdk-25
  • GitHub Check: build-ubicloud-standard-16-jdk-8
  • GitHub Check: build-ubicloud-standard-16-jdk-25
  • GitHub Check: build-macos-latest-jdk-25
  • GitHub Check: lint
  • GitHub Check: site
  • GitHub Check: flaky-tests
  • GitHub Check: Kubernetes Chaos test
  • GitHub Check: Summary
🔇 Additional comments (6)
core/src/test/java/com/linecorp/armeria/client/endpoint/AbstractEndpointSelectorTest.java (2)

49-55: Good test coverage for lazy initialization.

The assertions properly verify that the selector is not initialized before the first selection and becomes initialized afterward. This validates the core lazy initialization behavior.


62-73: Correct verification of initialization timing for delayed selection.

The test ensures that initialization occurs during select() even when no endpoint is immediately available, which is the expected behavior.

core/src/main/java/com/linecorp/armeria/client/endpoint/AbstractEndpointSelector.java (4)

42-48: Thread-safe lazy initialization implementation looks correct.

Using AtomicIntegerFieldUpdater with a volatile field and CAS ensures safe one-time initialization across concurrent threads without the overhead of synchronization.


76-84: CAS-based initialization is well-implemented.

The early return check on line 77-79 provides a fast path for already-initialized selectors, while the CAS on line 81 ensures thread-safe one-time registration of the listener.


98-108: Good use of Template Method pattern.

Making selectNow() final and delegating to doSelectNow() ensures that initialization is guaranteed before any endpoint selection, preventing the initialization omission issue described in the PR objectives.


142-146: Unable to verify the delegation chain due to repository access limitations.

The review comment makes specific technical claims about the initialization flow in AbstractEndpointSelector.java (lines 142-146), including that tryInitialize() is idempotent with an early return and that redundant calls are safe. The original shell script request aimed to verify that no code paths bypass initialization. However, the repository could not be accessed in the sandbox environment to confirm these assertions. Manual verification against the actual codebase is required to validate the delegation chain, the idempotency of tryInitialize(), and the safety of the redundant call pattern described.

@codecov
Copy link

codecov bot commented Dec 3, 2025

Codecov Report

❌ Patch coverage is 76.92308% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.18%. Comparing base (8150425) to head (afa61ca).
⚠️ Report is 271 commits behind head on main.

Files with missing lines Patch % Lines
...eria/client/endpoint/AbstractEndpointSelector.java 76.92% 2 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main    #6535      +/-   ##
============================================
- Coverage     74.46%   74.18%   -0.29%     
- Complexity    22234    23406    +1172     
============================================
  Files          1963     2102     +139     
  Lines         82437    87598    +5161     
  Branches      10764    11501     +737     
============================================
+ Hits          61385    64981    +3596     
- Misses        15918    17126    +1208     
- Partials       5134     5491     +357     

☔ View full report in Codecov by Sentry.
📢 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.

@Override
public CompletableFuture<Endpoint> select(ClientRequestContext ctx, ScheduledExecutorService executor,
long timeoutMillis) {
tryInitialize();
Copy link
Contributor

Choose a reason for hiding this comment

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

Question) For my understanding, it seems reasonable to assume that pretty much all cases of extending AbstractEndpointSelector will want to listen to the endpointGroup.
Why isn't tryInitialize() called from the constructor? Was there a race condition?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good question. tryInitialize() may invoke updateNewEndpoints synchronously if the endpointGroup has been updated with the initial endpoints. Since updateNewEndpoints can access member fields of subclasses, calling tryInitialize() in the constructor of AbstractEndpointSelector may result in updateNewEndpoints being invoked before those fields are initialized.

DefaultEndpointSelector(EndpointGroup endpointGroup,
LoadBalancerFactory<T> loadBalancerFactory) {
super(endpointGroup);
this.loadBalancerFactory = loadBalancerFactory;

protected void updateNewEndpoints(List<Endpoint> endpoints) {
lock.lock();
try {
if (closed) {
return;
}
loadBalancer = loadBalancerFactory.newLoadBalancer(loadBalancer, endpoints);

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants