Skip to content

Commit 45eadfc

Browse files
committed
JVMCBC-1665 Let internal Couchbase products bypass cluster type check
Modifications ------------- Add AnalyticsAccessor.skipClusterTypeCheck(). Internal projects can call this to disable the check that prevents the operational SDK from executing queries against Enterprise Analytics. Model the `prodName` field of the cluster topology JSON as a new `ClusterType` class. Rename ClusterIdentifier.prodName() -> clusterType(), and modify it to always return a non-null value. If the value would previously have been null, assume ClusterType.COUCHBASE_SERVER. Extract the cluster type check into a new method: requireCouchbaseServer(). Change-Id: I389ccf6d64f780525b62a8d9cd3c261f2d146200 Reviewed-on: https://review.couchbase.org/c/couchbase-jvm-clients/+/229924 Reviewed-by: Michael Reiche <[email protected]> Tested-by: Build Bot <[email protected]>
1 parent 7d0e1f2 commit 45eadfc

File tree

5 files changed

+120
-63
lines changed

5 files changed

+120
-63
lines changed

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

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,35 +15,36 @@
1515
*/
1616
package com.couchbase.client.core.topology;
1717

18-
import com.couchbase.client.core.annotation.SinceCouchbase;
1918
import com.couchbase.client.core.annotation.Stability;
2019
import com.couchbase.client.core.deps.com.fasterxml.jackson.databind.JsonNode;
2120
import com.couchbase.client.core.deps.com.fasterxml.jackson.databind.node.ObjectNode;
2221
import org.jspecify.annotations.Nullable;
2322

2423
import static com.couchbase.client.core.logging.RedactableArgument.redactMeta;
24+
import static java.util.Objects.requireNonNull;
2525

2626
@Stability.Internal
2727
public class ClusterIdentifier {
2828
private final String clusterUuid;
2929
private final String clusterName;
30-
@SinceCouchbase("8.0")
31-
private final @Nullable String prodName;
30+
private final ClusterType clusterType;
3231

33-
ClusterIdentifier(String clusterUuid, String clusterName, @Nullable String prodName) {
34-
this.clusterUuid = clusterUuid;
35-
this.clusterName = clusterName;
36-
this.prodName = prodName;
32+
ClusterIdentifier(String clusterUuid, String clusterName, ClusterType product) {
33+
this.clusterUuid = requireNonNull(clusterUuid);
34+
this.clusterName = requireNonNull(clusterName);
35+
this.clusterType = requireNonNull(product);
3736
}
3837

3938
public static @Nullable ClusterIdentifier parse(ObjectNode config) {
4039
JsonNode clusterUuid = config.path("clusterUUID");
4140
JsonNode clusterName = config.path("clusterName");
42-
JsonNode prodName = config.path("prodName");
4341
if (clusterUuid.isMissingNode() || clusterName.isMissingNode()) {
4442
return null;
4543
}
46-
return new ClusterIdentifier(clusterUuid.asText(), clusterName.asText(), prodName.isMissingNode() ? null : prodName.asText());
44+
45+
JsonNode prodName = config.path("prodName"); // field added in Couchbase Server 8.0.
46+
ClusterType type = ClusterType.of(prodName.textValue());
47+
return new ClusterIdentifier(clusterUuid.asText(), clusterName.asText(), type);
4748
}
4849

4950
public String clusterUuid() {
@@ -54,16 +55,16 @@ public String clusterName() {
5455
return clusterName;
5556
}
5657

57-
public @Nullable String prodName() {
58-
return prodName;
58+
public ClusterType clusterType() {
59+
return clusterType;
5960
}
6061

6162
@Override
6263
public String toString() {
6364
return "ClusterIdent{" +
6465
"clusterUuid='" + clusterUuid + '\'' +
6566
", clusterName='" + redactMeta(clusterName) + '\'' +
66-
", prodName='" + prodName + '\'' +
67+
", clusterType='" + clusterType + '\'' +
6768
'}';
6869
}
6970
}

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

Lines changed: 0 additions & 27 deletions
This file was deleted.

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public final class ClusterTopologyBuilder {
4444
private NetworkResolution networkResolution = NetworkResolution.DEFAULT;
4545
private String clusterName = "fake-cluster";
4646
private String clusterUuid = "fake-cluster-uuid";
47-
private String prodName = ClusterProdName.COUCHBASE_SERVER;
47+
private ClusterType clusterType = ClusterType.COUCHBASE_SERVER;
4848
private Set<ClusterCapability> capabilities = EnumSet.allOf(ClusterCapability.class);
4949
private final List<HostAndServicePorts> nodes = new ArrayList<>();
5050

@@ -55,7 +55,7 @@ public ClusterTopology build() {
5555
private ClusterTopology buildWithOrWithoutBucket(@Nullable BucketTopology bucket) {
5656
return ClusterTopology.of(
5757
revision,
58-
new ClusterIdentifier(clusterUuid, clusterName, prodName),
58+
new ClusterIdentifier(clusterUuid, clusterName, clusterType),
5959
nodes,
6060
capabilities,
6161
networkResolution,
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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.annotation.Stability;
20+
import org.jspecify.annotations.Nullable;
21+
22+
import static java.util.Objects.requireNonNull;
23+
24+
/**
25+
* Sourced from the "prodName" (product name) field of the cluster topology JSON.
26+
*/
27+
@Stability.Internal
28+
public class ClusterType {
29+
private static final String COUCHBASE_SERVER_NAME = "Couchbase Server";
30+
31+
public static ClusterType COUCHBASE_SERVER = new ClusterType(COUCHBASE_SERVER_NAME);
32+
33+
private final String name;
34+
private final boolean couchbaseServer;
35+
36+
private ClusterType(String name) {
37+
this.name = requireNonNull(name);
38+
this.couchbaseServer = name.startsWith(COUCHBASE_SERVER_NAME);
39+
}
40+
41+
public boolean isCouchbaseServer() {
42+
return couchbaseServer;
43+
}
44+
45+
public static ClusterType from(@Nullable ClusterIdentifier id) {
46+
return id == null ? of(null) : id.clusterType();
47+
}
48+
49+
public static ClusterType of(@Nullable String name) {
50+
return name == null || name.equals(COUCHBASE_SERVER_NAME)
51+
? COUCHBASE_SERVER
52+
: new ClusterType(name);
53+
}
54+
55+
public String name() {
56+
return name;
57+
}
58+
59+
@Override
60+
public String toString() {
61+
return name;
62+
}
63+
}

java-client/src/main/java/com/couchbase/client/java/analytics/AnalyticsAccessor.java

Lines changed: 42 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,37 @@
1818

1919
import com.couchbase.client.core.Core;
2020
import com.couchbase.client.core.Reactor;
21+
import com.couchbase.client.core.annotation.Stability;
2122
import com.couchbase.client.core.error.CouchbaseException;
2223
import com.couchbase.client.core.msg.analytics.AnalyticsRequest;
2324
import com.couchbase.client.core.msg.analytics.AnalyticsResponse;
24-
import com.couchbase.client.core.topology.ClusterProdName;
25+
import com.couchbase.client.core.topology.ClusterType;
2526
import com.couchbase.client.java.codec.JsonSerializer;
2627
import reactor.core.publisher.Mono;
2728

29+
import java.time.Duration;
2830
import java.util.concurrent.CompletableFuture;
2931

3032
/**
3133
* Internal helper to map the results from the analytics requests.
3234
*
3335
* @since 3.0.0
3436
*/
37+
@Stability.Internal
3538
public class AnalyticsAccessor {
3639

40+
private static volatile boolean skipClusterTypeCheck = false;
41+
42+
/**
43+
* Call this method once when your app starts up to disable the check that
44+
* prevents the operational SDK from being used with an Enterprise Analytics cluster.
45+
* <p>
46+
* This is super-extra-unsupported internal API.
47+
*/
48+
public static void skipClusterTypeCheck() {
49+
skipClusterTypeCheck = true;
50+
}
51+
3752
public static CompletableFuture<AnalyticsResult> analyticsQueryAsync(final Core core,
3853
final AnalyticsRequest request,
3954
final JsonSerializer serializer) {
@@ -54,28 +69,33 @@ public static Mono<ReactiveAnalyticsResult> analyticsQueryReactive(final Core co
5469
}
5570

5671
private static Mono<AnalyticsResponse> analyticsQueryInternal(final Core core, final AnalyticsRequest request) {
57-
return core.waitForClusterTopology(request.timeout())
58-
.flatMap(clusterTopology -> {
72+
return requireCouchbaseServer(core, request.timeout())
73+
.then(Mono.defer(() -> {
74+
core.send(request);
75+
return Reactor.wrap(request, request.response(), true)
76+
.doOnNext(ignored -> request.context().logicallyComplete())
77+
.doOnError(err -> request.context().logicallyComplete(err));
78+
}));
79+
}
5980

60-
if (clusterTopology.id() != null
61-
&& clusterTopology.id().prodName() != null
62-
&& !clusterTopology.id().prodName().startsWith(ClusterProdName.COUCHBASE_SERVER)) {
63-
StringBuilder sb = new StringBuilder();
64-
sb.append("This '");
65-
sb.append(clusterTopology.id().prodName());
66-
sb.append("' cluster cannot be used with this SDK, which is intended for use with operational clusters");
67-
if (clusterTopology.id().prodName().startsWith(ClusterProdName.ENTERPRISE_ANALYTICS)) {
68-
sb.append(". For this cluster, an Enterprise Analytics SDK should be used.");
69-
}
70-
return Mono.error(new CouchbaseException(sb.toString()));
71-
}
81+
private static Mono<Void> requireCouchbaseServer(Core core, Duration timeout) {
82+
if (skipClusterTypeCheck) {
83+
return Mono.empty();
84+
}
7285

73-
return Mono.defer(() -> {
74-
core.send(request);
75-
return Reactor.wrap(request, request.response(), true);
76-
}).doOnNext(ignored -> request.context().logicallyComplete())
77-
.doOnError(err -> request.context().logicallyComplete(err));
78-
});
79-
}
86+
return core.waitForClusterTopology(timeout)
87+
.mapNotNull(clusterTopology -> {
88+
ClusterType type = ClusterType.from(clusterTopology.id());
89+
if (type.isCouchbaseServer()) {
90+
return null; // success! complete the empty mono.
91+
}
92+
93+
String message = "This SDK is for Couchbase Server (operational) clusters, but the remote cluster type is '" + type + "'.";
94+
if (type.name().startsWith("Enterprise Analytics")) {
95+
message += " Please use the Enterprise Analytics SDK to access this cluster.";
96+
}
8097

98+
throw new CouchbaseException(message);
99+
});
100+
}
81101
}

0 commit comments

Comments
 (0)