Add VirtualHost support for random ports (#6410)#6603
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds ServerPort-aware virtual hosts and runtime port mapping: ServerPort now tracks actual bound ports; ServerBuilder gains Changes
Sequence Diagram(s)sequenceDiagram
participant App as Application
participant SB as ServerBuilder
participant SP as ServerPort
participant VHB as VirtualHostBuilder
participant S as Server
participant DSC as DefaultServerConfig
participant Client as Client
App->>SB: port(ServerPort) / port(0)
SB->>SP: create ServerPort(ephemeral)
App->>SB: virtualHost(ServerPort)
SB->>VHB: new VirtualHostBuilder(ServerPort)
VHB-->>SB: register VirtualHostBuilder(serverPort)
App->>SB: build()
SB->>S: build/start
S->>SP: bind to actual port (OS)
S->>SP: setActualPort(actualPort)
S->>DSC: rebuildDomainAndPortMapping()
Client->>DSC: findVirtualHost(hostname, actualPort)
DSC-->>Client: return VirtualHost bound to original ServerPort
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Suggested labels
Suggested reviewers
Poem
✨ Finishing Touches🧪 Generate unit tests (beta)
Tip Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs). 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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@core/src/main/java/com/linecorp/armeria/server/ServerBuilder.java`:
- Around line 2506-2513: The validation currently uses
this.ports.contains(serverPort) which relies on equals() but runtime matching
requires reference identity; update the check in the ServerBuilder validation
loop (involving VirtualHostBuilder and its serverPort() call) to test identity
instead of equality (e.g., ensure any entry in this.ports is == serverPort) and
keep the existing checkState message; locate the block using VirtualHostBuilder
vhb, serverPort(), and this.ports and replace the contains(...) condition with
an identity-based predicate (anyMatch(p -> p == serverPort) or equivalent).
- Around line 1860-1889: The new public overload virtualHost(ServerPort) in
ServerBuilder must be annotated with `@UnstableApi` to mark the API as unstable;
update the declaration of the virtualHost(ServerPort) method in ServerBuilder to
add the `@UnstableApi` annotation (importing it if needed) so the public API
surface for VirtualHostBuilder returned by virtualHost(ServerPort) is properly
marked per project guidelines.
🧹 Nitpick comments (2)
core/src/test/java/com/linecorp/armeria/server/PortBasedVirtualHostTest.java (1)
241-331: Add assertions for/barrouting invirtualHostWithServerPort.
Right now/barresponses are computed but never asserted, so misrouting could slip through.✅ Proposed test assertion
// Check /bar on both ports final AggregatedHttpResponse resp1Bar = client1.get("/bar").aggregate().join(); final AggregatedHttpResponse resp2Bar = client2.get("/bar").aggregate().join(); // One port should serve /foo with OK, the other with NOT_FOUND final boolean port1ServesFoo = resp1Foo.status() == HttpStatus.OK; final boolean port2ServesFoo = resp2Foo.status() == HttpStatus.OK; assertThat(port1ServesFoo || port2ServesFoo) .as("At least one port should serve /foo, but got: port1=%s, port2=%s", resp1Foo.status(), resp2Foo.status()) .isTrue(); + // One port should serve /bar with OK as well. + final boolean port1ServesBar = resp1Bar.status() == HttpStatus.OK; + final boolean port2ServesBar = resp2Bar.status() == HttpStatus.OK; + assertThat(port1ServesBar || port2ServesBar) + .as("At least one port should serve /bar, but got: port1=%s, port2=%s", + resp1Bar.status(), resp2Bar.status()) + .isTrue();core/src/main/java/com/linecorp/armeria/server/VirtualHost.java (1)
316-341: Consider documenting the dynamic port resolution behavior.The
defaultHostname()andhostnamePattern()methods now return different values depending on whether the server has bound to its ports. Before binding,serverPort.localAddress()returnsnull, so the pre-computed value is returned. After binding, the actual ephemeral port is appended.This dynamic behavior is intentional for ephemeral port support, but callers may not expect the return value to change over the VirtualHost lifecycle. Consider adding Javadoc clarification, e.g.:
/** * Returns the default hostname of this virtual host. If this virtual host is bound to a * {`@link` ServerPort} with an ephemeral port (0), the actual bound port is appended after * the server starts. */
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In
`@core/src/test/java/com/linecorp/armeria/server/PortBasedVirtualHostTest.java`:
- Around line 241-286: The test virtualHostWithServerPort collects responses for
/foo and /bar but never asserts the /bar results; update the test to compute
booleans for resp1Bar.status() and resp2Bar.status() (e.g., port1ServesBar,
port2ServesBar) and add assertions: (1) at least one port serves /bar
(port1ServesBar || port2ServesBar) and (2) ensure exclusivity per port by
asserting each port serves exactly one of the endpoints (for each port assert
(servesFoo ^ servesBar) is true), referencing the existing
resp1Foo/resp2Foo/resp1Bar/resp2Bar variables in virtualHostWithServerPort so
the test verifies both routing and exclusivity.
♻️ Duplicate comments (2)
core/src/main/java/com/linecorp/armeria/server/ServerBuilder.java (2)
1860-1903: Add@UnstableApito the new public overload.
New public APIs should be annotated as unstable per project guidelines.As per coding guidelines, ...✅ Proposed fix
+ `@UnstableApi` public VirtualHostBuilder virtualHost(ServerPort serverPort) { requireNonNull(serverPort, "serverPort");
2506-2513: ValidateServerPortby identity, not equals.
contains()usesequals, but runtime matching uses reference identity (==), so a different-but-equal instance would pass validation yet never match.🐛 Proposed fix
- if (serverPort != null) { - checkState(this.ports.contains(serverPort), + if (serverPort != null) { + final boolean presentByIdentity = this.ports.stream().anyMatch(p -> p == serverPort); + checkState(presentByIdentity, "The ServerPort for a virtual host is not in the server's port list. " + "Please add the ServerPort using port(ServerPort) before creating a virtual host."); }
🧹 Nitpick comments (1)
core/src/main/java/com/linecorp/armeria/server/DefaultServerConfig.java (1)
427-445: Consider avoiding per-request full scans when no ServerPort-based vhosts exist.
findVirtualHost(...)now iterates active ports (and then virtual hosts) on every request; a boolean guard or a precomputed map could keep the hot path cheaper.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #6603 +/- ##
============================================
- Coverage 74.46% 74.26% -0.20%
- Complexity 22234 23974 +1740
============================================
Files 1963 2161 +198
Lines 82437 89633 +7196
Branches 10764 11707 +943
============================================
+ Hits 61385 66570 +5185
- Misses 15918 17482 +1564
- Partials 5134 5581 +447 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
…lookup - Add actualPort field and methods to ServerPort for storing actual bound port - Add actualPortToVirtualHost map to DefaultServerConfig for O(1) lookup - Update Server to set actualPort after binding and build port mapping - Add tests for actualPort and multiple random port VirtualHosts
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
core/src/main/java/com/linecorp/armeria/server/Server.java (1)
800-825:⚠️ Potential issue | 🔴 CriticalAdd build-time validation to prevent
virtualHost(ServerPort)with port groups or ensuresetActualPort()is called for port group reuse scenarios.When a
ServerPortbelongs to a port group and reuses a previously bound port inNextServerPortStartListener(lines 900-904), the temporaryServerPortcreated doesn't havesetActualPort()called on the original config port. This causesbuildActualPortMapping()to skip creating routing entries for ephemeral ports, breaking port-based virtual host routing silently.Consider adding a build-time check in
ServerBuilderto either:
- Reject configurations where
virtualHost(ServerPort)uses aServerPortwith aportGroup, or- Fix the runtime behavior to ensure
setActualPort()is called for all reused ports in a port group
🤖 Fix all issues with AI agents
In `@core/src/main/java/com/linecorp/armeria/server/DefaultServerConfig.java`:
- Around line 405-423: ServerBuilder.virtualHost(ServerPort) currently accepts
any ServerPort but DefaultServerConfig.buildActualPortMapping only maps
ephemeral ports (where ServerPort.localAddress().getPort() == 0), so add a
validation in ServerBuilder.virtualHost(ServerPort) to enforce ephemeral ports:
check serverPort.localAddress().getPort() == 0 and throw an
IllegalArgumentException (or use Objects.requireNonNull/assert) with a clear
message if not ephemeral; reference ServerBuilder.virtualHost(ServerPort),
ServerPort.localAddress().getPort(), and
DefaultServerConfig.buildActualPortMapping to locate the related logic.
In `@core/src/main/java/com/linecorp/armeria/server/ServerPort.java`:
- Around line 249-260: Add the `@UnstableApi` annotation to the new public method
actualPort() in the ServerPort class to follow project guidelines for newly
added public APIs; update imports to include the UnstableApi annotation (e.g.,
com.linecorp.armeria.common.annotation.UnstableApi) and place the annotation
immediately above the actualPort() method signature so the method is marked as
unstable.
🧹 Nitpick comments (2)
core/src/main/java/com/linecorp/armeria/server/ServerPort.java (2)
270-277:setActualPorthas a non-atomic check-then-act on a volatile field.The guard
checkState(this.actualPort == -1, ...)followed bythis.actualPort = actualPortis not atomic. While this is package-private and currently only called from a single event-loop thread inServerPortStartListener, a future caller from a different thread could trigger a race where two threads both see-1and both proceed to write.Consider either:
- Documenting the single-thread assumption (e.g.,
@GuardedByor a Javadoc note), or- Using a CAS via
AtomicInteger/VarHandlefor correctness.
337-345:toString()does not reflectactualPortororiginalServerPort.After binding,
actualPortholds meaningful diagnostic information (the real ephemeral port). Including it intoString()would improve debuggability when inspecting bound ports in logs.
- Add @UnstableApi annotation to ServerPort.actualPort() per Armeria conventions - Add fixedPortVirtualHostWithServerPort() test to verify virtualHost(ServerPort) works correctly
jrhee17
left a comment
There was a problem hiding this comment.
Changes look reasonable to me 👍
- Defer VirtualHost domain/port mapping until after ports are bound, unifying virtualHostAndPortMapping and actualPortToVirtualHost into a single volatile map that is rebuilt via rebuildDomainAndPortMapping() - Remove originalServerPort from ServerPort as it is no longer used after the mapping unification - Cache defaultHostname() and hostnamePattern() results in VirtualHost to avoid repeated String allocation on every call - Remove unnecessary null check on ServerPort.localAddress() which is guaranteed non-null by requireNonNull in constructor
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
core/src/main/java/com/linecorp/armeria/server/DefaultServerConfig.java (2)
447-465:⚠️ Potential issue | 🟠 MajorMultiple reads of volatile
virtualHostAndPortMapping— use a local snapshot.
findVirtualHostreads the volatile field on lines 449, 453, and 463 without capturing it in a local variable. IfrebuildDomainAndPortMapping()replaces the reference between reads, different reads may see different map instances, leading to inconsistent lookups within a single call.Proposed fix
`@Override` public VirtualHost findVirtualHost(String hostname, int port) { - if (virtualHostAndPortMapping == null) { + final Int2ObjectMap<Mapping<String, VirtualHost>> mapping = virtualHostAndPortMapping; + if (mapping == null) { return defaultVirtualHost; } - final Mapping<String, VirtualHost> virtualHostMapping = virtualHostAndPortMapping.get(port); + final Mapping<String, VirtualHost> virtualHostMapping = mapping.get(port); if (virtualHostMapping != null) { final VirtualHost virtualHost = virtualHostMapping.map(hostname + ':' + port); // Exclude the default virtual host from port-based virtual hosts. if (virtualHost != defaultVirtualHost) { return virtualHost; } } // No port-based virtual host is configured. Look for named-based virtual host. - final Mapping<String, VirtualHost> nameBasedMapping = requireNonNull(virtualHostAndPortMapping.get(-1)); + final Mapping<String, VirtualHost> nameBasedMapping = requireNonNull(mapping.get(-1)); return nameBasedMapping.map(hostname); }
439-445:⚠️ Potential issue | 🟡 MinorSame volatile read issue in deprecated
findVirtualHost(String).This method also reads
virtualHostAndPortMappingtwice without a local snapshot (lines 440 and 443). Apply the same local-variable pattern for consistency.Proposed fix
`@Override` `@Deprecated` public VirtualHost findVirtualHost(String hostname) { - if (virtualHostAndPortMapping == null) { + final Int2ObjectMap<Mapping<String, VirtualHost>> mapping = virtualHostAndPortMapping; + if (mapping == null) { return defaultVirtualHost; } - final Mapping<String, VirtualHost> virtualHostMapping = virtualHostAndPortMapping.get(-1); + final Mapping<String, VirtualHost> virtualHostMapping = mapping.get(-1); return virtualHostMapping.map(hostname); }
🧹 Nitpick comments (1)
core/src/test/java/com/linecorp/armeria/server/PortBasedVirtualHostTest.java (1)
241-293: Consider usingServerPort.actualPort()directly for clarity.The
serverPortBasedVirtualHostRoutesCorrectlyandmultipleRandomPortVirtualHoststests accessport1.actualPort()directly, which is cleaner than sortingactivePorts(). This test could be simplified similarly, though it still works correctly as-is.
- Restore two-pass approach in buildDomainAndPortMapping() to collect per-port default VirtualHosts before building DomainMappingBuilder, fixing order-dependent bug where computeIfAbsent ignored the correct port-specific default when a named VirtualHost was processed first - Fix compilation error in RoutersBenchmark caused by missing TlsEngineType parameter in VirtualHost constructor - Add shouldFallbackToDefaultPortVirtualHost test
- Capture virtualHostAndPortMapping into a local variable before use in findVirtualHost() to prevent inconsistent reads if the field is replaced by rebuildDomainAndPortMapping() between accesses
ikhoon
left a comment
There was a problem hiding this comment.
Looks nice! @junjunclub 👍👍
minwoox
left a comment
There was a problem hiding this comment.
Thanks a lot, @junjunclub!
|
🎉 🎉 🎉 |
Motivation
Currently, using
virtualHost(0)throws anIllegalArgumentException:java.lang.IllegalArgumentException: port: 0 (expected: 1-65535)This is problematic for CI environments where random ports are essential to avoid port conflicts. The existing
virtualHost(int port)API cannot support random ports because:intvalue0cannot distinguish between multiple random ports.virtualHost(0)twice returns the sameVirtualHostBuilderdue to the builder reuse logic.See: issue #6410
Modifications
ServerBuilder.virtualHost(ServerPort)API that uses reference equality to distinguish different random port configurations.VirtualHostBuilder.serverPortfield and constructor to supportServerPort-based virtual hosts.VirtualHost.serverPortfield and accessor.ServerPort.originalServerPortfield to track the original configuration when binding to ephemeral ports.ServerPortreference when creatingactualPortinServer.java.DefaultServerConfig.findVirtualHost()to route requests based on the actual bound port (priority lookup).VirtualHostBuilder.serverPort(),VirtualHost.serverPort(), andServerPort.originalServerPort()accessors.virtualHost(ServerPort)API routing behavior.Result
VirtualHosts to random ports usingServerPortobjects:Each
ServerPortinstance is assigned a different random port by the OS, and requests are correctly routed to the correspondingVirtualHost.Follow-up: O(1)
ServerPort-based VirtualHost lookup (new in this PR)While implementing
virtualHost(ServerPort), we noticed thatfindVirtualHost(String hostname, int port)can become inefficient forServerPort-based virtual hosts using ephemeral ports, because it may scanactivePortsandvirtualHoststo resolve the actual bound port.This PR also introduces an
actualPort()approach and a precomputed mapping to make this routing step O(1) after all ports are bound.Additional changes
ServerPort.actualPort()volatile int actualPortto store the actual bound port.actualPort()getter and package-privatesetActualPort(int)setter.Server.javaport.setActualPort(...)after binding ephemeral ports.DefaultServerConfigvolatile Map<Integer, VirtualHost> actualPortToVirtualHost.buildActualPortMapping()to populate the mapping after all ports are bound.findVirtualHost(String, int)to prefer the O(1) map lookup forServerPort-based routing.Tests
ServerPort.actualPort()getter/setter behavior.actualPortis reflected after server start and that multiple random-port virtual hosts work as expected.Outcome
findVirtualHost()now performs O(1) lookup forServerPort-based virtual host routing once ports are bound.ServerPort.actualPort()provides a clean API to retrieve the actual bound port.