Skip to content

perf: Use LinkedHashSet to deduplicate search domains - #17400

Merged
normanmaurer merged 3 commits into
netty:4.2from
JunggiKim:perf/ordered-search-domain-dedup
Sep 10, 2026
Merged

normanmaurer merged 3 commits into
netty:4.2from
JunggiKim:perf/ordered-search-domain-dedup

Conversation

@JunggiKim

Copy link
Copy Markdown
Contributor

Motivation:

DnsNameResolverBuilder.searchDomains keeps unique domains in an ArrayList.
For every new domain, List.contains scans the values already collected. A large
list of distinct domains therefore performs a growing number of comparisons.

Modification:

Use one LinkedHashSet while reading the supplied Iterable. It removes
duplicates in expected constant time and preserves the first-seen order. Convert
the set to the existing String[] field after the loop, so resolver lookup order
and the stored representation stay unchanged.

Add a regression test for order, duplicate removal, case sensitivity, Aa / BB
hash collisions, and stopping at the first null value.

Result:

The duplicate-checking work changes from O(N^2) for N distinct domains to expected
O(N). A local JMH collection-loop benchmark on JDK 21, with 2 forks, measured:

Distinct domains ArrayList LinkedHashSet
64 3.196 us/op 1.065 us/op
256 46.678 us/op 4.584 us/op
1024 780.932 us/op 26.025 us/op
4096 12196.313 us/op 147.224 us/op

This is builder configuration work, not DNS request latency. Inputs containing
only repeated values may be slower because the old list stays at one element.

Verification:

./mvnw -pl resolver-dns -am -Dtest=DnsNameResolverBuilderTest \
  -Dsurefire.failIfNoSpecifiedTests=false test

The reactor build succeeded. DnsNameResolverBuilderTest ran 10 tests with zero
failures, errors, or skipped tests.

@JunggiKim
JunggiKim force-pushed the perf/ordered-search-domain-dedup branch from b6c6c50 to 0ba96ff Compare September 8, 2026 09:56
@normanmaurer

Copy link
Copy Markdown
Member

@JunggiKim did you sign our icla yet ? https://netty.io/s/icla

This change looks good... That said the impact is more or less meaningless in real-world as it is just used in the builder.

@JunggiKim

JunggiKim commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

@normanmaurer
Ah, I completely missed that! Thank you for kindly pointing it out—I just went ahead and signed it! 😊

Regarding the PR, while the performance impact in a real-world environment might be minimal, the change itself carries very low risk. Therefore, I believe it would still be a nice improvement to merge.

However, I completely respect your decision. If you feel this change isn't necessary, please feel free to close this PR (or let me know, and I can close it myself). Thanks again for your time and review!

@JunggiKim

Copy link
Copy Markdown
Contributor Author

Hi @normanmaurer,

I investigated the failed CI job.

The only failed check is linux-x86_64-java21-graal build. It failed in
SingleThreadEventExecutorTest.testNotSuspendedUntilScheduledTaskIsCancelled()
from the common module.

This PR changes only resolver-dns/src/main/java/io/netty/resolver/dns/ DnsNameResolverBuilder.java and its corresponding DNS builder test. The failing
test is unrelated to DnsNameResolverBuilder, searchDomains(), or this PR's
LinkedHashSet change.

TestLens marks this exact test as flaky: 4 flaky outcomes in 1,418 recent runs
(about 1%). The failure is caused by a timing race while a cancelled scheduled task
is being removed asynchronously from the event-loop queue. Netty previously
addressed the same flaky test in #15256.

Could a maintainer please rerun only the failed linux-x86_64-java21-graal build
job?

Thank you.

Comment thread resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverBuilder.java Outdated
Co-authored-by: Norman Maurer <norman_maurer@apple.com>
@JunggiKim

Copy link
Copy Markdown
Contributor Author

Thanks for applying the suggestions directly, @normanmaurer — using Set as the declared type is cleaner. Nothing further from my side the branch is up to date
with your commit. Let me know if you'd like anything else here.

@testlens-app

testlens-app Bot commented Sep 9, 2026

Copy link
Copy Markdown

🚨 TestLens detected 2 failed tests 🚨

Here is what you can do:

  1. Inspect the test failures carefully.
  2. If you are convinced that some of the tests are flaky, you can mute them below.
  3. Finally, trigger a rerun by checking the rerun checkbox.

Test Summary

Build PR / linux-x86_64-java11-boringssl-jdk8-tests build > Netty/Codec/HTTP2

Test Runs Flakiness
Http2ConnectionRoundtripTest > createStreamAfterReceiveGoAwayShouldNotSendGoAway() 🚫 ❌ 1% 🟡

Build PR / windows-x86_64-java11-boringssl > Netty/Handler

Test Runs Flakiness
OpenSslConscryptSslEngineInteropTest > testSessionInvalidate(SSLEngineTestParam) > [4] OpenSslEngineTestParam{type=Direct, protocolCipherCombo=ProtocolCipherCombo{protocol='TLSv1.2', cipher='TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256'}, delegate=false, useTasks=false, useTickets=true} 🚫 ❌ 1% 🟡

🏷️ Commit: 31e15b8
▶️ Tests: 417982 executed
⚪️ Checks: 31/31 completed

Test Failures

Http2ConnectionRoundtripTest > createStreamAfterReceiveGoAwayShouldNotSendGoAway() (Netty/Codec/HTTP2 in Build PR / linux-x86_64-java11-boringssl-jdk8-tests build)
org.opentest4j.AssertionFailedError: expected: <true> but was: <false>
	at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:151)
	at org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:132)
	at org.junit.jupiter.api.AssertTrue.failNotTrue(AssertTrue.java:63)
	at org.junit.jupiter.api.AssertTrue.assertTrue(AssertTrue.java:36)
	at org.junit.jupiter.api.AssertTrue.assertTrue(AssertTrue.java:31)
	at org.junit.jupiter.api.Assertions.assertTrue(Assertions.java:183)
	at io.netty.handler.codec.http2.Http2ConnectionRoundtripTest.createStreamAfterReceiveGoAwayShouldNotSendGoAway(Http2ConnectionRoundtripTest.java:879)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289)
	at java.util.concurrent.ForkJoinTask.doJoin(ForkJoinTask.java:389)
	at java.util.concurrent.ForkJoinTask.join(ForkJoinTask.java:719)
	at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289)
	at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056)
	at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692)
	at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:175)
expected actual
true false
OpenSslConscryptSslEngineInteropTest > testSessionInvalidate(SSLEngineTestParam) > [4] OpenSslEngineTestParam{type=Direct, protocolCipherCombo=ProtocolCipherCombo{protocol='TLSv1.2', cipher='TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256'}, delegate=false, useTasks=false, useTickets=true} (Netty/Handler in Build PR / windows-x86_64-java11-boringssl)
org.opentest4j.AssertionFailedError: session should be invalid: org.conscrypt.Java8ExtendedSSLSession@73e34c56 ==> expected: <false> but was: <true>
	at io.netty.handler.ssl.SSLEngineTest.testSessionInvalidate(SSLEngineTest.java:1412)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at java.base/java.util.Optional.ifPresent(Optional.java:183)
	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:183)
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:195)
	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:183)
	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:183)
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:195)
	at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1655)
	at java.base/java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:658)
	at java.base/java.util.stream.ReferencePipeline$7$1.accept(ReferencePipeline.java:274)
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:195)
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:195)
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:195)
	at java.base/java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948)
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:484)
	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474)
	at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:150)
	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:173)
	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
	at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:497)
	at java.base/java.util.stream.ReferencePipeline$7$1.accept(ReferencePipeline.java:274)
	at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1655)
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:484)
	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474)
	at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:150)
	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:173)
	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
	at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:497)
	at java.base/java.util.stream.ReferencePipeline$7$1.accept(ReferencePipeline.java:274)
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:195)
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:195)
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:195)
	at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1655)
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:484)
	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474)
	at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:150)
	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:173)
	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
	at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:497)
	at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:290)
	at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1020)
	at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1656)
	at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1594)
	at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:183)
expected actual
false true

Rerun Controls

Select tests to mute in this pull request:

  • Http2ConnectionRoundtripTest > createStreamAfterReceiveGoAwayShouldNotSendGoAway()
  • OpenSslConscryptSslEngineInteropTest > testSessionInvalidate(SSLEngineTestParam)

Reuse successful test results:

  • ♻️ Only rerun the tests that failed or were muted before

Click the checkbox to trigger a rerun:

  • Rerun jobs

Learn more about TestLens at testlens.app/docs.

@normanmaurer normanmaurer added this to the 4.2.18.Final milestone Sep 9, 2026
@chrisvest chrisvest modified the milestones: 4.2.18.Final, 4.2.19.Final Sep 10, 2026
@normanmaurer normanmaurer added needs-cherry-pick-4.1 This PR should be cherry-picked to 4.1 once merged. needs-cherry-pick-5.0 This PR should be cherry-picked to 5.0 once merged. labels Sep 10, 2026
@normanmaurer
normanmaurer merged commit 4021ead into netty:4.2 Sep 10, 2026
28 of 30 checks passed
@netty-project-bot

Copy link
Copy Markdown
Contributor

Auto-port PR for 4.1: #17407

@netty-project-bot

Copy link
Copy Markdown
Contributor

Auto-port PR for 5.0: #17408

@github-actions github-actions Bot removed the needs-cherry-pick-4.1 This PR should be cherry-picked to 4.1 once merged. label Sep 10, 2026
@github-actions github-actions Bot removed the needs-cherry-pick-5.0 This PR should be cherry-picked to 5.0 once merged. label Sep 10, 2026
normanmaurer added a commit that referenced this pull request Sep 10, 2026
…17407)

Auto-port of #17400 to 4.1
Cherry-picked commit: 4021ead

---
Motivation:

`DnsNameResolverBuilder.searchDomains` keeps unique domains in an
`ArrayList`.
For every new domain, `List.contains` scans the values already
collected. A large
list of distinct domains therefore performs a growing number of
comparisons.

Modification:

Use one `LinkedHashSet` while reading the supplied `Iterable`. It
removes
duplicates in expected constant time and preserves the first-seen order.
Convert
the set to the existing `String[]` field after the loop, so resolver
lookup order
and the stored representation stay unchanged.

Add a regression test for order, duplicate removal, case sensitivity,
`Aa` / `BB`
hash collisions, and stopping at the first `null` value.

Result:

The duplicate-checking work changes from O(N^2) for N distinct domains
to expected
O(N). A local JMH collection-loop benchmark on JDK 21, with 2 forks,
measured:

| Distinct domains | ArrayList | LinkedHashSet |
| ---: | ---: | ---: |
| 64 | 3.196 us/op | 1.065 us/op |
| 256 | 46.678 us/op | 4.584 us/op |
| 1024 | 780.932 us/op | 26.025 us/op |
| 4096 | 12196.313 us/op | 147.224 us/op |

This is builder configuration work, not DNS request latency. Inputs
containing
only repeated values may be slower because the old list stays at one
element.

Verification:

```text
./mvnw -pl resolver-dns -am -Dtest=DnsNameResolverBuilderTest \
  -Dsurefire.failIfNoSpecifiedTests=false test
```

The reactor build succeeded. `DnsNameResolverBuilderTest` ran 10 tests
with zero
failures, errors, or skipped tests.

Co-authored-by: kjg <kimjg2477@gmail.com>
Co-authored-by: Norman Maurer <norman_maurer@apple.com>
normanmaurer added a commit that referenced this pull request Sep 10, 2026
…17408)

Auto-port of #17400 to 5.0
Cherry-picked commit: 4021ead

---
Motivation:

`DnsNameResolverBuilder.searchDomains` keeps unique domains in an
`ArrayList`.
For every new domain, `List.contains` scans the values already
collected. A large
list of distinct domains therefore performs a growing number of
comparisons.

Modification:

Use one `LinkedHashSet` while reading the supplied `Iterable`. It
removes
duplicates in expected constant time and preserves the first-seen order.
Convert
the set to the existing `String[]` field after the loop, so resolver
lookup order
and the stored representation stay unchanged.

Add a regression test for order, duplicate removal, case sensitivity,
`Aa` / `BB`
hash collisions, and stopping at the first `null` value.

Result:

The duplicate-checking work changes from O(N^2) for N distinct domains
to expected
O(N). A local JMH collection-loop benchmark on JDK 21, with 2 forks,
measured:

| Distinct domains | ArrayList | LinkedHashSet |
| ---: | ---: | ---: |
| 64 | 3.196 us/op | 1.065 us/op |
| 256 | 46.678 us/op | 4.584 us/op |
| 1024 | 780.932 us/op | 26.025 us/op |
| 4096 | 12196.313 us/op | 147.224 us/op |

This is builder configuration work, not DNS request latency. Inputs
containing
only repeated values may be slower because the old list stays at one
element.

Verification:

```text
./mvnw -pl resolver-dns -am -Dtest=DnsNameResolverBuilderTest \
  -Dsurefire.failIfNoSpecifiedTests=false test
```

The reactor build succeeded. `DnsNameResolverBuilderTest` ran 10 tests
with zero
failures, errors, or skipped tests.

Co-authored-by: kjg <kimjg2477@gmail.com>
Co-authored-by: Norman Maurer <norman_maurer@apple.com>
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.

4 participants