Skip to content

Commit 516ef26

Browse files
committed
JVMCBC-1660 Change Network Heuristic for JVM SDKs
Motivation ---------- Consistent behavior between SDKs. Make certain private endpoint deployments work without the user having to explicitly specify `network=external`. Modifications ------------- When there's no exact match between a seed node and an address in the cluster topology, fall back to "external" network (if present) instead of "default". Throw an exception if the user explicitly specifies a non-existent network. This results in a `ConfigIgnoredEvent` in the log at WARN level with a message that includes: """ Requested network 'external' is not available for this cluster. Available networks: [default] """ CAVEATS ------- This is potentially a breaking behavior change. Upgrade with caution. Change-Id: I38675cd02456088662c29a4fa315720a651e65c1 Reviewed-on: https://review.couchbase.org/c/couchbase-jvm-clients/+/229917 Reviewed-by: Michael Reiche <[email protected]> Reviewed-by: Matt Ingenthron <[email protected]> Tested-by: Build Bot <[email protected]>
1 parent 83d7a27 commit 516ef26

File tree

5 files changed

+160
-21
lines changed

5 files changed

+160
-21
lines changed

core-io/src/main/java/com/couchbase/client/core/env/NetworkResolution.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
import java.util.Objects;
2020

21-
import static com.couchbase.client.core.util.CbStrings.isNullOrEmpty;
21+
import static com.couchbase.client.core.util.CbStrings.nullToEmpty;
2222
import static java.util.Objects.requireNonNull;
2323

2424
/**
@@ -64,7 +64,17 @@ public class NetworkResolution {
6464
* if the given name is null or empty.
6565
*/
6666
public static NetworkResolution valueOf(final String name) {
67-
return isNullOrEmpty(name) ? AUTO : new NetworkResolution(name);
67+
switch (nullToEmpty(name)) {
68+
case "":
69+
case "auto":
70+
return AUTO;
71+
72+
case "external":
73+
return EXTERNAL;
74+
75+
default:
76+
return new NetworkResolution(name);
77+
}
6878
}
6979

7080
/**

core-io/src/main/java/com/couchbase/client/core/topology/ClusterTopologyParser.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,14 @@ public static ClusterTopology parse(
7272
HostAndServicePortsParser.parse(addHostnameIfMissing(node, originHost), portSelector)
7373
);
7474

75-
NetworkResolution resolvedNetwork = networkSelector.selectNetwork(nodes)
76-
.orElse(NetworkResolution.DEFAULT); // Hope for the best!
75+
NetworkResolution resolvedNetwork = networkSelector.selectNetwork(nodes);
76+
77+
if (nodes.stream().noneMatch(it -> it.containsKey(resolvedNetwork))) {
78+
// User explicitly specified a network that doesn't exist.
79+
Set<NetworkResolution> availableNetworks = new HashSet<>();
80+
nodes.forEach(it -> availableNetworks.addAll(it.keySet()));
81+
throw new CouchbaseException("Requested network '" + resolvedNetwork + "' is not available for this cluster. Available networks: " + availableNetworks);
82+
}
7783

7884
// Discard node info from networks we don't care about.
7985
//

core-io/src/main/java/com/couchbase/client/core/topology/NetworkSelector.java

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@
2121
import com.couchbase.client.core.env.NetworkResolution;
2222
import com.couchbase.client.core.env.SeedNode;
2323
import org.jspecify.annotations.Nullable;
24+
import org.slf4j.Logger;
25+
import org.slf4j.LoggerFactory;
2426

2527
import java.util.List;
2628
import java.util.Map;
27-
import java.util.Optional;
2829
import java.util.Set;
2930

3031
import static com.couchbase.client.core.util.CbCollections.setCopyOf;
@@ -36,7 +37,7 @@
3637
@Stability.Internal
3738
public interface NetworkSelector {
3839

39-
Optional<NetworkResolution> selectNetwork(List<Map<NetworkResolution, HostAndServicePorts>> nodes);
40+
NetworkResolution selectNetwork(List<Map<NetworkResolution, HostAndServicePorts>> nodes);
4041

4142
/**
4243
* @param network The config parser's final output will include only addresses for the specified network.
@@ -54,8 +55,8 @@ static NetworkSelector create(NetworkResolution network, Set<SeedNode> seedNodes
5455

5556
return new NetworkSelector() {
5657
@Override
57-
public Optional<NetworkResolution> selectNetwork(List<Map<NetworkResolution, HostAndServicePorts>> nodes) {
58-
return Optional.of(network);
58+
public NetworkResolution selectNetwork(List<Map<NetworkResolution, HostAndServicePorts>> nodes) {
59+
return network;
5960
}
6061

6162
@Override
@@ -66,16 +67,18 @@ public String toString() {
6667

6768
}
6869

69-
@SuppressWarnings({"OptionalAssignedToNull", "OptionalUsedAsFieldOrParameterType"})
7070
class AutoNetworkSelector implements NetworkSelector {
71+
private static final Logger log = LoggerFactory.getLogger(AutoNetworkSelector.class);
72+
7173
private final Set<SeedNode> seedNodes;
72-
private @Nullable Optional<NetworkResolution> cachedResult; // @GuardedBy(this)
74+
private @Nullable NetworkResolution cachedResult; // @GuardedBy(this)
75+
private boolean usedFallback; // @GuardedBy(this)
7376

7477
public AutoNetworkSelector(Set<SeedNode> seedNodes) {
7578
this.seedNodes = setCopyOf(seedNodes);
7679
}
7780

78-
public synchronized Optional<NetworkResolution> selectNetwork(List<Map<NetworkResolution, HostAndServicePorts>> nodes) {
81+
public synchronized NetworkResolution selectNetwork(List<Map<NetworkResolution, HostAndServicePorts>> nodes) {
7982
if (cachedResult == null) {
8083
cachedResult = doSelectNetwork(nodes);
8184
}
@@ -86,11 +89,11 @@ public synchronized Optional<NetworkResolution> selectNetwork(List<Map<NetworkRe
8689
public synchronized String toString() {
8790
String network = cachedResult == null
8891
? "<TBD>"
89-
: cachedResult.map(NetworkResolution::name).orElse("no match -> default");
92+
: (usedFallback ? "no match -> " : "") + cachedResult.name();
9093
return "auto(" + network + "; seedNodes=" + seedNodes + ")";
9194
}
9295

93-
private Optional<NetworkResolution> doSelectNetwork(List<Map<NetworkResolution, HostAndServicePorts>> nodes) {
96+
private NetworkResolution doSelectNetwork(List<Map<NetworkResolution, HostAndServicePorts>> nodes) {
9497
// Search the given map for nodes whose host and KV or Manager port
9598
// match one of the addresses used to bootstrap the connection to the cluster.
9699
for (Map<NetworkResolution, HostAndServicePorts> node : nodes) {
@@ -99,14 +102,23 @@ private Optional<NetworkResolution> doSelectNetwork(List<Map<NetworkResolution,
99102
if (entry.getValue().matches(seedNode)) {
100103
// We bootstrapped using an address associated with this network+node,
101104
// so this is very likely the correct network.
102-
return Optional.of(entry.getKey());
105+
NetworkResolution exactMatch = entry.getKey();
106+
log.debug("Found exact match for {} in network '{}'", seedNode, exactMatch);
107+
return exactMatch;
103108
}
104109
}
105110
}
106111
}
107112

108113
// Didn't find a match.
109-
return Optional.empty();
114+
NetworkResolution fallback = nodes.stream().anyMatch(it -> it.containsKey(NetworkResolution.EXTERNAL))
115+
? NetworkResolution.EXTERNAL
116+
: NetworkResolution.DEFAULT;
117+
118+
log.info("Automatic network selection was requested, but no bootstrap address exactly matches an address in the cluster topology. Falling back to network: {}", fallback);
119+
120+
usedFallback = true;
121+
return fallback;
110122
}
111123
}
112124

core-io/src/test/java/com/couchbase/client/core/config/DefaultConfigurationProviderTest.java

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@
3030
import com.couchbase.client.core.msg.ResponseStatus;
3131
import com.couchbase.client.core.msg.kv.GetCollectionIdRequest;
3232
import com.couchbase.client.core.msg.kv.GetCollectionIdResponse;
33+
import com.couchbase.client.core.topology.ClusterTopology;
3334
import com.couchbase.client.core.topology.NodeIdentifier;
35+
import com.couchbase.client.core.topology.TopologyRevision;
3436
import com.couchbase.client.core.util.ConnectionString;
3537
import org.junit.jupiter.api.AfterAll;
3638
import org.junit.jupiter.api.AfterEach;
@@ -60,6 +62,7 @@
6062
import static java.util.stream.Collectors.toSet;
6163
import static org.junit.jupiter.api.Assertions.assertEquals;
6264
import static org.junit.jupiter.api.Assertions.assertFalse;
65+
import static org.junit.jupiter.api.Assertions.assertNotNull;
6366
import static org.junit.jupiter.api.Assertions.assertTrue;
6467
import static org.mockito.ArgumentMatchers.any;
6568
import static org.mockito.Mockito.doAnswer;
@@ -149,13 +152,23 @@ void canProposeNewBucketConfig() {
149152
provider.proposeBucketConfig(new ProposedBucketConfigContext(bucket, config, ORIGIN));
150153
assertEquals(1, configsPushed.get());
151154
assertFalse(provider.config().bucketConfigs().isEmpty());
152-
assertEquals(1073, provider.config().bucketConfig("default").rev());
153155

154-
assertEquals(3, getSeedNodesFromConfig(provider).size());
155-
for (SeedNode node : getSeedNodesFromConfig(provider)) {
156-
assertEquals(11210, node.kvPort().orElse(null));
157-
assertEquals(8091, node.clusterManagerPort().orElse(null));
158-
}
156+
ClusterTopology topology = provider.config().bucketTopology("default");
157+
assertNotNull(topology);
158+
159+
assertEquals(new TopologyRevision(0, 1073), topology.revision());
160+
161+
// No address in the default network matches a seed node, so the SDK should fall back to external network.
162+
assertEquals(NetworkResolution.EXTERNAL, topology.network());
163+
164+
assertEquals(
165+
setOf(
166+
SeedNode.create("192.168.132.234").withKvPort(32775).withManagerPort(32790),
167+
SeedNode.create("192.168.132.234").withKvPort(32799).withManagerPort(32814),
168+
SeedNode.create("192.168.132.234").withKvPort(32823).withManagerPort(32838)
169+
),
170+
getSeedNodesFromConfig(provider)
171+
);
159172
}
160173

161174
@Test
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Copyright 2025 Couchbase, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.couchbase.client.core.topology;
18+
19+
import com.couchbase.client.core.env.NetworkResolution;
20+
import com.couchbase.client.core.env.SeedNode;
21+
import com.couchbase.client.core.service.ServiceType;
22+
import org.junit.jupiter.api.Test;
23+
24+
import java.util.Set;
25+
26+
import static com.couchbase.client.core.util.CbCollections.listOf;
27+
import static com.couchbase.client.core.util.CbCollections.mapOf;
28+
import static com.couchbase.client.core.util.CbCollections.setOf;
29+
import static org.junit.jupiter.api.Assertions.assertEquals;
30+
31+
class NetworkSelectorTest {
32+
33+
private static HostAndServicePorts nodeWithHostAndKvPort(String host, int kvPort) {
34+
return new HostAndServicePorts(
35+
host,
36+
mapOf(
37+
ServiceType.KV, kvPort,
38+
ServiceType.MANAGER, 8091
39+
),
40+
NodeIdentifier.forBootstrap(host, 8091),
41+
null,
42+
null,
43+
null,
44+
null
45+
);
46+
}
47+
48+
@Test
49+
void autoSelectsDefaultIfExactMatch() {
50+
Set<SeedNode> seeds = setOf(SeedNode.create("example.com").withKvPort(11210));
51+
52+
NetworkResolution selected = NetworkSelector.autoDetect(seeds)
53+
.selectNetwork(listOf(
54+
mapOf(NetworkResolution.DEFAULT, nodeWithHostAndKvPort("example.com", 11210)),
55+
mapOf(NetworkResolution.EXTERNAL, nodeWithHostAndKvPort("example.com", 11210))
56+
));
57+
58+
assertEquals(NetworkResolution.DEFAULT, selected);
59+
}
60+
61+
@Test
62+
void autoSelectsExternalIfExactMatch() {
63+
Set<SeedNode> seeds = setOf(SeedNode.create("example.com").withKvPort(11210));
64+
65+
NetworkResolution selected = NetworkSelector.autoDetect(seeds)
66+
.selectNetwork(listOf(
67+
mapOf(NetworkResolution.DEFAULT, nodeWithHostAndKvPort("example.com", 1)),
68+
mapOf(NetworkResolution.EXTERNAL, nodeWithHostAndKvPort("example.com", 11210))
69+
));
70+
71+
assertEquals(NetworkResolution.EXTERNAL, selected);
72+
}
73+
74+
@Test
75+
void autoFallsBackToExternalIfPresent() {
76+
Set<SeedNode> seeds = setOf(SeedNode.create("example.com").withKvPort(11210));
77+
78+
NetworkResolution selected = NetworkSelector.autoDetect(seeds)
79+
.selectNetwork(listOf(
80+
mapOf(NetworkResolution.DEFAULT, nodeWithHostAndKvPort("example.com", 1)),
81+
mapOf(NetworkResolution.EXTERNAL, nodeWithHostAndKvPort("example.com", 2))
82+
));
83+
84+
assertEquals(NetworkResolution.EXTERNAL, selected);
85+
}
86+
87+
@Test
88+
void autoFallsBackToDefaultIfExternalIsAbsent() {
89+
Set<SeedNode> seeds = setOf(SeedNode.create("example.com").withKvPort(11210));
90+
91+
NetworkResolution selected = NetworkSelector.autoDetect(seeds)
92+
.selectNetwork(listOf(
93+
mapOf(NetworkResolution.DEFAULT, nodeWithHostAndKvPort("127.0.0.1", 1))
94+
));
95+
96+
assertEquals(NetworkResolution.DEFAULT, selected);
97+
}
98+
}

0 commit comments

Comments
 (0)