Skip to content

Fix Server.Builder#build() silently overriding an explicit AddressResolver - #11

Open
roydahan wants to merge 2 commits into
scylladb:masterfrom
roydahan:fix/multiple-nodes-per-ip-resolver-override
Open

Fix Server.Builder#build() silently overriding an explicit AddressResolver#11
roydahan wants to merge 2 commits into
scylladb:masterfrom
roydahan:fix/multiple-nodes-per-ip-resolver-override

Conversation

@roydahan

Copy link
Copy Markdown
Collaborator

Problem

Server.Builder#build() unconditionally replaced the configured AddressResolver with a fresh NodePerPortResolver whenever withMultipleNodesPerIp(true) had been set, regardless of the order in which builder methods were called:

AddressResolver addressResolver = this.addressResolver;
if (multipleNodesPerIp) {
  addressResolver = new NodePerPortResolver();
}

This meant a caller who did:

Server.builder()
    .withMultipleNodesPerIp(true)
    .withAddressResolver(myCustomResolver)
    .build();

would have myCustomResolver silently discarded at build() time and get a NodePerPortResolver instead, even though withAddressResolver(...) was called after withMultipleNodesPerIp(true). There was no way to combine withMultipleNodesPerIp(true) (needed for its peer-metadata behavior via PeerMetadataHandler(multipleNodesPerIp)) with a custom address/port allocation strategy.

Fix

withMultipleNodesPerIp(true) now sets this.addressResolver = new NodePerPortResolver() eagerly, at the time it is called, instead of the resolver being forced in build(). This matches the ordinary "last call wins" semantics of every other Builder setter, and matches upstream datastax/simulacron's behavior in the same method:

public Builder withMultipleNodesPerIp(boolean enabled) {
  this.multipleNodesPerIp = enabled;
  if (enabled) {
    this.addressResolver = AddressResolver.nodePerPortResolver;
  }
  return this;
}

With this change, a withAddressResolver(...) call made after withMultipleNodesPerIp(true) is honored, while a withMultipleNodesPerIp(true) call made after withAddressResolver(...) still overrides it with the default NodePerPortResolver (again matching upstream / normal builder semantics).

Tests

Added two unit tests to ServerTest:

  • testMultipleNodesPerIpShouldNotOverrideAddressResolverConfiguredAfterwards - builds a Server with withMultipleNodesPerIp(true) followed by a custom withAddressResolver(...), and asserts the custom resolver is actually used to assign the registered node's address (previously this would have gotten a NodePerPortResolver-assigned address instead).
  • testMultipleNodesPerIpShouldOverrideAddressResolverConfiguredBefore - confirms the reverse call order still installs a NodePerPortResolver by default.

Ran the full test suite locally (mvn test across all modules): 195 tests, 0 failures.

Context

Found while diagnosing a flaky Simulacron-backed integration test in scylladb/java-driver: scylladb/java-driver#1017. That PR was closed in favor of fixing the root cause here rather than working around it with retries on the driver side - retrying doesn't reliably help because every retry recreates a Server, whose build() always re-hardcoded NodePerPortResolver starting at the same two ports (49152/49153), which doesn't help against a port held in TIME_WAIT. Once this is released, a follow-up PR against java-driver will bump the Simulacron dependency and configure a collision-safe resolver alongside withMultipleNodesPerIp(true).

…olver

withMultipleNodesPerIp(true) always installed a fresh NodePerPortResolver
at build() time, discarding any resolver previously (or subsequently, once
withMultipleNodesPerIp had already been called) configured via
withAddressResolver(...) - regardless of the order builder methods were
called in.

Change withMultipleNodesPerIp(true) to set the NodePerPortResolver eagerly,
at the time it is called, instead of forcing it in build(). This matches
upstream datastax/simulacron semantics: normal builder "last call wins"
behavior, so a withAddressResolver(...) call made after
withMultipleNodesPerIp(true) is honored instead of silently discarded.

Found while diagnosing a flaky Simulacron-backed integration test in
scylladb/java-driver (scylladb/java-driver#1017), where a caller-supplied
collision-safe resolver was being replaced by a NodePerPortResolver that
always retries the same two ports (49152/49153).
withMultipleNodesPerIp(false) left whatever resolver a preceding
withMultipleNodesPerIp(true) had installed in place: a plain
enable-then-disable sequence left a stale NodePerPortResolver behind
even though multipleNodesPerIp was now false (inconsistent with the
PeerMetadataHandler(false) built alongside it), and
withAddressResolver(custom).withMultipleNodesPerIp(true).withMultipleNodesPerIp(false)
permanently lost the caller's custom resolver, which is a regression
versus pre-fix master (where the override was gated on the final
value of multipleNodesPerIp at build() time, so a final false never
triggered it).

Track the resolver most recently set via withAddressResolver(...)
separately from the one withMultipleNodesPerIp(true) auto-installs, so
withMultipleNodesPerIp(false) can restore it (or the true default when
none was ever set) instead of leaving the NodePerPortResolver in
place.

Addresses review feedback from dkropachev on PR scylladb#11.
@roydahan
roydahan requested a review from dkropachev August 26, 2026 11:41
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.

2 participants