Skip to content

fix: Unwrap DelegatingEventLoopGroup in isSupported() and supportsDomainSockets()#6633

Open
novoj wants to merge 2 commits intoline:mainfrom
novoj:main
Open

fix: Unwrap DelegatingEventLoopGroup in isSupported() and supportsDomainSockets()#6633
novoj wants to merge 2 commits intoline:mainfrom
novoj:main

Conversation

@novoj
Copy link
Contributor

@novoj novoj commented Feb 21, 2026

The find() method already unwraps DelegatingEventLoopGroup, but isSupported() and supportsDomainSockets() bypass find() and call findOrNull() directly. Without unwrapping, passing a wrapped group (e.g. ShutdownConfigurableEventLoopGroup) to these methods would incorrectly return false.

Motivation:

In lastest version of armeria (1.36.0) new functionality allowed to configure shutdown on client (excerpt from release notes):

EventLoopGroup eventLoopGroup =
  EventLoopGroups
    .builder()
    .numThreads(4)
    // Reduce quiet period to 100ms and timeout to 500ms
    .gracefulShutdown(Duration.ofMillis(100), Duration.ofMillis(500)) // 👈👈👈
    .build();
ClientFactory.builder()
             .workerGroup(eventLoopGroup)
             ...
             .build();

Unfortunatelly this throws exception:

Caused by: java.lang.IllegalStateException: unsupported event loop type: com.linecorp.armeria.common.util.ShutdownConfigurableEventLoopGroup
	at com.linecorp.armeria.common.util.TransportType.unsupportedEventLoopType(TransportType.java:266)
	at com.linecorp.armeria.common.util.TransportType.find(TransportType.java:202)
	at com.linecorp.armeria.common.util.TransportType.socketChannelType(TransportType.java:83)
	at com.linecorp.armeria.client.AbstractDnsResolverBuilder.buildConfigurator(AbstractDnsResolverBuilder.java:558)
	at com.linecorp.armeria.client.DnsResolverGroupBuilder.build(DnsResolverGroupBuilder.java:201)
	at com.linecorp.armeria.client.ClientFactoryBuilder.lambda$buildOptions$3(ClientFactoryBuilder.java:1072)
	at com.linecorp.armeria.client.HttpClientFactory.<init>(HttpClientFactory.java:134)

Modifications:

  • com.linecorp.armeria.common.util.TransportType: The find() method already unwraps DelegatingEventLoopGroup, but
    isSupported() and supportsDomainSockets() bypass find() and call findOrNull() directly. Without unwrapping, passing a wrapped group (e.g. ShutdownConfigurableEventLoopGroup) to these methods would incorrectly return false.

Result:

…ainSockets()

The find() method already unwraps DelegatingEventLoopGroup, but
isSupported() and supportsDomainSockets() bypass find() and call
findOrNull() directly. Without unwrapping, passing a wrapped group
(e.g. ShutdownConfigurableEventLoopGroup) to these methods would
incorrectly return false.
Copilot AI review requested due to automatic review settings February 21, 2026 20:17
@coderabbitai
Copy link

coderabbitai bot commented Feb 21, 2026

📝 Walkthrough

Walkthrough

Unwraps DelegatingEventLoopGroup instances before type checks and transport discovery in three static helpers in TransportType, and adds an integration test validating a custom Netty EventLoopGroup used with a ClientFactory-backed WebClient.

Changes

Cohort / File(s) Summary
TransportType unwrapping
core/src/main/java/com/linecorp/armeria/common/util/TransportType.java
Unwrap DelegatingEventLoopGroup in supportsDomainSockets, isSupported, and find helpers so underlying delegate is inspected for type checks and transport capability resolution.
Integration test
core/src/test/java/com/linecorp/armeria/client/EventLoopGroupBuilderIntegrationTest.java
New test class that builds a custom EventLoopGroup, creates a ClientFactory with it, issues a GET request to an in-process server, and asserts response correctness; ensures resource cleanup.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I nudged the wrapper, peeked beneath the seam,
Found the true runner, let the transports gleam.
Tests hop in to check the friendly flow,
No more surprises — onward we go! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% 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 change: unwrapping DelegatingEventLoopGroup in the isSupported() and supportsDomainSockets() methods, which directly addresses the core fix.
Description check ✅ Passed The description clearly explains the problem, motivation, modifications, and expected outcome, all of which align with the code changes in the PR.
Linked Issues check ✅ Passed The PR modifications directly address issue #6632 by unwrapping DelegatingEventLoopGroup in isSupported() and supportsDomainSockets() to fix the IllegalStateException when using ShutdownConfigurableEventLoopGroup.
Out of Scope Changes check ✅ Passed The changes are scoped to fixing the identified issue: modifications to TransportType and a new integration test validating the fix with a custom EventLoopGroup, both directly related to the linked issue.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 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

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes an IllegalStateException that occurs when using a ShutdownConfigurableEventLoopGroup (a wrapped EventLoopGroup) with Armeria's ClientFactory. The issue arose from the newly added graceful shutdown configuration feature in version 1.36.0.

Changes:

  • Added DelegatingEventLoopGroup unwrapping to TransportType.find() to fix the primary issue
  • Added DelegatingEventLoopGroup unwrapping to TransportType.isSupported() for consistency
  • Added DelegatingEventLoopGroup unwrapping to TransportType.supportsDomainSockets() for consistency

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

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: 1

🧹 Nitpick comments (1)
core/src/main/java/com/linecorp/armeria/common/util/TransportType.java (1)

107-107: Simplify ternary to &&

♻️ Proposed simplification
-        return found != null ? found.supportsDomainSockets() : false;
+        return found != null && found.supportsDomainSockets();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/src/main/java/com/linecorp/armeria/common/util/TransportType.java` at
line 107, The ternary expression returning found != null ?
found.supportsDomainSockets() : false should be simplified to use short-circuit
AND; update the return in TransportType (the method referencing the local
variable found and calling supportsDomainSockets()) to return found != null &&
found.supportsDomainSockets() to make the intent clearer and remove the
redundant conditional.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@core/src/main/java/com/linecorp/armeria/common/util/TransportType.java`:
- Around line 100-102: TransportType contains three identical single-layer
unwraps of DelegatingEventLoopGroup (the blocks around lines where
eventLoopGroup instanceof DelegatingEventLoopGroup is checked); implement a
private helper method (e.g., unwrapDelegatingEventLoopGroup(EventLoopGroup))
that uses a while loop to repeatedly call ((DelegatingEventLoopGroup)
g).delegate() until the returned EventLoopGroup is not a
DelegatingEventLoopGroup, then replace the three identical blocks with a call to
this helper so supportsDomainSockets(), isSupported(), and find() operate on the
fully-unwrapped EventLoopGroup.

---

Nitpick comments:
In `@core/src/main/java/com/linecorp/armeria/common/util/TransportType.java`:
- Line 107: The ternary expression returning found != null ?
found.supportsDomainSockets() : false should be simplified to use short-circuit
AND; update the return in TransportType (the method referencing the local
variable found and calling supportsDomainSockets()) to return found != null &&
found.supportsDomainSockets() to make the intent clearer and remove the
redundant conditional.

Comment on lines +100 to +102
if (eventLoopGroup instanceof DelegatingEventLoopGroup) {
eventLoopGroup = ((DelegatingEventLoopGroup) eventLoopGroup).delegate();
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "*.java" | xargs grep -l "interface DelegatingEventLoopGroup\|class DelegatingEventLoopGroup" | head -5

Repository: line/armeria

Length of output: 139


🏁 Script executed:

rg -A10 "interface DelegatingEventLoopGroup" -t java

Repository: line/armeria

Length of output: 38


🏁 Script executed:

rg -A5 "delegate()" -t java | head -100

Repository: line/armeria

Length of output: 11960


🏁 Script executed:

# Check the three specific locations mentioned
cat -n core/src/main/java/com/linecorp/armeria/common/util/TransportType.java | sed -n '98,105p; 187,195p; 205,212p'

Repository: line/armeria

Length of output: 1463


🏁 Script executed:

cat -n core/src/main/java/com/linecorp/armeria/common/util/DelegatingEventLoopGroup.java

Repository: line/armeria

Length of output: 7269


🏁 Script executed:

rg "implements DelegatingEventLoopGroup|extends.*DelegatingEventLoopGroup" -t java -B2 -A10

Repository: line/armeria

Length of output: 1776


🏁 Script executed:

rg "class.*DelegatingEventLoopGroup|interface.*DelegatingEventLoopGroup" -t java -B2 -A15

Repository: line/armeria

Length of output: 4465


Extract unwrap logic into a while-loop helper to handle nested DelegatingEventLoopGroup wrappers and eliminate code duplication

The three identical unwrap blocks at lines 100–102, 189–191, and 207–209 only peel one layer. Since DelegatingEventLoopGroup.delegate() returns a generic EventLoopGroup (which could itself be another DelegatingEventLoopGroup), nested wrapping is possible. A single-level unwrap will leave the inner wrapper undetected, causing supportsDomainSockets() and isSupported() to return false incorrectly, and find() to throw a spurious exception.

A private helper with a while loop resolves both the robustness issue and the DRY violation:

♻️ Proposed refactor
+    private static EventLoopGroup unwrapDelegate(EventLoopGroup eventLoopGroup) {
+        while (eventLoopGroup instanceof DelegatingEventLoopGroup) {
+            eventLoopGroup = ((DelegatingEventLoopGroup) eventLoopGroup).delegate();
+        }
+        return eventLoopGroup;
+    }

Then replace all three blocks:

 public static boolean supportsDomainSockets(EventLoopGroup eventLoopGroup) {
     requireNonNull(eventLoopGroup, "eventLoopGroup");
-    if (eventLoopGroup instanceof DelegatingEventLoopGroup) {
-        eventLoopGroup = ((DelegatingEventLoopGroup) eventLoopGroup).delegate();
-    }
+    eventLoopGroup = unwrapDelegate(eventLoopGroup);
     ...
 }

 public static boolean isSupported(EventLoopGroup eventLoopGroup) {
     requireNonNull(eventLoopGroup, "eventLoopGroup");
-    if (eventLoopGroup instanceof DelegatingEventLoopGroup) {
-        eventLoopGroup = ((DelegatingEventLoopGroup) eventLoopGroup).delegate();
-    }
+    eventLoopGroup = unwrapDelegate(eventLoopGroup);
     ...
 }

 private static TransportType find(EventLoopGroup eventLoopGroup) {
-    if (eventLoopGroup instanceof DelegatingEventLoopGroup) {
-        eventLoopGroup = ((DelegatingEventLoopGroup) eventLoopGroup).delegate();
-    }
+    eventLoopGroup = unwrapDelegate(eventLoopGroup);
     ...
 }

Also applies to: 189–191, 207–209

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/src/main/java/com/linecorp/armeria/common/util/TransportType.java`
around lines 100 - 102, TransportType contains three identical single-layer
unwraps of DelegatingEventLoopGroup (the blocks around lines where
eventLoopGroup instanceof DelegatingEventLoopGroup is checked); implement a
private helper method (e.g., unwrapDelegatingEventLoopGroup(EventLoopGroup))
that uses a while loop to repeatedly call ((DelegatingEventLoopGroup)
g).delegate() until the returned EventLoopGroup is not a
DelegatingEventLoopGroup, then replace the three identical blocks with a call to
this helper so supportsDomainSockets(), isSupported(), and find() operate on the
fully-unwrapped EventLoopGroup.

…ainSockets()

The find() method already unwraps DelegatingEventLoopGroup, but
isSupported() and supportsDomainSockets() bypass find() and call
findOrNull() directly. Without unwrapping, passing a wrapped group
(e.g. ShutdownConfigurableEventLoopGroup) to these methods would
incorrectly return false.
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.

🧹 Nitpick comments (1)
core/src/test/java/com/linecorp/armeria/client/EventLoopGroupBuilderIntegrationTest.java (1)

46-54: Consider guarding eventLoopGroup against leaks if ClientFactory construction throws.

eventLoopGroup is created outside the try block, but the ClientFactory (which owns its shutdown via workerGroup(eventLoopGroup, true)) is only entered inside the try. If ClientFactory.builder().build() throws, the eventLoopGroup threads are never terminated. In test code with zero-duration graceful shutdown this is largely harmless, but a nested try-with-resources (or moving the group into its own try) makes the intent explicit and protects against flaky teardown if a future change introduces a non-zero duration.

♻️ Suggested fix: guard `eventLoopGroup` with its own try-with-resources
-        final EventLoopGroup eventLoopGroup = EventLoopGroups
-                .builder()
-                .numThreads(4)
-                .gracefulShutdown(Duration.ofMillis(0), Duration.ofMillis(0))
-                .build();
-
-        try (ClientFactory clientFactory = ClientFactory.builder()
-                .workerGroup(eventLoopGroup, true)
-                .build()) {
+        try (ClientFactory clientFactory = ClientFactory.builder()
+                .workerGroup(EventLoopGroups.builder()
+                                            .numThreads(4)
+                                            .gracefulShutdown(Duration.ofMillis(0), Duration.ofMillis(0))
+                                            .build(), true)
+                .build()) {

This keeps the eventLoopGroup lifetime entirely managed by the ClientFactory (via shutdownOnClose = true), so a single try is sufficient and there is no window for a leak.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/src/test/java/com/linecorp/armeria/client/EventLoopGroupBuilderIntegrationTest.java`
around lines 46 - 54, Create the EventLoopGroup inside its own
try-with-resources so it is closed if ClientFactory.builder().build() throws:
instantiate EventLoopGroup via
EventLoopGroups.builder().numThreads(4).gracefulShutdown(...).build() in a
try(...) resource, then inside that try create the ClientFactory with
ClientFactory.builder().workerGroup(eventLoopGroup, true).build() (nested
try-with-resources) so workerGroup(..., true) still sets shutdownOnClose but the
EventLoopGroup is guarded against leaks on builder failure.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@core/src/test/java/com/linecorp/armeria/client/EventLoopGroupBuilderIntegrationTest.java`:
- Around line 46-54: Create the EventLoopGroup inside its own try-with-resources
so it is closed if ClientFactory.builder().build() throws: instantiate
EventLoopGroup via
EventLoopGroups.builder().numThreads(4).gracefulShutdown(...).build() in a
try(...) resource, then inside that try create the ClientFactory with
ClientFactory.builder().workerGroup(eventLoopGroup, true).build() (nested
try-with-resources) so workerGroup(..., true) still sets shutdownOnClose but the
EventLoopGroup is guarded against leaks on builder failure.

@codecov
Copy link

codecov bot commented Feb 21, 2026

Codecov Report

❌ Patch coverage is 66.66667% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.26%. Comparing base (8150425) to head (452d0fa).
⚠️ Report is 347 commits behind head on main.

Files with missing lines Patch % Lines
...om/linecorp/armeria/common/util/TransportType.java 66.66% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main    #6633      +/-   ##
============================================
- Coverage     74.46%   74.26%   -0.20%     
- Complexity    22234    23936    +1702     
============================================
  Files          1963     2160     +197     
  Lines         82437    89494    +7057     
  Branches      10764    11702     +938     
============================================
+ Hits          61385    66466    +5081     
- Misses        15918    17442    +1524     
- Partials       5134     5586     +452     

☔ 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.

@ikhoon ikhoon added the defect label Feb 23, 2026
@ikhoon ikhoon added this to the 1.37.0 milestone Feb 23, 2026
Copy link
Contributor

@jrhee17 jrhee17 left a comment

Choose a reason for hiding this comment

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

Question) Would it be possible to instead modify so that ShutdownConfigurableEventLoopGroup extends from IoEventLoopGroup?
This way, we don't need to worry about the unwrapping logic from usage locations.

It seems like TransportTypeProvider only returns an MultiThreadIoEventLoopGroup type at the moment, so it seems safe to assume only an IoEventLoopGroup will be returned.

i.e.

class DelegatingEventLoopGroup implements IoEventLoopGroup {
...

and

...
        final EventLoopGroup eventLoopGroup = type.newEventLoopGroup(numThreads, unused -> factory);

        // Wrap with shutdown configuration if non-default values are used
        if (shutdownQuietPeriodMillis != DEFAULT_SHUTDOWN_QUIET_PERIOD_MILLIS ||
            shutdownTimeoutMillis != DEFAULT_SHUTDOWN_TIMEOUT_MILLIS) {
            checkArgument(eventLoopGroup instanceof IoEventLoopGroup,
                          "Expected a type of IOEventLoopGroup, but got: %s", eventLoopGroup.getClass());
            final IoEventLoopGroup ioEventLoopGroup = (IoEventLoopGroup) eventLoopGroup;
            return new ShutdownConfigurableEventLoopGroup(
                    ioEventLoopGroup, shutdownQuietPeriodMillis, shutdownTimeoutMillis);
...

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.

IllegalStateException: unsupported event loop type: com.linecorp.armeria.common.util.ShutdownConfigurableEventLoopGroup

4 participants