+ * When you're done interacting with the cluster, it's important to call
+ * {@link Cluster#close()} to release resources used by the cluster.
*/
+@ThreadSafe
public final class Cluster implements Closeable, Queryable {
private final Environment environment;
private final CoreCouchbaseOps couchbaseOps;
@@ -105,9 +114,21 @@ public static Cluster newInstance(
/**
* Returns a new instance, with options customized by the {@code optionsCustomizer} callback.
+ *
params = new LinkedHashMap<>(cs.params());
+
+ // "security.trust_only_non_prod" is special; it doesn't have a corresponding programmatic
+ // config option. It's not a secret, but we don't want to confuse external users with a
+ // security config option they never need to set.
+ boolean trustOnlyNonProdCertificates = lastTrustParamIsNonProd(params);
+
+ try {
+ BuilderPropertySetter propertySetter = new BuilderPropertySetter("", Collections.emptyMap(), Cluster::lowerSnakeCaseToLowerCamelCase);
+ propertySetter.set(builder, params);
+
+ } catch (InvalidPropertyException e) {
+ // Translate core-io exception (internal API) to platform exception!
+ throw new IllegalArgumentException(e.getMessage(), e.getCause());
+ }
+
+ // Do this last, after any other "trust_only_*" params are validated and applied.
+ // Otherwise, the earlier params would clobber the config set by this param.
+ // (There's no compelling use case for including multiple "trust_only_*" params in
+ // the connection string, but we behave consistently if someone tries it.)
+ if (trustOnlyNonProdCertificates) {
+ builder.security(it -> it.trustOnlyCertificates(Certificates.getNonProdCertificates()));
+ }
+ }
+
+ /**
+ * Returns true if the "security.trust_only_non_prod" connection string param is
+ * present, and no other trust params appear after it (since last one wins).
+ *
+ * Side effect: Removes that param from the map.
+ *
+ * @throws IllegalArgumentException if the param has an invalid value
+ */
+ private static boolean lastTrustParamIsNonProd(LinkedHashMap params) {
+ final String TRUST_ONLY_NON_PROD_PARAM = "security.trust_only_non_prod";
+
+ // Last trust param wins, so check whether "trust only non-prod" was last trust param.
+ boolean trustOnlyNonProdWasLast = params.keySet().stream()
+ .filter(it -> it.startsWith("security.trust_"))
+ .reduce((a, b) -> b) // last
+ .orElse("")
+ .equals(TRUST_ONLY_NON_PROD_PARAM);
+
+ // Always remove it, so later processing doesn't treat it as unrecognized param.
+ String trustOnlyNonProdValue = params.remove(TRUST_ONLY_NON_PROD_PARAM);
+
+ // Always validate if present, regardless of whether it was last.
+ if (trustOnlyNonProdValue != null && !Set.of("", "true", "1").contains(trustOnlyNonProdValue)) {
+ throw new IllegalArgumentException("Invalid value for connection string property '" + TRUST_ONLY_NON_PROD_PARAM + "'; expected 'true', '1', or empty string, but got: '" + trustOnlyNonProdValue + "'");
+ }
+
+ return trustOnlyNonProdWasLast;
+ }
+
+ private static void checkParameterNamesAreLowercase(ConnectionString cs) {
+ cs.params().keySet().stream()
+ .filter(Cluster::hasUppercase)
+ .findFirst()
+ .ifPresent(badName -> {
+ throw new IllegalArgumentException("Invalid connection string parameter '" + badName + "'. Please use lower_snake_case in connection string parameter names.");
+ });
+ }
+
+ private static boolean hasUppercase(String s) {
+ return s.codePoints().anyMatch(Character::isUpperCase);
+ }
+
+ private static String lowerSnakeCaseToLowerCamelCase(String s) {
+ StringBuilder sb = new StringBuilder();
+ int[] codePoints = s.codePoints().toArray();
+
+ boolean prevWasUnderscore = false;
+ for (int i : codePoints) {
+ if (i == '_') {
+ prevWasUnderscore = true;
+ continue;
+ }
+
+ if (prevWasUnderscore) {
+ i = Character.toUpperCase(i);
+ }
+ sb.appendCodePoint(i);
+ prevWasUnderscore = false;
+ }
+
+ return sb.toString();
+ }
+
private static CoreTransactionsConfig disableTransactionsCleanup() {
return new CoreTransactionsConfig(
DEFAULT_TRANSACTION_DURABILITY_LEVEL,
@@ -186,21 +298,6 @@ private static CoreTransactionsConfig disableTransactionsCleanup() {
);
}
- private static final String SYSTEM_PROPERTY_PREFIX = "com.couchbase.columnar.env.";
-
- /**
- * Returns a map of all system properties whose names start with the given prefix,
- * transformed to remove the prefix.
- */
- private static Map systemPropertyMap(String prefix) {
- return System.getProperties()
- .entrySet()
- .stream()
- .filter(entry -> entry.getKey() instanceof String && entry.getValue() instanceof String)
- .filter(entry -> ((String) entry.getKey()).startsWith(prefix))
- .collect(toMap(e -> ((String) e.getKey()).substring(prefix.length()), e -> (String) e.getValue()));
- }
-
/**
* @see #newInstance
*/
@@ -218,11 +315,11 @@ private Cluster(
this.queryExecutor = new QueryExecutor(core, environment, connectionString);
}
+ /**
+ * Releases resources and prevents further use of this object.
+ */
public void close() {
- close(environment.timeoutConfig().disconnectTimeout());
- }
-
- public void close(Duration timeout) {
+ Duration timeout = environment.timeoutConfig().disconnectTimeout();
disconnectInternal(disconnected, timeout, couchbaseOps, environment).block();
}
@@ -237,6 +334,15 @@ static Mono disconnectInternal(
.then(Mono.fromRunnable(() -> disconnected.set(true)));
}
+ /**
+ * Returns the database in this cluster with the given name.
+ *
+ * A database is a container for {@link Scope}s.
+ *
+ * If the database does not exist, this method still returns a
+ * non-null object, but operations using that object fail with
+ * an exception indicating the database does not exist.
+ */
public Database database(String name) {
return new Database(this, name);
}
diff --git a/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/ClusterOptions.java b/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/ClusterOptions.java
index 52c6e0151..d4e3df530 100644
--- a/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/ClusterOptions.java
+++ b/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/ClusterOptions.java
@@ -22,7 +22,13 @@
import java.util.function.Consumer;
+/**
+ * A mutable builder for configuring the cluster's behavior.
+ *
+ * @see Cluster#newInstance(String, Credential, Consumer)
+ */
public final class ClusterOptions {
+ boolean srv = true;
@Nullable Deserializer deserializer;
final SecurityOptions security = new SecurityOptions();
final TimeoutOptions timeout = new TimeoutOptions();
@@ -34,6 +40,15 @@ Unmodifiable build() {
return new Unmodifiable(this);
}
+ /**
+ * Specifies whether the SDK should treat the connection string address
+ * as a DNS SRV record. Defaults to true.
+ */
+ public ClusterOptions srv(boolean useDnsSrv) {
+ this.srv = useDnsSrv;
+ return this;
+ }
+
/**
* Sets the default deserializer for converting query result rows into Java objects.
*
@@ -65,10 +80,12 @@ static class Unmodifiable {
private final TimeoutOptions.Unmodifiable timeout;
private final SecurityOptions.Unmodifiable security;
private final Deserializer deserializer;
+ private final boolean srv;
Unmodifiable(ClusterOptions builder) {
this.timeout = builder.timeout.build();
this.security = builder.security.build();
+ this.srv = builder.srv;
this.deserializer = builder.deserializer != null
? builder.deserializer
@@ -87,12 +104,17 @@ public Deserializer deserializer() {
return deserializer;
}
+ public boolean srv() {
+ return srv;
+ }
+
@Override
public String toString() {
return "ClusterOptions{" +
"timeout=" + timeout +
", security=" + security +
", deserializer=" + deserializer +
+ ", srv=" + srv +
'}';
}
diff --git a/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/Credential.java b/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/Credential.java
index 7be3b46a0..bf7400451 100644
--- a/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/Credential.java
+++ b/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/Credential.java
@@ -20,6 +20,7 @@
import com.couchbase.client.core.env.Authenticator;
import com.couchbase.client.core.env.PasswordAuthenticator;
import com.couchbase.columnar.client.java.internal.DynamicAuthenticator;
+import com.couchbase.columnar.client.java.internal.ThreadSafe;
import java.util.function.Supplier;
@@ -34,6 +35,7 @@
* For advanced use cases involving dynamic credentials, see
* {@link Credential#ofDynamic(Supplier)}.
*/
+@ThreadSafe
public abstract class Credential {
/**
diff --git a/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/Database.java b/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/Database.java
index 12dad16e7..c108ce2e5 100644
--- a/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/Database.java
+++ b/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/Database.java
@@ -16,8 +16,14 @@
package com.couchbase.columnar.client.java;
+import com.couchbase.columnar.client.java.internal.ThreadSafe;
+
import static java.util.Objects.requireNonNull;
+/**
+ * Contains {@link Scope}s.
+ */
+@ThreadSafe
public final class Database {
private final Cluster cluster;
private final String name;
diff --git a/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/QueryMetadata.java b/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/QueryMetadata.java
index cd387032a..2e88b172c 100644
--- a/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/QueryMetadata.java
+++ b/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/QueryMetadata.java
@@ -19,6 +19,7 @@
import com.couchbase.client.core.error.ErrorCodeAndMessage;
import com.couchbase.client.core.msg.analytics.AnalyticsChunkHeader;
import com.couchbase.client.core.msg.analytics.AnalyticsChunkTrailer;
+import com.couchbase.columnar.client.java.internal.ThreadSafe;
import java.util.Collections;
import java.util.List;
@@ -27,6 +28,7 @@
/**
* Holds associated metadata returned by the server.
*/
+@ThreadSafe
public final class QueryMetadata {
private final AnalyticsChunkHeader header;
diff --git a/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/QueryMetrics.java b/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/QueryMetrics.java
index dbd853a41..2e5ac1853 100644
--- a/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/QueryMetrics.java
+++ b/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/QueryMetrics.java
@@ -21,6 +21,7 @@
import com.couchbase.client.core.json.Mapper;
import com.couchbase.client.core.util.Golang;
import com.couchbase.columnar.client.java.internal.JacksonTransformers;
+import com.couchbase.columnar.client.java.internal.ThreadSafe;
import java.io.IOException;
import java.time.Duration;
@@ -31,6 +32,7 @@
/**
* Holds the metrics as returned from an analytics response.
*/
+@ThreadSafe
public final class QueryMetrics {
/**
diff --git a/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/QueryResult.java b/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/QueryResult.java
index 5b6826366..8d52d4f57 100644
--- a/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/QueryResult.java
+++ b/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/QueryResult.java
@@ -18,11 +18,13 @@
import com.couchbase.client.core.msg.analytics.AnalyticsChunkHeader;
import com.couchbase.client.core.msg.analytics.AnalyticsChunkTrailer;
+import com.couchbase.columnar.client.java.internal.ThreadSafe;
import java.util.List;
import static com.couchbase.client.core.util.CbCollections.listCopyOf;
+@ThreadSafe(caveat = "Unless you modify the byte array returned by Row.bytes()")
public final class QueryResult {
private final List rows;
private final QueryMetadata metadata;
diff --git a/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/QueryWarning.java b/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/QueryWarning.java
index 0a7d94f15..ae3f9fe0c 100644
--- a/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/QueryWarning.java
+++ b/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/QueryWarning.java
@@ -17,6 +17,7 @@
package com.couchbase.columnar.client.java;
import com.couchbase.client.core.error.ErrorCodeAndMessage;
+import com.couchbase.columnar.client.java.internal.ThreadSafe;
import static java.util.Objects.requireNonNull;
@@ -25,6 +26,7 @@
*
* Note that warnings are not terminal errors, but hints from the engine that something went not as expected.
*/
+@ThreadSafe
public final class QueryWarning {
private final ErrorCodeAndMessage inner;
diff --git a/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/Queryable.java b/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/Queryable.java
index ebb1e85f9..d4b478b30 100644
--- a/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/Queryable.java
+++ b/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/Queryable.java
@@ -16,9 +16,12 @@
package com.couchbase.columnar.client.java;
+import com.couchbase.columnar.client.java.internal.ThreadSafe;
+
import java.util.concurrent.CancellationException;
import java.util.function.Consumer;
+@ThreadSafe
public interface Queryable {
/**
diff --git a/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/Row.java b/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/Row.java
index 3ed7620b6..6e43ec2ec 100644
--- a/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/Row.java
+++ b/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/Row.java
@@ -19,6 +19,7 @@
import com.couchbase.columnar.client.java.codec.Deserializer;
import com.couchbase.columnar.client.java.codec.TypeRef;
import com.couchbase.columnar.client.java.internal.InternalJacksonSerDes;
+import com.couchbase.columnar.client.java.internal.ThreadSafe;
import com.couchbase.columnar.client.java.json.JsonArray;
import com.couchbase.columnar.client.java.json.JsonObject;
import com.couchbase.columnar.client.java.json.JsonValue;
@@ -61,6 +62,7 @@
* @see ClusterOptions#deserializer(Deserializer)
* @see QueryOptions#deserializer(Deserializer)
*/
+@ThreadSafe(caveat = "Unless you modify the byte array returned by Row.bytes()")
public final class Row {
private final byte[] content;
private final Deserializer deserializer;
@@ -76,6 +78,8 @@ public final class Row {
/**
* Returns the raw content of the row, exactly as it was received from
* the server.
+ *
+ * This method returns the same array each time it is called.
*/
public byte[] bytes() {
return content;
diff --git a/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/Scope.java b/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/Scope.java
index 34c8cd05f..cf481004b 100644
--- a/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/Scope.java
+++ b/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/Scope.java
@@ -18,11 +18,13 @@
import com.couchbase.client.core.api.manager.CoreBucketAndScope;
+import com.couchbase.columnar.client.java.internal.ThreadSafe;
import java.util.function.Consumer;
import static java.util.Objects.requireNonNull;
+@ThreadSafe
public final class Scope implements Queryable {
private final Cluster cluster;
private final Database database;
diff --git a/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/SecurityOptions.java b/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/SecurityOptions.java
index 648afd4aa..3a2d0a9a1 100644
--- a/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/SecurityOptions.java
+++ b/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/SecurityOptions.java
@@ -36,7 +36,7 @@ public final class SecurityOptions {
private List cipherSuites = emptyList();
@Nullable private TrustSource trustSource = null;
- private boolean verifyServerCertificate = true;
+ private boolean disableServerCertificateVerification = false;
Unmodifiable build() {
return new Unmodifiable(this);
@@ -107,7 +107,7 @@ public SecurityOptions trustOnlyFactory(TrustManagerFactory factory) {
/**
* Server certification verification is enabled by default.
- * You can disable it by passing false to this method,
+ * You can disable it by passing true to this method,
* but you almost certainly shouldn't. Instead, call one of the
* {@code trust} methods to tell the SDK which certificates
* it should trust.
@@ -116,7 +116,7 @@ public SecurityOptions trustOnlyFactory(TrustManagerFactory factory) {
* because it exposes you to on-path attacks. Never do this in production.
* In fact, you probably shouldn't do it anywhere.
*
- * @param verify If false, the SDK does not verify the certificate
+ * @param disable If true, the SDK does not verify the certificate
* presented by the server.
* @see #trustOnlyPemFile(Path)
* @see #trustOnlyPemString(String)
@@ -126,8 +126,8 @@ public SecurityOptions trustOnlyFactory(TrustManagerFactory factory) {
* is almost always a bad idea.
*/
@Deprecated
- public SecurityOptions verifyServerCertificate(boolean verify) {
- this.verifyServerCertificate = verify;
+ public SecurityOptions disableServerCertificateVerification(boolean disable) {
+ this.disableServerCertificateVerification = disable;
return this;
}
@@ -142,9 +142,9 @@ static class Unmodifiable {
Unmodifiable(SecurityOptions builder) {
this.cipherSuites = builder.cipherSuites;
- this.trustSource = builder.verifyServerCertificate
- ? (builder.trustSource != null ? builder.trustSource : TrustSource.from(Certificates.getCapellaCertificates()))
- : TrustSource.insecure();
+ this.trustSource = builder.disableServerCertificateVerification
+ ? TrustSource.insecure()
+ : (builder.trustSource != null ? builder.trustSource : TrustSource.from(Certificates.getCapellaCertificates()));
}
public List cipherSuites() {
diff --git a/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/codec/Deserializer.java b/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/codec/Deserializer.java
index a047b787f..a879875b4 100644
--- a/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/codec/Deserializer.java
+++ b/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/codec/Deserializer.java
@@ -16,6 +16,7 @@
package com.couchbase.columnar.client.java.codec;
+import com.couchbase.columnar.client.java.internal.ThreadSafe;
import reactor.util.annotation.Nullable;
import java.io.IOException;
@@ -23,6 +24,7 @@
/**
* Converts query result rows into Java objects.
*/
+@ThreadSafe
public interface Deserializer {
/**
* Deserializes raw input into the target class.
diff --git a/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/codec/JacksonDeserializer.java b/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/codec/JacksonDeserializer.java
index 8b6e929d6..57900dac7 100644
--- a/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/codec/JacksonDeserializer.java
+++ b/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/codec/JacksonDeserializer.java
@@ -18,6 +18,9 @@
// CHECKSTYLE:OFF IllegalImport - Allow unbundled Jackson
+import com.couchbase.columnar.client.java.ClusterOptions;
+import com.couchbase.columnar.client.java.QueryOptions;
+import com.couchbase.columnar.client.java.internal.ThreadSafe;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -35,7 +38,11 @@
* .build();
* var deserializer = new JacksonDeserializer(mapper);
*
+ *
+ * @see ClusterOptions#deserializer(Deserializer)
+ * @see QueryOptions#deserializer(Deserializer)
*/
+@ThreadSafe
public final class JacksonDeserializer implements Deserializer {
private final ObjectMapper mapper;
diff --git a/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/codec/TypeRef.java b/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/codec/TypeRef.java
index 6d2ec0c7c..7716b98d1 100644
--- a/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/codec/TypeRef.java
+++ b/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/codec/TypeRef.java
@@ -16,6 +16,8 @@
package com.couchbase.columnar.client.java.codec;
+import com.couchbase.columnar.client.java.internal.ThreadSafe;
+
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
@@ -34,6 +36,7 @@
*
* Super Type Tokens.
*/
+@ThreadSafe
public abstract class TypeRef {
private final Type type;
diff --git a/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/codec/package-info.java b/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/codec/package-info.java
index 4ec46a559..5d794279d 100644
--- a/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/codec/package-info.java
+++ b/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/codec/package-info.java
@@ -14,6 +14,10 @@
* limitations under the License.
*/
+/**
+ * Classes that turn query result rows into Java objects
+ * when you call {@code row.as(Class)} or {@code row.as(TypeRef)}.
+ */
@NonNullApi
package com.couchbase.columnar.client.java.codec;
diff --git a/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/internal/ThreadSafe.java b/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/internal/ThreadSafe.java
new file mode 100644
index 000000000..927d4a1e0
--- /dev/null
+++ b/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/internal/ThreadSafe.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2024 Couchbase, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.couchbase.columnar.client.java.internal;
+
+import org.jetbrains.annotations.ApiStatus;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Documents that the annotated type is thread-safe.
+ *
+ * When applied to an interface or abstract class,
+ * documents that implementations and subclasses must be thread-safe.
+ *
+ * This annotation only provides information; nothing magical happens
+ * when it is present. The implementor of the annotated class/interface
+ * is responsible for ensuring it is actually thread-safe.
+ */
+@ApiStatus.Internal
+@Retention(RetentionPolicy.SOURCE)
+@Documented
+@Target(ElementType.TYPE)
+public @interface ThreadSafe {
+ String caveat() default "";
+}
diff --git a/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/package-info.java b/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/package-info.java
index 0954f8a5a..ec0440988 100644
--- a/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/package-info.java
+++ b/columnar-java-client/src/main/java/com/couchbase/columnar/client/java/package-info.java
@@ -14,6 +14,9 @@
* limitations under the License.
*/
+/**
+ * Start here by creating a {@link com.couchbase.columnar.client.java.Cluster}.
+ */
@NonNullApi
package com.couchbase.columnar.client.java;
diff --git a/columnar-java-client/src/test/java/com/couchbase/columnar/client/java/sandbox/Sandbox.java b/columnar-java-client/src/test/java/com/couchbase/columnar/client/java/sandbox/Sandbox.java
index 73efa5a2c..eca5731c4 100644
--- a/columnar-java-client/src/test/java/com/couchbase/columnar/client/java/sandbox/Sandbox.java
+++ b/columnar-java-client/src/test/java/com/couchbase/columnar/client/java/sandbox/Sandbox.java
@@ -35,7 +35,7 @@
public class Sandbox {
public static void main(String[] args) throws Exception {
- String connectionString = "couchbases://127.0.0.1?security.verifyServerCertificate=false";
+ String connectionString = "couchbases://127.0.0.1?security.disable_server_certificate_verification=true&srv=0";
String username = "Administrator";
String password = "password";
diff --git a/columnar-java-fit-performer/pom.xml b/columnar-java-fit-performer/pom.xml
index 987e796a4..8d0b9a187 100644
--- a/columnar-java-fit-performer/pom.xml
+++ b/columnar-java-fit-performer/pom.xml
@@ -7,7 +7,7 @@
com.couchbase.client
couchbase-jvm-clients
- 1.16.3
+ 1.16.6
columnar-java-fit-performer
diff --git a/columnar-java-fit-performer/src/main/java/com/couchbase/columnar/cluster/ColumnarClusterConnection.java b/columnar-java-fit-performer/src/main/java/com/couchbase/columnar/cluster/ColumnarClusterConnection.java
index 8061d410b..e1520e72b 100644
--- a/columnar-java-fit-performer/src/main/java/com/couchbase/columnar/cluster/ColumnarClusterConnection.java
+++ b/columnar-java-fit-performer/src/main/java/com/couchbase/columnar/cluster/ColumnarClusterConnection.java
@@ -87,9 +87,8 @@ public ColumnarClusterConnection(fit.columnar.ClusterNewInstanceRequest request,
if (secOptions.hasTrustOnlyPlatform()) {
sec.trustOnlyJvm();
}
- if (secOptions.hasVerifyServerCertificate()) {
- //noinspection deprecation
- sec.verifyServerCertificate(secOptions.getVerifyServerCertificate());
+ if (secOptions.hasDisableServerCertificateVerification()) {
+ sec.disableServerCertificateVerification(secOptions.getDisableServerCertificateVerification());
}
if (secOptions.getCipherSuitesCount() > 0) {
sec.cipherSuites(secOptions.getCipherSuitesList());
diff --git a/columnar-java-fit-performer/src/main/java/com/couchbase/columnar/content/ContentAsUtil.java b/columnar-java-fit-performer/src/main/java/com/couchbase/columnar/content/ContentAsUtil.java
index a36220268..57505d970 100644
--- a/columnar-java-fit-performer/src/main/java/com/couchbase/columnar/content/ContentAsUtil.java
+++ b/columnar-java-fit-performer/src/main/java/com/couchbase/columnar/content/ContentAsUtil.java
@@ -29,7 +29,8 @@ public static Try contentType(
fit.columnar.ContentAs contentAs,
Supplier asByteArray,
Supplier asList,
- Supplier asMap
+ Supplier asMap,
+ Supplier asString
) {
try {
if (contentAs.hasAsByteArray()) {
@@ -44,6 +45,10 @@ public static Try contentType(
return new Try<>(fit.columnar.ContentWas.newBuilder()
.setContentWasMap(ProtobufConversions.jsonObjectToStruct(asMap.get()))
.build());
+ } else if (contentAs.hasAsString()) {
+ return new Try<>(fit.columnar.ContentWas.newBuilder()
+ .setContentWasString(asString.get())
+ .build());
} else {
throw new UnsupportedOperationException("Java performer cannot handle contentAs " + contentAs);
}
diff --git a/columnar-java-fit-performer/src/main/java/com/couchbase/columnar/query/QueryOptionsUtil.java b/columnar-java-fit-performer/src/main/java/com/couchbase/columnar/query/QueryOptionsUtil.java
index dd9aa4a42..21511c46c 100644
--- a/columnar-java-fit-performer/src/main/java/com/couchbase/columnar/query/QueryOptionsUtil.java
+++ b/columnar-java-fit-performer/src/main/java/com/couchbase/columnar/query/QueryOptionsUtil.java
@@ -19,6 +19,7 @@
import com.couchbase.columnar.client.java.QueryOptions;
import com.couchbase.columnar.client.java.QueryPriority;
import com.couchbase.columnar.client.java.ScanConsistency;
+import com.couchbase.columnar.util.CustomDeserializer;
import com.couchbase.columnar.util.grpc.ProtobufConversions;
import reactor.util.annotation.Nullable;
@@ -60,6 +61,10 @@ public class QueryOptionsUtil {
if (opts.hasTimeout()) {
options.timeout(Duration.ofSeconds(opts.getTimeout().getSeconds()));
}
+ if (opts.hasDeserializer() && opts.getDeserializer().hasCustom()) {
+ CustomDeserializer customDeserializer = new CustomDeserializer();
+ options.deserializer(customDeserializer);
+ }
};
}
}
diff --git a/columnar-java-fit-performer/src/main/java/com/couchbase/columnar/query/QueryResultBufferedStreamer.java b/columnar-java-fit-performer/src/main/java/com/couchbase/columnar/query/QueryResultBufferedStreamer.java
index 78d77cdd7..dae3663ae 100644
--- a/columnar-java-fit-performer/src/main/java/com/couchbase/columnar/query/QueryResultBufferedStreamer.java
+++ b/columnar-java-fit-performer/src/main/java/com/couchbase/columnar/query/QueryResultBufferedStreamer.java
@@ -19,7 +19,7 @@
import com.couchbase.columnar.client.java.QueryResult;
import com.couchbase.columnar.client.java.Queryable;
import com.couchbase.columnar.client.java.Row;
-import com.couchbase.columnar.fit.core.util.ResultUtil;
+import com.couchbase.columnar.util.ResultUtil;
import fit.columnar.EmptyResultOrFailureResponse;
import fit.columnar.ExecuteQueryRequest;
import fit.columnar.QueryResultMetadataResponse;
diff --git a/columnar-java-fit-performer/src/main/java/com/couchbase/columnar/query/QueryResultPushBasedStreamer.java b/columnar-java-fit-performer/src/main/java/com/couchbase/columnar/query/QueryResultPushBasedStreamer.java
index 477f355c3..d868808df 100644
--- a/columnar-java-fit-performer/src/main/java/com/couchbase/columnar/query/QueryResultPushBasedStreamer.java
+++ b/columnar-java-fit-performer/src/main/java/com/couchbase/columnar/query/QueryResultPushBasedStreamer.java
@@ -23,7 +23,7 @@
import com.couchbase.columnar.client.java.json.JsonArray;
import com.couchbase.columnar.client.java.json.JsonObject;
import com.couchbase.columnar.content.ContentAsUtil;
-import com.couchbase.columnar.fit.core.util.ErrorUtil;
+import com.couchbase.columnar.util.ErrorUtil;
import fit.columnar.EmptyResultOrFailureResponse;
import fit.columnar.ExecuteQueryRequest;
import fit.columnar.QueryResultMetadataResponse;
@@ -152,10 +152,13 @@ private void handleRow(Row row) {
private RowProcessingResult processRow(Row row) {
if (executeQueryRequest.hasContentAs()) {
- var content = ContentAsUtil.contentType(executeQueryRequest.getContentAs(),
+ var content = ContentAsUtil.contentType(
+ executeQueryRequest.getContentAs(),
() -> row.bytes(),
() -> row.asNullable(JsonArray.class),
- () -> row.asNullable(JsonObject.class));
+ () -> row.asNullable(JsonObject.class),
+ () -> row.asNullable(String.class)
+ );
if (content.isSuccess()) {
return new RowProcessingResult(null, fit.columnar.QueryRowResponse.newBuilder()
diff --git a/columnar-java-fit-performer/src/main/java/com/couchbase/columnar/query/QueryRowUtil.java b/columnar-java-fit-performer/src/main/java/com/couchbase/columnar/query/QueryRowUtil.java
index 9f083c862..8932016d3 100644
--- a/columnar-java-fit-performer/src/main/java/com/couchbase/columnar/query/QueryRowUtil.java
+++ b/columnar-java-fit-performer/src/main/java/com/couchbase/columnar/query/QueryRowUtil.java
@@ -20,7 +20,7 @@
import com.couchbase.columnar.client.java.json.JsonArray;
import com.couchbase.columnar.client.java.json.JsonObject;
import com.couchbase.columnar.content.ContentAsUtil;
-import com.couchbase.columnar.fit.core.util.ErrorUtil;
+import com.couchbase.columnar.util.ErrorUtil;
import fit.columnar.QueryRowResponse;
import javax.annotation.Nullable;
@@ -36,7 +36,8 @@ public static RowProcessingResult processRow(fit.columnar.ExecuteQueryRequest ex
executeQueryRequest.getContentAs(),
() -> row.bytes(),
() -> row.asNullable(JsonArray.class),
- () -> row.asNullable(JsonObject.class)
+ () -> row.asNullable(JsonObject.class),
+ () -> row.asNullable(String.class)
);
if (content.isSuccess()) {
diff --git a/columnar-java-fit-performer/src/main/java/com/couchbase/columnar/rpc/JavaColumnarCrossService.java b/columnar-java-fit-performer/src/main/java/com/couchbase/columnar/rpc/JavaColumnarCrossService.java
index 1336aa867..87aebf4bf 100644
--- a/columnar-java-fit-performer/src/main/java/com/couchbase/columnar/rpc/JavaColumnarCrossService.java
+++ b/columnar-java-fit-performer/src/main/java/com/couchbase/columnar/rpc/JavaColumnarCrossService.java
@@ -21,8 +21,8 @@
import com.couchbase.columnar.query.ExecuteQueryStreamer;
import com.couchbase.columnar.query.QueryResultBufferedStreamer;
import com.couchbase.columnar.query.QueryResultPushBasedStreamer;
-import com.couchbase.columnar.fit.core.util.ErrorUtil;
-import com.couchbase.columnar.fit.core.util.ResultUtil;
+import com.couchbase.columnar.util.ErrorUtil;
+import com.couchbase.columnar.util.ResultUtil;
import fit.columnar.CloseAllQueryResultsRequest;
import fit.columnar.CloseQueryResultRequest;
import fit.columnar.ColumnarCrossServiceGrpc;
diff --git a/columnar-java-fit-performer/src/main/java/com/couchbase/columnar/rpc/JavaColumnarService.java b/columnar-java-fit-performer/src/main/java/com/couchbase/columnar/rpc/JavaColumnarService.java
index 255ee38ba..1b5c316dc 100644
--- a/columnar-java-fit-performer/src/main/java/com/couchbase/columnar/rpc/JavaColumnarService.java
+++ b/columnar-java-fit-performer/src/main/java/com/couchbase/columnar/rpc/JavaColumnarService.java
@@ -19,15 +19,16 @@
import com.couchbase.client.protocol.shared.EchoRequest;
import com.couchbase.client.protocol.shared.EchoResponse;
import com.couchbase.columnar.cluster.ColumnarClusterConnection;
-import com.couchbase.columnar.modes.Mode;
import com.couchbase.columnar.fit.core.exceptions.ExceptionGrpcMappingUtil;
-import com.couchbase.columnar.fit.core.util.ResultUtil;
import com.couchbase.columnar.fit.core.util.VersionUtil;
+import com.couchbase.columnar.modes.Mode;
+import com.couchbase.columnar.util.ResultUtil;
import fit.columnar.CloseAllColumnarClustersRequest;
import fit.columnar.ClusterCloseRequest;
import fit.columnar.ClusterNewInstanceRequest;
import fit.columnar.ColumnarServiceGrpc;
import fit.columnar.EmptyResultOrFailureResponse;
+import fit.columnar.SdkConnectionError;
import io.grpc.stub.StreamObserver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -53,23 +54,32 @@ public void fetchPerformerCaps(fit.columnar.FetchPerformerCapsRequest request, S
builder.setSdkVersion(sdkVersion);
}
builder.setSdk(fit.columnar.SDK.SDK_JAVA);
- builder.putClusterNewInstance(0, fit.columnar.PerApiElementGeneric.getDefaultInstance());
- builder.putClusterClose(0, fit.columnar.PerApiElementGeneric.getDefaultInstance());
+ builder.putClusterNewInstance(0, fit.columnar.PerApiElementClusterNewInstance.getDefaultInstance());
+ builder.putClusterClose(0, fit.columnar.PerApiElementClusterClose.getDefaultInstance());
// The SDK has two main modes: buffered and push-based streaming.
var executeQueryBuffered = fit.columnar.PerApiElementExecuteQuery.newBuilder()
.setExecuteQueryReturns(fit.columnar.PerApiElementExecuteQuery.ExecuteQueryReturns.EXECUTE_QUERY_RETURNS_QUERY_RESULT)
.setRowIteration(fit.columnar.PerApiElementExecuteQuery.RowIteration.ROW_ITERATION_BUFFERED)
.setRowDeserialization(fit.columnar.PerApiElementExecuteQuery.RowDeserialization.ROW_DESERIALIZATION_STATIC_ROW_TYPING_INDIVIDUAL)
+ .setSupportsCustomDeserializer(true)
.build();
var executeQueryPushBased = fit.columnar.PerApiElementExecuteQuery.newBuilder()
.setExecuteQueryReturns(fit.columnar.PerApiElementExecuteQuery.ExecuteQueryReturns.EXECUTE_QUERY_RETURNS_QUERY_METADATA)
.setRowIteration(fit.columnar.PerApiElementExecuteQuery.RowIteration.ROW_ITERATION_STREAMING_PUSH_BASED)
.setRowDeserialization(fit.columnar.PerApiElementExecuteQuery.RowDeserialization.ROW_DESERIALIZATION_STATIC_ROW_TYPING_INDIVIDUAL)
+ .setSupportsCustomDeserializer(true)
.build();
builder.putClusterExecuteQuery(Mode.PUSH_BASED_STREAMING.ordinal(), executeQueryPushBased);
builder.putClusterExecuteQuery(Mode.BUFFERED.ordinal(), executeQueryBuffered);
builder.putScopeExecuteQuery(Mode.PUSH_BASED_STREAMING.ordinal(), executeQueryPushBased);
builder.putScopeExecuteQuery(Mode.BUFFERED.ordinal(), executeQueryBuffered);
+ for (Mode mode : new Mode[]{Mode.PUSH_BASED_STREAMING, Mode.BUFFERED}) {
+ builder.putSdkConnectionError(mode.ordinal(), SdkConnectionError.newBuilder()
+ .setInvalidCredErrorType(SdkConnectionError.InvalidCredentialErrorType.AS_INVALID_CREDENTIAL_EXCEPTION)
+ .setBootstrapErrorType(SdkConnectionError.BootstrapErrorType.ERROR_AS_TIMEOUT_EXCEPTION)
+ .build()
+ );
+ }
responseObserver.onNext(builder.build());
responseObserver.onCompleted();
} catch (RuntimeException err) {
diff --git a/columnar-java-fit-performer/src/main/java/com/couchbase/columnar/util/CustomDeserializer.java b/columnar-java-fit-performer/src/main/java/com/couchbase/columnar/util/CustomDeserializer.java
new file mode 100644
index 000000000..2454fd1fb
--- /dev/null
+++ b/columnar-java-fit-performer/src/main/java/com/couchbase/columnar/util/CustomDeserializer.java
@@ -0,0 +1,41 @@
+package com.couchbase.columnar.util;
+
+import com.couchbase.columnar.client.java.codec.Deserializer;
+import com.couchbase.columnar.client.java.codec.TypeRef;
+import com.couchbase.columnar.client.java.json.JsonObject;
+
+/**
+ * CustomJsonDeserializer provides a generic implementation of the Deserializer interface.
+ *
+ * This deserializer is designed to handle the conversion of Java objects to String format
+ * and back, with an additional boolean flag ("Serialized": false) that indicates whether
+ * the object has been deserialized. The flag is included in the JSON payload and then
+ * converted to string, making it easy to track the deserialization state of objects.
+ *
+ * Use Cases:
+ * - This deserializer can be used in scenarios where you need to deserialize
+ * objects while keeping track of their deserialization state.
+ *
+ * Limitations:
+ * - The current implementation assumes that the input objects can be deserialized into
+ * string format. Complex or non-standard objects may require additional handling.
+ * - The `deserialize` methods in this implementation modify the original JSON object
+ * by setting the `Serialized` flag to `false`, which might not be suitable for
+ * all use cases.
+ */
+
+public class CustomDeserializer implements Deserializer {
+ @Override
+ public T deserialize(Class target, byte[] input) {
+ JsonObject obj = JsonObject.fromJson(input);
+ obj.put("Serialized", false);
+ return (T) obj.toString();
+ }
+
+ @Override
+ public T deserialize(TypeRef target, byte[] input) {
+ JsonObject obj = JsonObject.fromJson(input);
+ obj.put("Serialized", false);
+ return (T) obj.toString();
+ }
+}
diff --git a/columnar-fit-performer-shared/src/main/java/com/couchbase/columnar/fit/core/util/ErrorUtil.java b/columnar-java-fit-performer/src/main/java/com/couchbase/columnar/util/ErrorUtil.java
similarity index 51%
rename from columnar-fit-performer-shared/src/main/java/com/couchbase/columnar/fit/core/util/ErrorUtil.java
rename to columnar-java-fit-performer/src/main/java/com/couchbase/columnar/util/ErrorUtil.java
index 4ab993939..166c9a061 100644
--- a/columnar-fit-performer-shared/src/main/java/com/couchbase/columnar/fit/core/util/ErrorUtil.java
+++ b/columnar-java-fit-performer/src/main/java/com/couchbase/columnar/util/ErrorUtil.java
@@ -13,26 +13,24 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.couchbase.columnar.fit.core.util;
+package com.couchbase.columnar.util;
-import fit.columnar.ColumnarErrorType;
+import com.couchbase.columnar.client.java.InvalidCredentialException;
+import com.couchbase.columnar.client.java.QueryException;
+import com.couchbase.columnar.client.java.TimeoutException;
import fit.columnar.PlatformErrorType;
-import javax.annotation.Nullable;
-
public class ErrorUtil {
private ErrorUtil() {
throw new AssertionError("not instantiable");
}
- private static @Nullable fit.columnar.ColumnarErrorType convertColumnarError(Throwable exception) {
+ private static boolean isColumnarError(Throwable exception) {
String simpleName = exception.getClass().getSimpleName();
return switch (simpleName) {
- case "QueryException" -> ColumnarErrorType.COLUMNAR_EXCEPTION_QUERY;
- case "InvalidCredentialException" -> ColumnarErrorType.COLUMNAR_EXCEPTION_INVALID_CREDENTIAL;
- case "TimeoutException" -> ColumnarErrorType.COLUMNAR_EXCEPTION_TIMEOUT;
- default -> null;
+ case "QueryException", "InvalidCredentialException", "TimeoutException", "ColumnarException" -> true;
+ default -> false;
};
}
@@ -45,12 +43,29 @@ private static fit.columnar.PlatformErrorType convertPlatformError(Throwable exc
public static fit.columnar.Error convertError(Throwable raw) {
var ret = fit.columnar.Error.newBuilder();
- var type = ErrorUtil.convertColumnarError(raw);
-
- if (type != null) {
+ if (isColumnarError(raw)) {
var out = fit.columnar.ColumnarError.newBuilder()
- .setType(type)
- .setAsString(raw.toString());
+ .setAsString(raw.toString());
+
+ if (raw instanceof QueryException queryException) {
+ out.setSubException(fit.columnar.SubColumnarError.newBuilder().setQueryException(
+ fit.columnar.QueryException.newBuilder()
+ .setErrorCode(queryException.code())
+ .setServerMessage(queryException.serverMessage())
+ .build())
+ .build());
+ }
+ if (raw instanceof InvalidCredentialException) {
+ out.setSubException(fit.columnar.SubColumnarError.newBuilder().setInvalidCredentialException(
+ fit.columnar.InvalidCredentialException.newBuilder().build())
+ .build());
+ }
+
+ if (raw instanceof TimeoutException) {
+ out.setSubException(fit.columnar.SubColumnarError.newBuilder().setTimeoutException(fit.columnar.TimeoutException.newBuilder().build())
+ .build());
+ }
+
if (raw.getCause() != null) {
out.setCause(convertError(raw.getCause()));
}
@@ -58,8 +73,8 @@ public static fit.columnar.Error convertError(Throwable raw) {
ret.setColumnar(out);
} else {
ret.setPlatform(fit.columnar.PlatformError.newBuilder()
- .setType(convertPlatformError(raw))
- .setAsString(raw.toString()));
+ .setType(convertPlatformError(raw))
+ .setAsString(raw.toString()));
}
return ret.build();
diff --git a/columnar-java-fit-performer/src/main/java/com/couchbase/columnar/util/ResultUtil.java b/columnar-java-fit-performer/src/main/java/com/couchbase/columnar/util/ResultUtil.java
new file mode 100644
index 000000000..0bfbb30fd
--- /dev/null
+++ b/columnar-java-fit-performer/src/main/java/com/couchbase/columnar/util/ResultUtil.java
@@ -0,0 +1,34 @@
+package com.couchbase.columnar.util;
+
+import com.couchbase.columnar.fit.core.util.StartTimes;
+import fit.columnar.EmptyResultOrFailureResponse;
+import fit.columnar.ResponseMetadata;
+
+public class ResultUtil {
+ public static EmptyResultOrFailureResponse success(StartTimes startTime) {
+ return fit.columnar.EmptyResultOrFailureResponse.newBuilder()
+ .setEmptySuccess(true)
+ .setMetadata(responseMetadata(startTime))
+ .build();
+ }
+
+ public static EmptyResultOrFailureResponse failure(Throwable err, StartTimes startTime) {
+ return fit.columnar.EmptyResultOrFailureResponse.newBuilder()
+ .setMetadata(responseMetadata(startTime))
+ .setError(ErrorUtil.convertError(err))
+ .build();
+ }
+
+ public static ResponseMetadata responseMetadata(StartTimes startTime) {
+ if (startTime != null) {
+ return fit.columnar.ResponseMetadata.newBuilder()
+ .setElapsedNanos(System.nanoTime() - startTime.asSystem())
+ .setInitiated(startTime.asWallclock())
+ .build();
+ }
+ else {
+ // todo remove when fix timings
+ return fit.columnar.ResponseMetadata.newBuilder().build();
+ }
+ }
+}
diff --git a/config/checkstyle/checkstyle-basic.xml b/config/checkstyle/checkstyle-basic.xml
index 952ef6a7e..18b9ebcf2 100644
--- a/config/checkstyle/checkstyle-basic.xml
+++ b/config/checkstyle/checkstyle-basic.xml
@@ -16,6 +16,10 @@
+
+
+
+ 1.16.6
fit-performer-core
diff --git a/core-fit-performer/src/main/java/com/couchbase/client/performer/core/CorePerformer.java b/core-fit-performer/src/main/java/com/couchbase/client/performer/core/CorePerformer.java
index ee3fd614f..0cc3ad993 100644
--- a/core-fit-performer/src/main/java/com/couchbase/client/performer/core/CorePerformer.java
+++ b/core-fit-performer/src/main/java/com/couchbase/client/performer/core/CorePerformer.java
@@ -64,6 +64,7 @@ public void performerCapsFetch(PerformerCapsFetchRequest request, StreamObserver
.addPerformerCaps(Caps.GRPC_TESTING)
// Add any shared caps here that all 3 performers possess:
.addPerformerCaps(Caps.KV_SUPPORT_1)
+ .addPerformerCaps(Caps.TXN_CLIENT_CONTEXT_ID_SUPPORT)
.addSdkImplementationCaps(com.couchbase.client.protocol.sdk.Caps.WAIT_UNTIL_READY)
.addSdkImplementationCaps(com.couchbase.client.protocol.sdk.Caps.PROTOSTELLAR)
.addSdkImplementationCaps(com.couchbase.client.protocol.sdk.Caps.SDK_SEARCH_RFC_REVISION_11)
diff --git a/core-io-deps/pom.xml b/core-io-deps/pom.xml
index a0607e84a..5b50c4741 100644
--- a/core-io-deps/pom.xml
+++ b/core-io-deps/pom.xml
@@ -6,7 +6,7 @@
com.couchbase.client
core-io-deps
- 1.7.3
+ 1.7.6
jar
Couchbase JVM Core IO Dependencies
@@ -19,7 +19,7 @@
UTF-8
- 4.1.112.Final
+ 4.1.115.Final
2.17.2
com.couchbase.client.core.deps.
@@ -79,7 +79,7 @@
org.jctools
jctools-core
- 4.0.1
+ 4.0.5
com.fasterxml.jackson.core
diff --git a/core-io/pom.xml b/core-io/pom.xml
index d5ca3814a..c3d40dcd7 100644
--- a/core-io/pom.xml
+++ b/core-io/pom.xml
@@ -7,11 +7,11 @@
com.couchbase.client
couchbase-jvm-clients
- 1.16.3
+ 1.16.6
core-io
- 3.7.3
+ 3.7.6
diff --git a/core-io/src/integrationTest/java/com/couchbase/client/core/config/loader/ClusterManagerBucketLoaderIntegrationTest.java b/core-io/src/integrationTest/java/com/couchbase/client/core/config/loader/ClusterManagerBucketLoaderIntegrationTest.java
index 1728058ee..3a94cb781 100644
--- a/core-io/src/integrationTest/java/com/couchbase/client/core/config/loader/ClusterManagerBucketLoaderIntegrationTest.java
+++ b/core-io/src/integrationTest/java/com/couchbase/client/core/config/loader/ClusterManagerBucketLoaderIntegrationTest.java
@@ -22,7 +22,7 @@
import com.couchbase.client.core.config.ProposedBucketConfigContext;
import com.couchbase.client.core.diagnostics.ClusterState;
import com.couchbase.client.core.env.CoreEnvironment;
-import com.couchbase.client.core.node.NodeIdentifier;
+import com.couchbase.client.core.topology.NodeIdentifier;
import com.couchbase.client.core.service.ServiceType;
import com.couchbase.client.core.util.ConfigWaitHelper;
import com.couchbase.client.core.util.CoreIntegrationTest;
diff --git a/core-io/src/integrationTest/java/com/couchbase/client/core/config/loader/GlobalLoaderIntegrationTest.java b/core-io/src/integrationTest/java/com/couchbase/client/core/config/loader/GlobalLoaderIntegrationTest.java
index 34c6475df..0b5c3724c 100644
--- a/core-io/src/integrationTest/java/com/couchbase/client/core/config/loader/GlobalLoaderIntegrationTest.java
+++ b/core-io/src/integrationTest/java/com/couchbase/client/core/config/loader/GlobalLoaderIntegrationTest.java
@@ -19,7 +19,7 @@
import com.couchbase.client.core.Core;
import com.couchbase.client.core.config.ProposedGlobalConfigContext;
import com.couchbase.client.core.env.CoreEnvironment;
-import com.couchbase.client.core.node.NodeIdentifier;
+import com.couchbase.client.core.topology.NodeIdentifier;
import com.couchbase.client.core.util.ConfigWaitHelper;
import com.couchbase.client.core.util.CoreIntegrationTest;
import com.couchbase.client.test.Capabilities;
diff --git a/core-io/src/integrationTest/java/com/couchbase/client/core/config/loader/KeyValueBucketLoaderIntegrationTest.java b/core-io/src/integrationTest/java/com/couchbase/client/core/config/loader/KeyValueBucketLoaderIntegrationTest.java
index 5ebd665e6..2c92f7924 100644
--- a/core-io/src/integrationTest/java/com/couchbase/client/core/config/loader/KeyValueBucketLoaderIntegrationTest.java
+++ b/core-io/src/integrationTest/java/com/couchbase/client/core/config/loader/KeyValueBucketLoaderIntegrationTest.java
@@ -19,7 +19,7 @@
import com.couchbase.client.core.Core;
import com.couchbase.client.core.config.ProposedBucketConfigContext;
import com.couchbase.client.core.env.CoreEnvironment;
-import com.couchbase.client.core.node.NodeIdentifier;
+import com.couchbase.client.core.topology.NodeIdentifier;
import com.couchbase.client.core.util.ConfigWaitHelper;
import com.couchbase.client.core.util.CoreIntegrationTest;
import com.couchbase.client.test.Services;
diff --git a/core-io/src/integrationTest/java/com/couchbase/client/core/config/refresher/GlobalBucketRefresherIntegrationTest.java b/core-io/src/integrationTest/java/com/couchbase/client/core/config/refresher/GlobalBucketRefresherIntegrationTest.java
index 11d2eebb8..c667e8eb8 100644
--- a/core-io/src/integrationTest/java/com/couchbase/client/core/config/refresher/GlobalBucketRefresherIntegrationTest.java
+++ b/core-io/src/integrationTest/java/com/couchbase/client/core/config/refresher/GlobalBucketRefresherIntegrationTest.java
@@ -22,7 +22,7 @@
import com.couchbase.client.core.config.ProposedGlobalConfigContext;
import com.couchbase.client.core.config.loader.GlobalLoader;
import com.couchbase.client.core.env.CoreEnvironment;
-import com.couchbase.client.core.node.NodeIdentifier;
+import com.couchbase.client.core.topology.NodeIdentifier;
import com.couchbase.client.core.util.CoreIntegrationTest;
import com.couchbase.client.test.Capabilities;
import com.couchbase.client.test.IgnoreWhen;
diff --git a/core-io/src/integrationTest/resources/integration.properties b/core-io/src/integrationTest/resources/integration.properties
index f151f3c17..6bb61ccea 100644
--- a/core-io/src/integrationTest/resources/integration.properties
+++ b/core-io/src/integrationTest/resources/integration.properties
@@ -11,7 +11,7 @@ cluster.adminPassword=password
# Default configs for the mocked environment
cluster.mocked.numNodes=1
-cluster.mocked.numReplicas=1
+cluster.mocked.numReplicas=0
# Entry point configuration if not managed
# value of hostname and ns_server port
diff --git a/core-io/src/main/java/com/couchbase/client/core/Core.java b/core-io/src/main/java/com/couchbase/client/core/Core.java
index ece1848f8..cde50fa38 100644
--- a/core-io/src/main/java/com/couchbase/client/core/Core.java
+++ b/core-io/src/main/java/com/couchbase/client/core/Core.java
@@ -36,6 +36,7 @@
import com.couchbase.client.core.cnc.CbTracing;
import com.couchbase.client.core.cnc.Event;
import com.couchbase.client.core.cnc.EventBus;
+import com.couchbase.client.core.cnc.RequestTracer;
import com.couchbase.client.core.cnc.TracingIdentifiers;
import com.couchbase.client.core.cnc.ValueRecorder;
import com.couchbase.client.core.cnc.events.core.BucketClosedEvent;
@@ -53,11 +54,9 @@
import com.couchbase.client.core.cnc.events.core.WatchdogRunFailedEvent;
import com.couchbase.client.core.cnc.events.transaction.TransactionsStartedEvent;
import com.couchbase.client.core.cnc.metrics.LoggingMeter;
-import com.couchbase.client.core.config.BucketConfig;
import com.couchbase.client.core.config.ClusterConfig;
import com.couchbase.client.core.config.ConfigurationProvider;
import com.couchbase.client.core.config.DefaultConfigurationProvider;
-import com.couchbase.client.core.config.GlobalConfig;
import com.couchbase.client.core.diagnostics.ClusterState;
import com.couchbase.client.core.diagnostics.EndpointDiagnostics;
import com.couchbase.client.core.diagnostics.InternalEndpointDiagnostics;
@@ -65,12 +64,14 @@
import com.couchbase.client.core.endpoint.http.CoreHttpClient;
import com.couchbase.client.core.env.Authenticator;
import com.couchbase.client.core.env.CoreEnvironment;
+import com.couchbase.client.core.env.RequestTracerDecorator;
import com.couchbase.client.core.env.SeedNode;
import com.couchbase.client.core.error.AlreadyShutdownException;
import com.couchbase.client.core.error.ConfigException;
import com.couchbase.client.core.error.GlobalConfigNotFoundException;
import com.couchbase.client.core.error.InvalidArgumentException;
import com.couchbase.client.core.error.RequestCanceledException;
+import com.couchbase.client.core.error.UnambiguousTimeoutException;
import com.couchbase.client.core.error.UnsupportedConfigMechanismException;
import com.couchbase.client.core.io.CollectionIdentifier;
import com.couchbase.client.core.manager.CoreBucketManagerOps;
@@ -87,29 +88,37 @@
import com.couchbase.client.core.node.KeyValueLocator;
import com.couchbase.client.core.node.Locator;
import com.couchbase.client.core.node.Node;
-import com.couchbase.client.core.node.NodeIdentifier;
import com.couchbase.client.core.node.RoundRobinLocator;
import com.couchbase.client.core.node.ViewLocator;
import com.couchbase.client.core.service.ServiceScope;
import com.couchbase.client.core.service.ServiceState;
import com.couchbase.client.core.service.ServiceType;
+import com.couchbase.client.core.topology.ClusterIdentifier;
+import com.couchbase.client.core.topology.ClusterIdentifierUtil;
+import com.couchbase.client.core.topology.ClusterTopology;
+import com.couchbase.client.core.topology.ClusterTopologyWithBucket;
+import com.couchbase.client.core.topology.NodeIdentifier;
import com.couchbase.client.core.transaction.cleanup.CoreTransactionsCleanup;
import com.couchbase.client.core.transaction.components.CoreTransactionRequest;
import com.couchbase.client.core.transaction.context.CoreTransactionsContext;
import com.couchbase.client.core.util.ConnectionString;
import com.couchbase.client.core.util.CoreIdGenerator;
+import com.couchbase.client.core.util.Deadline;
import com.couchbase.client.core.util.LatestStateSubscription;
import com.couchbase.client.core.util.NanoTimestamp;
import reactor.core.Disposable;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.annotation.Nullable;
+import reactor.util.retry.Retry;
import java.time.Duration;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
@@ -233,7 +242,8 @@ public class Core implements CoreCouchbaseOps, AutoCloseable {
private final Disposable invalidStateWatchdog;
/**
- * Holds the response metrics per
+ * Holds the response metrics.
+ * Note that because tags have to be provided on ValueRecorder creation, every unique combination of tags needs to be represented in the ResponseMetricIdentifier key.
*/
private final Map responseMetrics = new ConcurrentHashMap<>();
@@ -243,6 +253,8 @@ public class Core implements CoreCouchbaseOps, AutoCloseable {
private final ConnectionString connectionString;
+ private final CoreResources coreResources;
+
/**
* @deprecated Please use {@link #create(CoreEnvironment, Authenticator, ConnectionString)} instead.
*/
@@ -280,6 +292,24 @@ protected Core(
CoreLimiter.incrementAndVerifyNumInstances(environment.eventBus());
this.connectionString = requireNonNull(connectionString);
+ boolean ignoresAttributes = CbTracing.isInternalTracer(environment.requestTracer());
+ RequestTracer requestTracerDecoratedIfRequired = ignoresAttributes
+ ? environment.requestTracer()
+ : new RequestTracerDecorator(environment.requestTracer(), () -> {
+ if (currentConfig == null) {
+ return null;
+ }
+ if (currentConfig.globalConfig() == null) {
+ return null;
+ }
+ return currentConfig.globalConfig().clusterIdent();
+ });
+ this.coreResources = new CoreResources() {
+ @Override
+ public RequestTracer requestTracer() {
+ return requestTracerDecoratedIfRequired;
+ }
+ };
this.coreContext = new CoreContext(this, CoreIdGenerator.nextId(), environment, authenticator);
this.configurationProvider = createConfigurationProvider();
this.nodes = new CopyOnWriteArrayList<>();
@@ -319,7 +349,7 @@ protected Core(
);
this.transactionsCleanup = new CoreTransactionsCleanup(this, environment.transactionsConfig());
- this.transactionsContext = new CoreTransactionsContext(environment.meter());
+ this.transactionsContext = new CoreTransactionsContext(this, environment.meter());
context().environment().eventBus().publish(new TransactionsStartedEvent(environment.transactionsConfig().cleanupConfig().runLostAttemptsCleanupThread(),
environment.transactionsConfig().cleanupConfig().runRegularAttemptsCleanupThread()));
}
@@ -525,6 +555,32 @@ private Mono closeBucket(final String name) {
});
}
+ @Stability.Internal
+ public Mono waitForClusterTopology(Duration timeout) {
+ return Mono.defer(() -> {
+ Deadline deadline = Deadline.of(timeout);
+
+ return Mono.fromCallable(() -> {
+ ClusterTopology globalTopology = clusterConfig().globalTopology();
+ if (globalTopology != null) {
+ return globalTopology;
+ }
+
+ for (ClusterTopologyWithBucket topology : clusterConfig().bucketTopologies()) {
+ return topology;
+ }
+
+ throw deadline.exceeded()
+ ? new UnambiguousTimeoutException("Timed out while waiting for cluster topology", null)
+ : new NoSuchElementException(); // trigger retry!
+ })
+ .retryWhen(Retry
+ .fixedDelay(Long.MAX_VALUE, Duration.ofMillis(100))
+ .filter(t -> t instanceof NoSuchElementException)
+ );
+ });
+ }
+
/**
* This method can be used by a caller to make sure a certain service is enabled at the given
* target node.
@@ -574,9 +630,10 @@ public ValueRecorder responseMetric(final Request> request, @Nullable Throwabl
}
}
final String finalExceptionSimpleName = exceptionSimpleName;
+ final ClusterIdentifier clusterIdent = ClusterIdentifierUtil.fromConfig(currentConfig);
- return responseMetrics.computeIfAbsent(new ResponseMetricIdentifier(request, exceptionSimpleName), key -> {
- Map tags = new HashMap<>(7);
+ return responseMetrics.computeIfAbsent(new ResponseMetricIdentifier(request, exceptionSimpleName, clusterIdent), key -> {
+ Map tags = new HashMap<>(9);
if (key.serviceType == null) {
// Virtual service
if (request instanceof CoreTransactionRequest) {
@@ -590,9 +647,21 @@ public ValueRecorder responseMetric(final Request> request, @Nullable Throwabl
// The LoggingMeter only uses the service and operation labels, so optimise this hot-path by skipping
// assigning other labels.
if (!isDefaultLoggingMeter) {
- tags.put(TracingIdentifiers.ATTR_NAME, key.bucketName);
- tags.put(TracingIdentifiers.ATTR_SCOPE, key.scopeName);
- tags.put(TracingIdentifiers.ATTR_COLLECTION, key.collectionName);
+ // Crucial note for Micrometer:
+ // If we are ever going to output an attribute from a given JVM run then we must always
+ // output that attribute in this run. Specifying null as an attribute value allows the OTel backend to strip it, and
+ // the Micrometer backend to provide a default value.
+ // See (internal to Couchbase) discussion here for full details:
+ // https://issues.couchbase.com/browse/CBSE-17070?focusedId=779820&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-779820
+ // If this rule is not followed, then Micrometer will silently discard some metrics. Micrometer requires that
+ // every value output under a given metric has the same set of attributes.
+
+ tags.put(TracingIdentifiers.ATTR_NAME, key.bucketName);
+ tags.put(TracingIdentifiers.ATTR_SCOPE, key.scopeName);
+ tags.put(TracingIdentifiers.ATTR_COLLECTION, key.collectionName);
+
+ tags.put(TracingIdentifiers.ATTR_CLUSTER_UUID, key.clusterUuid);
+ tags.put(TracingIdentifiers.ATTR_CLUSTER_NAME, key.clusterName);
if (finalExceptionSimpleName != null) {
tags.put(TracingIdentifiers.ATTR_OUTCOME, finalExceptionSimpleName);
@@ -626,24 +695,11 @@ protected Node createNode(final NodeIdentifier identifier) {
*/
private Mono maybeRemoveNode(final Node node, final ClusterConfig config) {
return Mono.defer(() -> {
- boolean stillPresentInBuckets = config
- .bucketConfigs()
- .values()
- .stream()
- .flatMap(bc -> bc.nodes().stream())
- .anyMatch(ni -> ni.identifier().equals(node.identifier()));
-
-
- boolean stillPresentInGlobal;
- if (config.globalConfig() != null) {
- stillPresentInGlobal = config
- .globalConfig()
- .portInfos()
- .stream()
- .anyMatch(ni -> ni.identifier().equals(node.identifier()));
- } else {
- stillPresentInGlobal = false;
- }
+ boolean stillPresentInBuckets = config.bucketTopologies().stream()
+ .anyMatch(topology -> hasNode(topology, node.identifier()));
+
+ ClusterTopology globalTopology = config.globalTopology();
+ boolean stillPresentInGlobal = globalTopology != null && hasNode(globalTopology, node.identifier());
if ((!stillPresentInBuckets && !stillPresentInGlobal) || !node.hasServicesEnabled()) {
return node.disconnect().doOnTerminate(() -> nodes.remove(node));
@@ -653,6 +709,11 @@ private Mono maybeRemoveNode(final Node node, final ClusterConfig config)
});
}
+ private static boolean hasNode(ClusterTopology topology, NodeIdentifier nodeId) {
+ return topology.nodes().stream()
+ .anyMatch(node -> node.id().equals(nodeId));
+ }
+
/**
* This method is used to remove a service from a node.
*
@@ -688,7 +749,7 @@ public Mono shutdown(final Duration timeout) {
invalidStateWatchdog.dispose();
return Flux
- .fromIterable(currentConfig.bucketConfigs().keySet())
+ .fromIterable(currentConfig.bucketNames())
.flatMap(this::closeBucket)
.then(configurationProvider.shutdown())
.then(configurationProcessor.awaitTermination())
@@ -717,18 +778,18 @@ public Mono shutdown(final Duration timeout) {
private void reconfigure(Runnable doFinally) {
final ClusterConfig configForThisAttempt = currentConfig;
- if (configForThisAttempt.bucketConfigs().isEmpty() && configForThisAttempt.globalConfig() == null) {
+ final ClusterTopology globalTopology = configForThisAttempt.globalTopology();
+ final Collection bucketTopologies = configForThisAttempt.bucketTopologies();
+
+ if (bucketTopologies.isEmpty() && globalTopology == null) {
reconfigureDisconnectAll(doFinally);
return;
}
final NanoTimestamp start = NanoTimestamp.now();
- Flux bucketConfigFlux = Flux
- .just(configForThisAttempt)
- .flatMap(cc -> Flux.fromIterable(cc.bucketConfigs().values()));
- reconfigureBuckets(bucketConfigFlux)
- .then(reconfigureGlobal(configForThisAttempt.globalConfig()))
+ reconfigureBuckets(Flux.fromIterable(bucketTopologies))
+ .then(reconfigureGlobal(globalTopology))
.then(Mono.defer(() ->
Flux
.fromIterable(new ArrayList<>(nodes))
@@ -781,115 +842,75 @@ private void reconfigureDisconnectAll(Runnable doFinally) {
);
}
- private Mono reconfigureGlobal(final GlobalConfig config) {
- return Mono.defer(() -> {
- if (config == null) {
- return Mono.empty();
- }
-
- return Flux
- .fromIterable(config.portInfos())
- .flatMap(ni -> {
- boolean tls = coreContext.environment().securityConfig().tlsEnabled();
-
- final Map services = tls ? ni.sslPorts() : ni.ports();
-
- Flux serviceRemoveFlux = Flux
- .fromArray(ServiceType.values())
- .filter(s -> !services.containsKey(s))
- .flatMap(s -> removeServiceFrom(
- ni.identifier(),
- s,
- Optional.empty())
- .onErrorResume(throwable -> {
- eventBus.publish(new ServiceReconfigurationFailedEvent(
- coreContext,
- ni.hostname(),
- s,
- throwable
- ));
- return Mono.empty();
- })
- );
-
-
- Flux serviceAddFlux = Flux
- .fromIterable(services.entrySet())
- .flatMap(s -> ensureServiceAt(
- ni.identifier(),
- s.getKey(),
- s.getValue(),
- Optional.empty())
- .onErrorResume(throwable -> {
- eventBus.publish(new ServiceReconfigurationFailedEvent(
- coreContext,
- ni.hostname(),
- s.getKey(),
- throwable
- ));
- return Mono.empty();
- })
- );
-
- return Flux.merge(serviceAddFlux, serviceRemoveFlux);
- })
- .then();
- });
+ private Mono reconfigureGlobal(final @Nullable ClusterTopology topology) {
+ return topology == null
+ ? Mono.empty()
+ : reconfigureGlobalOrBucket(topology, null);
}
/**
* Contains logic to perform reconfiguration for a bucket config.
*
- * @param bucketConfigs the flux of bucket configs currently open.
+ * @param bucketTopologies the flux of topologies from currently open buckets
* @return a mono once reconfiguration for all buckets is complete
*/
- private Mono reconfigureBuckets(final Flux bucketConfigs) {
- return bucketConfigs.flatMap(bc ->
- Flux.fromIterable(bc.nodes())
- .flatMap(ni -> {
- boolean tls = coreContext.environment().securityConfig().tlsEnabled();
-
- final Map services = tls ? ni.sslServices() : ni.services();
-
- Flux serviceRemoveFlux = Flux
- .fromArray(ServiceType.values())
- .filter(s -> !services.containsKey(s))
- .flatMap(s -> removeServiceFrom(
- ni.identifier(),
+ private Mono reconfigureBuckets(final Flux bucketTopologies) {
+ return bucketTopologies.flatMap(bc -> reconfigureGlobalOrBucket(bc, bc.bucket().name()))
+ .then();
+ }
+
+ /**
+ * @param bucketName pass non-null if using the topology to configure bucket-scoped services.
+ *
+ * @implNote Maybe in the future we can inspect the ClusterTopology to see if it has a BucketTopology,
+ * and get the bucket name from there. However, let's make it explicit for now; this leaves the door open
+ * to using a ClusterTopologyWithBucket to configure global services (by passing a null bucket name).
+ */
+ private Mono reconfigureGlobalOrBucket(
+ ClusterTopology topology,
+ @Nullable String bucketName
+ ) {
+ return Flux.fromIterable(topology.nodes())
+ .flatMap(ni -> {
+ Flux serviceRemoveFlux = Flux
+ .fromArray(ServiceType.values())
+ .filter(s -> !ni.has(s))
+ .flatMap(s -> removeServiceFrom(
+ ni.id(),
s,
- s.scope() == ServiceScope.BUCKET ? Optional.of(bc.name()) : Optional.empty())
+ s.scope() == ServiceScope.BUCKET ? Optional.ofNullable(bucketName) : Optional.empty())
.onErrorResume(throwable -> {
eventBus.publish(new ServiceReconfigurationFailedEvent(
coreContext,
- ni.hostname(),
+ ni.host(),
s,
throwable
));
return Mono.empty();
})
- );
+ );
- Flux serviceAddFlux = Flux
- .fromIterable(services.entrySet())
- .flatMap(s -> ensureServiceAt(
- ni.identifier(),
+ Flux serviceAddFlux = Flux
+ .fromIterable(ni.ports().entrySet())
+ .flatMap(s -> ensureServiceAt(
+ ni.id(),
s.getKey(),
s.getValue(),
- s.getKey().scope() == ServiceScope.BUCKET ? Optional.of(bc.name()) : Optional.empty())
+ s.getKey().scope() == ServiceScope.BUCKET ? Optional.ofNullable(bucketName) : Optional.empty())
.onErrorResume(throwable -> {
eventBus.publish(new ServiceReconfigurationFailedEvent(
coreContext,
- ni.hostname(),
+ ni.host(),
s.getKey(),
throwable
));
return Mono.empty();
})
- );
+ );
- return Flux.merge(serviceAddFlux, serviceRemoveFlux);
- })
- ).then();
+ return Flux.merge(serviceAddFlux, serviceRemoveFlux);
+ })
+ .then();
}
/**
@@ -986,6 +1007,12 @@ public CoreEnvironment environment() {
return context().environment();
}
+ @Stability.Internal
+ @Override
+ public CoreResources coreResources() {
+ return coreResources;
+ }
+
@Override
public CompletableFuture waitUntilReady(
Set serviceTypes,
@@ -1005,8 +1032,10 @@ public static class ResponseMetricIdentifier {
private final @Nullable String scopeName;
private final @Nullable String collectionName;
private final @Nullable String exceptionSimpleName;
+ private final @Nullable String clusterName;
+ private final @Nullable String clusterUuid;
- ResponseMetricIdentifier(final Request> request, @Nullable String exceptionSimpleName) {
+ ResponseMetricIdentifier(final Request> request, @Nullable String exceptionSimpleName, @Nullable ClusterIdentifier clusterIdent) {
this.exceptionSimpleName = exceptionSimpleName;
if (request.serviceType() == null) {
if (request instanceof CoreTransactionRequest) {
@@ -1019,6 +1048,8 @@ public static class ResponseMetricIdentifier {
this.serviceType = CbTracing.getTracingId(request.serviceType());
}
this.requestName = request.name();
+ this.clusterName = clusterIdent == null ? null : clusterIdent.clusterName();
+ this.clusterUuid = clusterIdent == null ? null : clusterIdent.clusterUuid();
if (request instanceof KeyValueRequest) {
KeyValueRequest> kv = (KeyValueRequest>) request;
bucketName = request.bucket();
@@ -1053,6 +1084,8 @@ public ResponseMetricIdentifier(final String serviceType, final String requestNa
this.scopeName = null;
this.collectionName = null;
this.exceptionSimpleName = null;
+ this.clusterName = null;
+ this.clusterUuid = null;
}
public String serviceType() {
@@ -1073,12 +1106,14 @@ public boolean equals(Object o) {
&& Objects.equals(bucketName, that.bucketName)
&& Objects.equals(scopeName, that.scopeName)
&& Objects.equals(collectionName, that.collectionName)
- && Objects.equals(exceptionSimpleName, that.exceptionSimpleName);
+ && Objects.equals(exceptionSimpleName, that.exceptionSimpleName)
+ && Objects.equals(clusterName, that.clusterName)
+ && Objects.equals(clusterUuid, that.clusterUuid);
}
@Override
public int hashCode() {
- return Objects.hash(serviceType, requestName, bucketName, scopeName, collectionName, exceptionSimpleName);
+ return Objects.hash(serviceType, requestName, bucketName, scopeName, collectionName, exceptionSimpleName, clusterName, clusterUuid);
}
}
diff --git a/core-io/src/main/java/com/couchbase/client/core/CoreContext.java b/core-io/src/main/java/com/couchbase/client/core/CoreContext.java
index 62654bf68..6c264a4c4 100644
--- a/core-io/src/main/java/com/couchbase/client/core/CoreContext.java
+++ b/core-io/src/main/java/com/couchbase/client/core/CoreContext.java
@@ -81,6 +81,11 @@ public CoreEnvironment environment() {
return env;
}
+ @Stability.Internal
+ public CoreResources coreResources() {
+ return core.coreResources();
+ }
+
/**
* @deprecated Always return an empty optional. Alternate addresses
* are now resolved immediately when parsing cluster topology.
diff --git a/core-io/src/main/java/com/couchbase/client/core/CoreProtostellar.java b/core-io/src/main/java/com/couchbase/client/core/CoreProtostellar.java
index 0c2bef921..b33babf8a 100644
--- a/core-io/src/main/java/com/couchbase/client/core/CoreProtostellar.java
+++ b/core-io/src/main/java/com/couchbase/client/core/CoreProtostellar.java
@@ -83,7 +83,8 @@ public CoreProtostellar(
final Authenticator authenticator,
final ConnectionString connectionString
) {
- this.ctx = new ProtostellarContext(env, authenticator);
+ CoreResources coreResources = () -> env.requestTracer();
+ this.ctx = new ProtostellarContext(env, authenticator, coreResources);
notNull(connectionString, "connectionString");
checkConnectionStringScheme(connectionString, ConnectionString.Scheme.COUCHBASE2);
@@ -208,6 +209,11 @@ public CoreEnvironment environment() {
return context().environment();
}
+ @Override
+ public CoreResources coreResources() {
+ return context().coreResources();
+ }
+
@Override
public CompletableFuture waitUntilReady(
Set serviceTypes,
diff --git a/core-io/src/main/java/com/couchbase/client/core/CoreResources.java b/core-io/src/main/java/com/couchbase/client/core/CoreResources.java
new file mode 100644
index 000000000..bade79739
--- /dev/null
+++ b/core-io/src/main/java/com/couchbase/client/core/CoreResources.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2024 Couchbase, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.couchbase.client.core;
+
+import com.couchbase.client.core.annotation.Stability;
+import com.couchbase.client.core.api.CoreCouchbaseOps;
+import com.couchbase.client.core.cnc.RequestTracer;
+
+/**
+ * Resources that are owned by a {@link CoreCouchbaseOps}. (E.g. either a {@link Core} or {@link CoreProtostellar}.
+ *
+ * It is explicitly not owned by a CoreEnvironment, which can be shared between multiple Cluster objects, and so is not suitable for any information
+ * tied to a CoreCouchbaseOps.
+ *
+ * Consider preferring adding new resources here rather than into the *Environment objects.
+ */
+@Stability.Internal
+public interface CoreResources {
+ RequestTracer requestTracer();
+}
diff --git a/core-io/src/main/java/com/couchbase/client/core/annotation/UsedBy.java b/core-io/src/main/java/com/couchbase/client/core/annotation/UsedBy.java
index 153f49f15..4a879be47 100644
--- a/core-io/src/main/java/com/couchbase/client/core/annotation/UsedBy.java
+++ b/core-io/src/main/java/com/couchbase/client/core/annotation/UsedBy.java
@@ -33,6 +33,7 @@
Project value();
enum Project {
- SPRING_DATA_COUCHBASE
+ SPRING_DATA_COUCHBASE,
+ QUARKUS_COUCHBASE
}
}
diff --git a/core-io/src/main/java/com/couchbase/client/core/api/CoreCouchbaseOps.java b/core-io/src/main/java/com/couchbase/client/core/api/CoreCouchbaseOps.java
index df349580b..f6097e69c 100644
--- a/core-io/src/main/java/com/couchbase/client/core/api/CoreCouchbaseOps.java
+++ b/core-io/src/main/java/com/couchbase/client/core/api/CoreCouchbaseOps.java
@@ -19,6 +19,7 @@
import com.couchbase.client.core.Core;
import com.couchbase.client.core.CoreKeyspace;
import com.couchbase.client.core.CoreProtostellar;
+import com.couchbase.client.core.CoreResources;
import com.couchbase.client.core.annotation.Stability;
import com.couchbase.client.core.api.kv.CoreKvBinaryOps;
import com.couchbase.client.core.api.kv.CoreKvOps;
@@ -67,6 +68,8 @@ public interface CoreCouchbaseOps {
CoreEnvironment environment();
+ CoreResources coreResources();
+
CompletableFuture waitUntilReady(
Set serviceTypes,
Duration timeout,
diff --git a/core-io/src/main/java/com/couchbase/client/core/api/kv/CoreKvOps.java b/core-io/src/main/java/com/couchbase/client/core/api/kv/CoreKvOps.java
index afa4d703e..d720e9417 100644
--- a/core-io/src/main/java/com/couchbase/client/core/api/kv/CoreKvOps.java
+++ b/core-io/src/main/java/com/couchbase/client/core/api/kv/CoreKvOps.java
@@ -328,23 +328,27 @@ default Mono subdocGetReactive(
Flux subdocGetAllReplicasReactive(
CoreCommonOptions common,
String key,
- List commands
+ List commands,
+ CoreReadPreference readPreference
);
Mono subdocGetAnyReplicaReactive(
CoreCommonOptions common,
String key,
- List commands
+ List commands,
+ CoreReadPreference readPreference
);
Flux getAllReplicasReactive(
CoreCommonOptions common,
- String key
+ String key,
+ CoreReadPreference readPreference
);
Mono getAnyReplicaReactive(
CoreCommonOptions common,
- String key
+ String key,
+ CoreReadPreference readPreference
);
CoreAsyncResponse subdocMutateAsync(
diff --git a/core-io/src/main/java/com/couchbase/client/core/api/kv/CoreReadPreference.java b/core-io/src/main/java/com/couchbase/client/core/api/kv/CoreReadPreference.java
new file mode 100644
index 000000000..0ff983d50
--- /dev/null
+++ b/core-io/src/main/java/com/couchbase/client/core/api/kv/CoreReadPreference.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2024 Couchbase, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.couchbase.client.core.api.kv;
+
+public enum CoreReadPreference {
+ NO_PREFERENCE,
+ PREFERRED_SERVER_GROUP
+}
diff --git a/core-io/src/main/java/com/couchbase/client/core/api/query/CoreQueryOps.java b/core-io/src/main/java/com/couchbase/client/core/api/query/CoreQueryOps.java
index 9bb4965be..439916ea2 100644
--- a/core-io/src/main/java/com/couchbase/client/core/api/query/CoreQueryOps.java
+++ b/core-io/src/main/java/com/couchbase/client/core/api/query/CoreQueryOps.java
@@ -17,7 +17,7 @@
import com.couchbase.client.core.annotation.Stability;
import com.couchbase.client.core.api.kv.CoreAsyncResponse;
-import com.couchbase.client.core.node.NodeIdentifier;
+import com.couchbase.client.core.topology.NodeIdentifier;
import reactor.core.publisher.Mono;
import reactor.util.annotation.Nullable;
diff --git a/core-io/src/main/java/com/couchbase/client/core/api/query/CoreQueryResult.java b/core-io/src/main/java/com/couchbase/client/core/api/query/CoreQueryResult.java
index 19a997d17..f98c25dcc 100644
--- a/core-io/src/main/java/com/couchbase/client/core/api/query/CoreQueryResult.java
+++ b/core-io/src/main/java/com/couchbase/client/core/api/query/CoreQueryResult.java
@@ -18,7 +18,7 @@
import com.couchbase.client.core.annotation.Stability;
import com.couchbase.client.core.msg.query.QueryChunkRow;
-import com.couchbase.client.core.node.NodeIdentifier;
+import com.couchbase.client.core.topology.NodeIdentifier;
import reactor.util.annotation.Nullable;
import java.util.List;
diff --git a/core-io/src/main/java/com/couchbase/client/core/api/query/CoreReactiveQueryResult.java b/core-io/src/main/java/com/couchbase/client/core/api/query/CoreReactiveQueryResult.java
index 6e149d3a0..793d7e041 100644
--- a/core-io/src/main/java/com/couchbase/client/core/api/query/CoreReactiveQueryResult.java
+++ b/core-io/src/main/java/com/couchbase/client/core/api/query/CoreReactiveQueryResult.java
@@ -18,7 +18,7 @@
import com.couchbase.client.core.annotation.Stability;
import com.couchbase.client.core.msg.query.QueryChunkRow;
-import com.couchbase.client.core.node.NodeIdentifier;
+import com.couchbase.client.core.topology.NodeIdentifier;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.annotation.Nullable;
diff --git a/core-io/src/main/java/com/couchbase/client/core/api/search/ClassicCoreSearchOps.java b/core-io/src/main/java/com/couchbase/client/core/api/search/ClassicCoreSearchOps.java
index b3418961a..fd711dc28 100644
--- a/core-io/src/main/java/com/couchbase/client/core/api/search/ClassicCoreSearchOps.java
+++ b/core-io/src/main/java/com/couchbase/client/core/api/search/ClassicCoreSearchOps.java
@@ -17,6 +17,7 @@
package com.couchbase.client.core.api.search;
import com.couchbase.client.core.Core;
+import com.couchbase.client.core.CoreResources;
import com.couchbase.client.core.annotation.Stability;
import com.couchbase.client.core.api.kv.CoreAsyncResponse;
import com.couchbase.client.core.api.manager.CoreBucketAndScope;
@@ -158,6 +159,10 @@ private CoreEnvironment environment() {
return core.context().environment();
}
+ private CoreResources coreResources() {
+ return core.context().coreResources();
+ }
+
private ServerSearchRequest searchRequest(String indexName, CoreSearchQuery query, CoreSearchOptions opts) {
notNullOrEmpty(indexName, "IndexName", () -> new ReducedSearchErrorContext(indexName, query));
Duration timeout = opts.commonOptions().timeout().orElse(environment().timeoutConfig().searchTimeout());
@@ -170,7 +175,7 @@ private ServerSearchRequest searchRequest(String indexName, CoreSearchQuery quer
RetryStrategy retryStrategy = opts.commonOptions().retryStrategy().orElse(environment().retryStrategy());
- RequestSpan span = environment()
+ RequestSpan span = coreResources()
.requestTracer()
.requestSpan(TracingIdentifiers.SPAN_REQUEST_SEARCH, opts.commonOptions().parentSpan().orElse(null));
ServerSearchRequest request = new ServerSearchRequest(timeout, core.context(), retryStrategy, core.context().authenticator(), indexName, bytes, span, scope);
@@ -352,7 +357,7 @@ private ServerSearchRequest searchRequestV2(String indexName, CoreSearchRequest
RetryStrategy retryStrategy = opts.commonOptions().retryStrategy().orElse(environment().retryStrategy());
- RequestSpan span = environment()
+ RequestSpan span = coreResources()
.requestTracer()
.requestSpan(TracingIdentifiers.SPAN_REQUEST_SEARCH, opts.commonOptions().parentSpan().orElse(null));
ServerSearchRequest request = new ServerSearchRequest(timeout, core.context(), retryStrategy, core.context().authenticator(), indexName, bytes, span, scope);
diff --git a/core-io/src/main/java/com/couchbase/client/core/api/search/util/SearchCapabilityCheck.java b/core-io/src/main/java/com/couchbase/client/core/api/search/util/SearchCapabilityCheck.java
index 259867c8f..77731202c 100644
--- a/core-io/src/main/java/com/couchbase/client/core/api/search/util/SearchCapabilityCheck.java
+++ b/core-io/src/main/java/com/couchbase/client/core/api/search/util/SearchCapabilityCheck.java
@@ -17,10 +17,8 @@
import com.couchbase.client.core.Core;
import com.couchbase.client.core.annotation.Stability;
-import com.couchbase.client.core.config.ClusterCapabilities;
import com.couchbase.client.core.error.FeatureNotAvailableException;
-import com.couchbase.client.core.service.ServiceType;
-import com.couchbase.client.core.util.ClusterCapabilitiesUtil;
+import com.couchbase.client.core.topology.ClusterCapability;
import java.time.Duration;
import java.util.concurrent.CompletableFuture;
@@ -31,25 +29,26 @@ private SearchCapabilityCheck() {
}
public static CompletableFuture scopedSearchIndexCapabilityCheck(Core core, Duration timeout) {
- return ClusterCapabilitiesUtil.waitForClusterCapabilities(core, timeout)
- .doOnNext(clusterCapabilities -> {
- if (!clusterCapabilities.get(ServiceType.SEARCH).contains(ClusterCapabilities.SCOPED_SEARCH_INDEX)) {
- throw new FeatureNotAvailableException("This method cannot be used with this cluster, as it does not support scoped search indexes. Please use a cluster fully upgraded to Couchbase Server 7.6 or above.");
- }
- })
- .then()
- .toFuture();
+ return requireCapability(core, timeout, ClusterCapability.SEARCH_SCOPED,
+ "This method cannot be used with this cluster, as it does not support scoped search indexes." +
+ " Please use a cluster fully upgraded to Couchbase Server 7.6 or above.");
}
public static CompletableFuture vectorSearchCapabilityCheck(Core core, Duration timeout) {
- return ClusterCapabilitiesUtil.waitForClusterCapabilities(core, timeout)
- .doOnNext(clusterCapabilities -> {
- if (!clusterCapabilities.get(ServiceType.SEARCH).contains(ClusterCapabilities.VECTOR_SEARCH)) {
- throw new FeatureNotAvailableException("This method cannot be used with this cluster, as it does not support vector search. Please use a cluster fully upgraded to Couchbase Server 7.6 or above.");
- }
- })
- .then()
- .toFuture();
+ return requireCapability(core, timeout, ClusterCapability.SEARCH_VECTOR,
+ "This method cannot be used with this cluster, as it does not support vector search." +
+ " Please use a cluster fully upgraded to Couchbase Server 7.6 or above.");
+ }
+
+ private static CompletableFuture requireCapability(Core core, Duration timeout, ClusterCapability capability, String message) {
+ return core.waitForClusterTopology(timeout)
+ .doOnNext(topology -> {
+ if (!topology.hasCapability(capability)) {
+ throw new FeatureNotAvailableException(message);
+ }
+ })
+ .then()
+ .toFuture();
}
}
diff --git a/core-io/src/main/java/com/couchbase/client/core/classic/kv/ClassicCoreKvBinaryOps.java b/core-io/src/main/java/com/couchbase/client/core/classic/kv/ClassicCoreKvBinaryOps.java
index 5003679cd..56e6ba31b 100644
--- a/core-io/src/main/java/com/couchbase/client/core/classic/kv/ClassicCoreKvBinaryOps.java
+++ b/core-io/src/main/java/com/couchbase/client/core/classic/kv/ClassicCoreKvBinaryOps.java
@@ -18,6 +18,7 @@
import com.couchbase.client.core.Core;
import com.couchbase.client.core.CoreContext;
import com.couchbase.client.core.CoreKeyspace;
+import com.couchbase.client.core.CoreResources;
import com.couchbase.client.core.annotation.Stability;
import com.couchbase.client.core.api.kv.CoreAsyncResponse;
import com.couchbase.client.core.api.kv.CoreCounterResult;
@@ -92,7 +93,7 @@ private AppendRequest appendRequestClassic(final String id, final byte[] content
CoreKvBinaryParamValidators.validateAppendPrependArgs(id, keyspace, options, content, cas, durability);
Duration timeout = timeout(options, durability);
RetryStrategy retryStrategy = options.retryStrategy().orElse(environment().retryStrategy());
- RequestSpan span = environment().requestTracer().requestSpan(TracingIdentifiers.SPAN_REQUEST_KV_APPEND,
+ RequestSpan span = coreResources().requestTracer().requestSpan(TracingIdentifiers.SPAN_REQUEST_KV_APPEND,
options.parentSpan().orElse(null));
AppendRequest request = new AppendRequest(timeout, context(), collectionIdentifier(), retryStrategy, id,
content, cas, durability.levelIfSynchronous(), span);
@@ -126,7 +127,7 @@ private PrependRequest prependRequestClassic(final String id, final byte[] conte
CoreKvBinaryParamValidators.validateAppendPrependArgs(id, keyspace, options, content, cas, durability);
Duration timeout = timeout(options, durability);
RetryStrategy retryStrategy = options.retryStrategy().orElse(environment().retryStrategy());
- RequestSpan span = environment().requestTracer().requestSpan(TracingIdentifiers.SPAN_REQUEST_KV_PREPEND,
+ RequestSpan span = coreResources().requestTracer().requestSpan(TracingIdentifiers.SPAN_REQUEST_KV_PREPEND,
options.parentSpan().orElse(null));
PrependRequest request = new PrependRequest(timeout, context(), collectionIdentifier(), retryStrategy, id,
content, cas, durability.levelIfSynchronous(), span);
@@ -159,7 +160,7 @@ private IncrementRequest incrementRequestClassic(final String id, final CoreComm
durability);
Duration timeout = timeout(options, durability);
RetryStrategy retryStrategy = options.retryStrategy().orElse(environment().retryStrategy());
- RequestSpan span = environment().requestTracer().requestSpan(TracingIdentifiers.SPAN_REQUEST_KV_INCREMENT,
+ RequestSpan span = coreResources().requestTracer().requestSpan(TracingIdentifiers.SPAN_REQUEST_KV_INCREMENT,
options.parentSpan().orElse(null));
IncrementRequest request = new IncrementRequest(timeout, context(), collectionIdentifier(), retryStrategy, id,
@@ -193,7 +194,7 @@ private DecrementRequest decrementRequestClassic(final String id, final CoreComm
notNullOrEmpty(id, "Id", () -> ReducedKeyValueErrorContext.create(id, collectionIdentifier()));
Duration timeout = timeout(opts, durability);
RetryStrategy retryStrategy = opts.retryStrategy().orElse(environment().retryStrategy());
- RequestSpan span = environment().requestTracer().requestSpan(TracingIdentifiers.SPAN_REQUEST_KV_DECREMENT,
+ RequestSpan span = coreResources().requestTracer().requestSpan(TracingIdentifiers.SPAN_REQUEST_KV_DECREMENT,
opts.parentSpan().orElse(null));
DecrementRequest request = new DecrementRequest(timeout, context(), collectionIdentifier(), retryStrategy, id,
@@ -210,6 +211,10 @@ private CoreEnvironment environment() {
return core.context().environment();
}
+ private CoreResources coreResources() {
+ return core.context().coreResources();
+ }
+
private CollectionIdentifier collectionIdentifier() {
return keyspace.toCollectionIdentifier();
}
diff --git a/core-io/src/main/java/com/couchbase/client/core/classic/kv/ClassicCoreKvOps.java b/core-io/src/main/java/com/couchbase/client/core/classic/kv/ClassicCoreKvOps.java
index 0b7d4a9ee..84d94a6bd 100644
--- a/core-io/src/main/java/com/couchbase/client/core/classic/kv/ClassicCoreKvOps.java
+++ b/core-io/src/main/java/com/couchbase/client/core/classic/kv/ClassicCoreKvOps.java
@@ -27,7 +27,6 @@
import com.couchbase.client.core.api.kv.CoreExpiry;
import com.couchbase.client.core.api.kv.CoreGetResult;
import com.couchbase.client.core.api.kv.CoreKvOps;
-import com.couchbase.client.core.api.kv.CoreKvParamValidators;
import com.couchbase.client.core.api.kv.CoreKvResponseMetadata;
import com.couchbase.client.core.api.kv.CoreLookupInMacro;
import com.couchbase.client.core.api.kv.CoreMutationResult;
@@ -36,6 +35,7 @@
import com.couchbase.client.core.api.kv.CoreSubdocGetResult;
import com.couchbase.client.core.api.kv.CoreSubdocMutateCommand;
import com.couchbase.client.core.api.kv.CoreSubdocMutateResult;
+import com.couchbase.client.core.api.kv.CoreReadPreference;
import com.couchbase.client.core.classic.ClassicHelper;
import com.couchbase.client.core.cnc.CbTracing;
import com.couchbase.client.core.cnc.RequestSpan;
@@ -49,7 +49,6 @@
import com.couchbase.client.core.error.DocumentNotFoundException;
import com.couchbase.client.core.error.DocumentUnretrievableException;
import com.couchbase.client.core.error.InvalidArgumentException;
-import com.couchbase.client.core.error.context.ErrorContext;
import com.couchbase.client.core.error.context.KeyValueErrorContext;
import com.couchbase.client.core.error.context.ReducedKeyValueErrorContext;
import com.couchbase.client.core.io.CollectionIdentifier;
@@ -146,7 +145,7 @@ public ClassicCoreKvOps(Core core, CoreKeyspace keyspace) {
this.defaultKvTimeout = ctx.environment().timeoutConfig().kvTimeout();
this.defaultKvDurableTimeout = ctx.environment().timeoutConfig().kvDurableTimeout();
this.defaultRetryStrategy = ctx.environment().retryStrategy();
- this.requestTracer = ctx.environment().requestTracer();
+ this.requestTracer = ctx.coreResources().requestTracer();
this.keyspace = requireNonNull(keyspace);
this.collectionIdentifier = keyspace.toCollectionIdentifier();
this.rangeScanOrchestrator = new RangeScanOrchestrator(core, collectionIdentifier);
@@ -711,19 +710,12 @@ public CoreAsyncResponse subdocGetAsync(
// This should be superfluous now - if the op failed then error() should be set - but leaving as a fail-safe.
commonKvResponseCheck(req, res);
},
- it -> new CoreSubdocGetResult(
- keyspace,
- key,
- CoreKvResponseMetadata.from(it.flexibleExtras()),
- Arrays.asList(it.values()),
- it.cas(),
- it.isDeleted()
- )
+ it -> it.toCore(keyspace, key)
);
}
@Override
- public Flux getAllReplicasReactive(CoreCommonOptions common, String key) {
+ public Flux getAllReplicasReactive(CoreCommonOptions common, String key, CoreReadPreference readPreference) {
validateGetAllReplicasParams(common, key);
Duration timeout = timeout(common);
@@ -736,7 +728,8 @@ public Flux getAllReplicasReactive(CoreCommonOptions common, Stri
timeout,
retryStrategy,
common.clientContext(),
- common.parentSpan().orElse(null)
+ common.parentSpan().orElse(null),
+ readPreference
).map(it -> new CoreGetResult(
CoreKvResponseMetadata.from(it.getResponse().flexibleExtras()),
keyspace,
@@ -750,17 +743,17 @@ public Flux getAllReplicasReactive(CoreCommonOptions common, Stri
}
@Override
- public Mono getAnyReplicaReactive(CoreCommonOptions common, String key) {
+ public Mono getAnyReplicaReactive(CoreCommonOptions common, String key, CoreReadPreference readPreference) {
validateGetAnyReplicaParams(common, key);
RequestSpan getAnySpan = span(common, TracingIdentifiers.SPAN_GET_ANY_REPLICA);
- return getAllReplicasReactive(common.withParentSpan(getAnySpan), key)
+ return getAllReplicasReactive(common.withParentSpan(getAnySpan), key, readPreference)
.next()
.doFinally(signalType -> getAnySpan.end());
}
@Override
- public Flux subdocGetAllReplicasReactive(CoreCommonOptions common, String key, List commands) {
+ public Flux subdocGetAllReplicasReactive(CoreCommonOptions common, String key, List commands, CoreReadPreference readPreference) {
validateSubdocGetAllParams(common, key, commands);
Duration timeout = timeout(common);
@@ -774,15 +767,16 @@ public Flux subdocGetAllReplicasReactive(CoreCommonOptions
timeout,
retryStrategy,
common.clientContext(),
- common.parentSpan().orElse(null)
+ common.parentSpan().orElse(null),
+ readPreference
);
}
@Override
- public Mono subdocGetAnyReplicaReactive(CoreCommonOptions common, String key, List commands) {
+ public Mono subdocGetAnyReplicaReactive(CoreCommonOptions common, String key, List commands, CoreReadPreference readPreference) {
validateSubdocGetAnyParams(common, key, commands);
RequestSpan getAnySpan = span(common, TracingIdentifiers.SPAN_GET_ANY_REPLICA);
- return subdocGetAllReplicasReactive(common.withParentSpan(getAnySpan), key, commands)
+ return subdocGetAllReplicasReactive(common.withParentSpan(getAnySpan), key, commands, readPreference)
.next()
.switchIfEmpty(Mono.error(new DocumentUnretrievableException(ReducedKeyValueErrorContext.create(key, collectionIdentifier))))
.doFinally(signalType -> getAnySpan.end());
diff --git a/core-io/src/main/java/com/couchbase/client/core/classic/query/ClassicCoreQueryOps.java b/core-io/src/main/java/com/couchbase/client/core/classic/query/ClassicCoreQueryOps.java
index 97d24db34..406bc05af 100644
--- a/core-io/src/main/java/com/couchbase/client/core/classic/query/ClassicCoreQueryOps.java
+++ b/core-io/src/main/java/com/couchbase/client/core/classic/query/ClassicCoreQueryOps.java
@@ -43,10 +43,10 @@
import com.couchbase.client.core.msg.kv.MutationToken;
import com.couchbase.client.core.msg.query.QueryRequest;
import com.couchbase.client.core.msg.query.QueryResponse;
-import com.couchbase.client.core.node.NodeIdentifier;
import com.couchbase.client.core.retry.RetryReason;
import com.couchbase.client.core.retry.RetryStrategy;
import com.couchbase.client.core.service.ServiceType;
+import com.couchbase.client.core.topology.NodeIdentifier;
import com.couchbase.client.core.transaction.CoreTransactionsReactive;
import com.couchbase.client.core.transaction.config.CoreSingleQueryTransactionOptions;
import com.couchbase.client.core.transaction.config.CoreTransactionsConfig;
@@ -197,7 +197,7 @@ private QueryRequest queryRequest(String statement,
}
byte[] queryBytes = query.toString().getBytes(StandardCharsets.UTF_8);
- RequestSpan span = core.context().environment()
+ RequestSpan span = core.context().coreResources()
.requestTracer()
.requestSpan(TracingIdentifiers.SPAN_REQUEST_QUERY, options.commonOptions().parentSpan().orElse(null));
@@ -219,7 +219,7 @@ private static Mono singleQueryTransactionBuffered(Core core,
}
CoreTransactionsReactive tri = configureTransactions(core, opts);
- SpanWrapper span = SpanWrapperUtil.createOp(null, core.context().environment().requestTracer(), null,
+ SpanWrapper span = SpanWrapperUtil.createOp(null, core.context().coreResources().requestTracer(), null,
null, TracingIdentifiers.SPAN_REQUEST_QUERY, opts.commonOptions().parentSpan().map(SpanWrapper::new).orElse(null))
.attribute(TracingIdentifiers.ATTR_STATEMENT, statement)
.attribute(TracingIdentifiers.ATTR_TRANSACTION_SINGLE_QUERY, true);
@@ -253,7 +253,7 @@ private Mono singleQueryTransactionReactive(String stat
}
CoreTransactionsReactive tri = configureTransactions(core, opts);
- SpanWrapper span = SpanWrapperUtil.createOp(null, core.context().environment().requestTracer(), null,
+ SpanWrapper span = SpanWrapperUtil.createOp(null, core.context().coreResources().requestTracer(), null,
null, TracingIdentifiers.SPAN_REQUEST_QUERY, opts.commonOptions().parentSpan().map(SpanWrapper::new).orElse(null))
.attribute(TracingIdentifiers.ATTR_STATEMENT, statement)
.attribute(TracingIdentifiers.ATTR_TRANSACTION_SINGLE_QUERY, true);
diff --git a/core-io/src/main/java/com/couchbase/client/core/classic/query/ClassicCoreQueryResult.java b/core-io/src/main/java/com/couchbase/client/core/classic/query/ClassicCoreQueryResult.java
index 2bee6f096..57c040b84 100644
--- a/core-io/src/main/java/com/couchbase/client/core/classic/query/ClassicCoreQueryResult.java
+++ b/core-io/src/main/java/com/couchbase/client/core/classic/query/ClassicCoreQueryResult.java
@@ -22,7 +22,7 @@
import com.couchbase.client.core.msg.query.QueryChunkHeader;
import com.couchbase.client.core.msg.query.QueryChunkRow;
import com.couchbase.client.core.msg.query.QueryChunkTrailer;
-import com.couchbase.client.core.node.NodeIdentifier;
+import com.couchbase.client.core.topology.NodeIdentifier;
import java.util.ArrayList;
import java.util.List;
diff --git a/core-io/src/main/java/com/couchbase/client/core/classic/query/ClassicCoreReactiveQueryResult.java b/core-io/src/main/java/com/couchbase/client/core/classic/query/ClassicCoreReactiveQueryResult.java
index 364f62777..5bd93a607 100644
--- a/core-io/src/main/java/com/couchbase/client/core/classic/query/ClassicCoreReactiveQueryResult.java
+++ b/core-io/src/main/java/com/couchbase/client/core/classic/query/ClassicCoreReactiveQueryResult.java
@@ -21,7 +21,7 @@
import com.couchbase.client.core.api.query.CoreReactiveQueryResult;
import com.couchbase.client.core.msg.query.QueryChunkRow;
import com.couchbase.client.core.msg.query.QueryResponse;
-import com.couchbase.client.core.node.NodeIdentifier;
+import com.couchbase.client.core.topology.NodeIdentifier;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.annotation.Nullable;
diff --git a/core-io/src/main/java/com/couchbase/client/core/classic/query/PreparedStatementStrategy.java b/core-io/src/main/java/com/couchbase/client/core/classic/query/PreparedStatementStrategy.java
index d30c9fd9a..92a5511a4 100644
--- a/core-io/src/main/java/com/couchbase/client/core/classic/query/PreparedStatementStrategy.java
+++ b/core-io/src/main/java/com/couchbase/client/core/classic/query/PreparedStatementStrategy.java
@@ -42,7 +42,7 @@ public PreparedStatementStrategy(Core core, int cacheSize) {
}
protected RequestTracer requestTracer() {
- return core.context().environment().requestTracer();
+ return core.context().coreResources().requestTracer();
}
public abstract Mono execute(QueryRequest request);
diff --git a/core-io/src/main/java/com/couchbase/client/core/cnc/CbTracing.java b/core-io/src/main/java/com/couchbase/client/core/cnc/CbTracing.java
index 3bf9e14f3..9860aacfb 100644
--- a/core-io/src/main/java/com/couchbase/client/core/cnc/CbTracing.java
+++ b/core-io/src/main/java/com/couchbase/client/core/cnc/CbTracing.java
@@ -65,7 +65,7 @@ public static boolean isInternalSpan(final RequestSpan span) {
*/
@UsedBy(SPRING_DATA_COUCHBASE)
public static RequestSpan newSpan(CoreContext coreContext, String spanName, RequestSpan parent) {
- return coreContext.environment().requestTracer().requestSpan(spanName, parent);
+ return coreContext.coreResources().requestTracer().requestSpan(spanName, parent);
}
/**
diff --git a/core-io/src/main/java/com/couchbase/client/core/cnc/DefaultEventBus.java b/core-io/src/main/java/com/couchbase/client/core/cnc/DefaultEventBus.java
index 75179814e..95ffbcdab 100644
--- a/core-io/src/main/java/com/couchbase/client/core/cnc/DefaultEventBus.java
+++ b/core-io/src/main/java/com/couchbase/client/core/cnc/DefaultEventBus.java
@@ -16,10 +16,10 @@
package com.couchbase.client.core.cnc;
-import com.couchbase.client.core.deps.org.jctools.queues.MpscArrayQueue;
import com.couchbase.client.core.json.Mapper;
import com.couchbase.client.core.util.CbCollections;
import com.couchbase.client.core.util.NanoTimestamp;
+import com.couchbase.client.core.util.NativeImageHelper;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
@@ -137,7 +137,7 @@ public static DefaultEventBus create(final Scheduler scheduler) {
}
private DefaultEventBus(final Builder builder) {
- eventQueue = new MpscArrayQueue<>(builder.queueCapacity);
+ eventQueue = NativeImageHelper.createMpscArrayQueue(builder.queueCapacity);
scheduler = builder.scheduler;
errorLogging = builder.errorLogging.orElse(null);
threadName = builder.threadName;
diff --git a/core-io/src/main/java/com/couchbase/client/core/cnc/OrphanReporter.java b/core-io/src/main/java/com/couchbase/client/core/cnc/OrphanReporter.java
index e1f58439f..f83f3fd47 100644
--- a/core-io/src/main/java/com/couchbase/client/core/cnc/OrphanReporter.java
+++ b/core-io/src/main/java/com/couchbase/client/core/cnc/OrphanReporter.java
@@ -20,7 +20,6 @@
import com.couchbase.client.core.cnc.events.tracing.OrphanRecordDroppedEvent;
import com.couchbase.client.core.cnc.events.tracing.OrphanReporterFailureDetectedEvent;
import com.couchbase.client.core.cnc.events.tracing.OrphansRecordedEvent;
-import com.couchbase.client.core.deps.org.jctools.queues.MpscArrayQueue;
import com.couchbase.client.core.env.OrphanReporterConfig;
import com.couchbase.client.core.msg.Request;
import com.couchbase.client.core.msg.UnmonitoredRequest;
@@ -29,6 +28,7 @@
import com.couchbase.client.core.service.ServiceType;
import com.couchbase.client.core.util.HostAndPort;
import com.couchbase.client.core.util.NanoTimestamp;
+import com.couchbase.client.core.util.NativeImageHelper;
import reactor.core.publisher.Mono;
import java.time.Duration;
@@ -86,7 +86,7 @@ public class OrphanReporter {
@Stability.Internal
public OrphanReporter(final EventBus eventBus, final OrphanReporterConfig config) {
this.eventBus = eventBus;
- this.orphanQueue = new MpscArrayQueue<>(config.queueLength());
+ this.orphanQueue = NativeImageHelper.createMpscArrayQueue(config.queueLength());
this.emitInterval = config.emitInterval();
this.sampleSize = config.sampleSize();
this.enabled = config.enabled();
diff --git a/core-io/src/main/java/com/couchbase/client/core/cnc/TracingIdentifiers.java b/core-io/src/main/java/com/couchbase/client/core/cnc/TracingIdentifiers.java
index 5200bf637..af42c3c78 100644
--- a/core-io/src/main/java/com/couchbase/client/core/cnc/TracingIdentifiers.java
+++ b/core-io/src/main/java/com/couchbase/client/core/cnc/TracingIdentifiers.java
@@ -264,6 +264,9 @@ private TracingIdentifiers() {}
public static final String ATTR_SCOPE = "db.couchbase.scope";
public static final String ATTR_DOCUMENT_ID = "db.couchbase.document_id";
+ public static final String ATTR_CLUSTER_UUID = "db.couchbase.cluster_uuid";
+ public static final String ATTR_CLUSTER_NAME = "db.couchbase.cluster_name";
+
public static final String ATTR_TRANSACTION_ID = "db.couchbase.transaction.id";
public static final String ATTR_TRANSACTION_ATTEMPT_ID = "db.couchbase.transaction.attempt_id";
public static final String ATTR_TRANSACTION_STATE = "db.couchbase.transaction.state";
@@ -291,6 +294,7 @@ private TracingIdentifiers() {}
public static final String TRANSACTION_OP_INSERT = "transaction_insert";
public static final String TRANSACTION_OP_REMOVE = "transaction_remove";
public static final String TRANSACTION_OP_GET = "transaction_get";
+ public static final String TRANSACTION_OP_GET_REPLICA_FROM_PREFERRED_SERVER_GROUP = "transaction_get_replica_from_preferred_server_group";
public static final String TRANSACTION_OP_QUERY = "transaction_query";
public static final String TRANSACTION_OP_INSERT_STAGE = "transaction_insert_stage";
public static final String TRANSACTION_OP_REPLACE_STAGE = "transaction_replace_stage";
diff --git a/core-io/src/main/java/com/couchbase/client/core/cnc/metrics/LoggingMeter.java b/core-io/src/main/java/com/couchbase/client/core/cnc/metrics/LoggingMeter.java
index a0dae3796..0faeae5c1 100644
--- a/core-io/src/main/java/com/couchbase/client/core/cnc/metrics/LoggingMeter.java
+++ b/core-io/src/main/java/com/couchbase/client/core/cnc/metrics/LoggingMeter.java
@@ -32,6 +32,7 @@
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
@@ -47,7 +48,7 @@ public class LoggingMeter implements Meter {
private final Thread worker;
private final AtomicBoolean running = new AtomicBoolean(false);
- private final Map valueRecorders = new ConcurrentHashMap<>();
+ private final ConcurrentMap valueRecorders = new ConcurrentHashMap<>();
private final long emitIntervalMs;
private final LoggingMeterConfig config;
@@ -86,7 +87,7 @@ public Counter counter(String name, Map tags) {
}
@Override
- public synchronized ValueRecorder valueRecorder(String name, Map tags) {
+ public ValueRecorder valueRecorder(String name, Map tags) {
try {
return valueRecorders.computeIfAbsent(
new NameAndTags(name, tags),
@@ -141,7 +142,7 @@ public void run() {
}
@SuppressWarnings("unchecked")
- private synchronized void dumpMetrics() {
+ private void dumpMetrics() {
Map output = new HashMap<>();
Map meta = new HashMap<>();
diff --git a/core-io/src/main/java/com/couchbase/client/core/cnc/metrics/NoopCounter.java b/core-io/src/main/java/com/couchbase/client/core/cnc/metrics/NoopCounter.java
index 53d124400..171ed0904 100644
--- a/core-io/src/main/java/com/couchbase/client/core/cnc/metrics/NoopCounter.java
+++ b/core-io/src/main/java/com/couchbase/client/core/cnc/metrics/NoopCounter.java
@@ -20,6 +20,8 @@
public class NoopCounter implements Counter {
+ public static final NoopCounter INSTANCE = new NoopCounter();
+
@Override
public void incrementBy(long number) {
diff --git a/core-io/src/main/java/com/couchbase/client/core/cnc/metrics/NoopMeter.java b/core-io/src/main/java/com/couchbase/client/core/cnc/metrics/NoopMeter.java
index e9b3de268..ce3b35271 100644
--- a/core-io/src/main/java/com/couchbase/client/core/cnc/metrics/NoopMeter.java
+++ b/core-io/src/main/java/com/couchbase/client/core/cnc/metrics/NoopMeter.java
@@ -30,12 +30,12 @@ private NoopMeter() {}
@Override
public Counter counter(String name, Map tags) {
- return new NoopCounter();
+ return NoopCounter.INSTANCE;
}
@Override
public ValueRecorder valueRecorder(String name, Map tags) {
- return new NoopValueRecorder();
+ return NoopValueRecorder.INSTANCE;
}
@Override
diff --git a/core-io/src/main/java/com/couchbase/client/core/cnc/metrics/NoopValueRecorder.java b/core-io/src/main/java/com/couchbase/client/core/cnc/metrics/NoopValueRecorder.java
index 4cc068a03..5bc30f433 100644
--- a/core-io/src/main/java/com/couchbase/client/core/cnc/metrics/NoopValueRecorder.java
+++ b/core-io/src/main/java/com/couchbase/client/core/cnc/metrics/NoopValueRecorder.java
@@ -20,6 +20,8 @@
public class NoopValueRecorder implements ValueRecorder {
+ public static final NoopValueRecorder INSTANCE = new NoopValueRecorder();
+
@Override
public void recordValue(long value) {
diff --git a/core-io/src/main/java/com/couchbase/client/core/cnc/tracing/ThresholdLoggingTracer.java b/core-io/src/main/java/com/couchbase/client/core/cnc/tracing/ThresholdLoggingTracer.java
index 825c519af..d2e433daa 100644
--- a/core-io/src/main/java/com/couchbase/client/core/cnc/tracing/ThresholdLoggingTracer.java
+++ b/core-io/src/main/java/com/couchbase/client/core/cnc/tracing/ThresholdLoggingTracer.java
@@ -21,7 +21,6 @@
import com.couchbase.client.core.cnc.RequestTracer;
import com.couchbase.client.core.cnc.TracingIdentifiers;
import com.couchbase.client.core.cnc.events.tracing.OverThresholdRequestsRecordedEvent;
-import com.couchbase.client.core.deps.org.jctools.queues.MpscArrayQueue;
import com.couchbase.client.core.env.ThresholdLoggingTracerConfig;
import com.couchbase.client.core.error.TracerException;
import com.couchbase.client.core.msg.Request;
@@ -29,6 +28,7 @@
import com.couchbase.client.core.transaction.components.CoreTransactionRequest;
import com.couchbase.client.core.util.HostAndPort;
import com.couchbase.client.core.util.NanoTimestamp;
+import com.couchbase.client.core.util.NativeImageHelper;
import reactor.core.publisher.Mono;
import java.time.Duration;
@@ -121,7 +121,7 @@ public static ThresholdLoggingTracer create(final EventBus eventBus, ThresholdLo
*/
private ThresholdLoggingTracer(final EventBus eventBus, ThresholdLoggingTracerConfig config) {
this.eventBus = eventBus;
- this.overThresholdQueue = new MpscArrayQueue<>(config.queueLength());
+ this.overThresholdQueue = NativeImageHelper.createMpscArrayQueue(config.queueLength());
kvThreshold = config.kvThreshold().toNanos();
analyticsThreshold = config.analyticsThreshold().toNanos();
searchThreshold = config.searchThreshold().toNanos();
diff --git a/core-io/src/main/java/com/couchbase/client/core/config/AbstractBucketConfig.java b/core-io/src/main/java/com/couchbase/client/core/config/AbstractBucketConfig.java
index 85096f1ec..55e7c34ed 100644
--- a/core-io/src/main/java/com/couchbase/client/core/config/AbstractBucketConfig.java
+++ b/core-io/src/main/java/com/couchbase/client/core/config/AbstractBucketConfig.java
@@ -16,7 +16,10 @@
package com.couchbase.client.core.config;
+import com.couchbase.client.core.annotation.Stability;
import com.couchbase.client.core.service.ServiceType;
+import com.couchbase.client.core.topology.ClusterTopologyWithBucket;
+import reactor.util.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collections;
@@ -29,6 +32,7 @@
import java.util.stream.Collectors;
import static com.couchbase.client.core.util.CbCollections.isNullOrEmpty;
+import static java.util.Objects.requireNonNull;
@Deprecated
public abstract class AbstractBucketConfig implements BucketConfig {
@@ -45,6 +49,9 @@ public abstract class AbstractBucketConfig implements BucketConfig {
private final List portInfos;
private final ConfigVersion version;
+ // Null only if this bucket config was created by a legacy config parser
+ @Nullable private final ClusterTopologyWithBucket clusterTopology;
+
/**
* A "dumb" constructor that assigns the given values
* directly to the corresponding fields, without any funny business.
@@ -60,7 +67,8 @@ protected AbstractBucketConfig(
Map> clusterCapabilities,
String origin,
List portInfos,
- ConfigVersion version
+ ConfigVersion version,
+ ClusterTopologyWithBucket clusterTopology
) {
this.uuid = uuid;
this.name = name;
@@ -73,6 +81,7 @@ protected AbstractBucketConfig(
this.origin = origin;
this.portInfos = portInfos;
this.version = version;
+ this.clusterTopology = requireNonNull(clusterTopology);
}
protected AbstractBucketConfig(String uuid, String name, BucketNodeLocator locator, String uri, String streamingUri,
@@ -92,6 +101,15 @@ protected AbstractBucketConfig(String uuid, String name, BucketNodeLocator locat
this.version = new ConfigVersion(revEpoch, rev);
this.portInfos = portInfos == null ? Collections.emptyList() : portInfos;
this.nodeInfo = portInfos == null ? nodeInfos : nodeInfoFromExtended(portInfos, nodeInfos);
+ this.clusterTopology = null;
+ }
+
+ @Stability.Internal
+ public ClusterTopologyWithBucket asClusterTopology() {
+ if (clusterTopology == null) {
+ throw new IllegalStateException("This BucketConfig instance was not created from a ClusterTopologyWithBucket.");
+ }
+ return clusterTopology;
}
static Set convertBucketCapabilities(final List input) {
diff --git a/core-io/src/main/java/com/couchbase/client/core/config/BucketConfig.java b/core-io/src/main/java/com/couchbase/client/core/config/BucketConfig.java
index 89b1210b9..a3e1e7d9b 100644
--- a/core-io/src/main/java/com/couchbase/client/core/config/BucketConfig.java
+++ b/core-io/src/main/java/com/couchbase/client/core/config/BucketConfig.java
@@ -16,9 +16,11 @@
package com.couchbase.client.core.config;
+import com.couchbase.client.core.annotation.Stability;
import com.couchbase.client.core.service.ServiceType;
import com.couchbase.client.core.deps.com.fasterxml.jackson.annotation.JsonSubTypes;
import com.couchbase.client.core.deps.com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.couchbase.client.core.topology.ClusterTopologyWithBucket;
import java.util.List;
import java.util.Map;
@@ -163,4 +165,9 @@ public interface BucketConfig {
*/
List portInfos();
+ /**
+ * @throws IllegalStateException if this BucketConfig was not created from a ClusterTopologyWithBucket.
+ */
+ @Stability.Internal
+ ClusterTopologyWithBucket asClusterTopology();
}
diff --git a/core-io/src/main/java/com/couchbase/client/core/config/ClusterCapabilities.java b/core-io/src/main/java/com/couchbase/client/core/config/ClusterCapabilities.java
index d183d93bd..258cde681 100644
--- a/core-io/src/main/java/com/couchbase/client/core/config/ClusterCapabilities.java
+++ b/core-io/src/main/java/com/couchbase/client/core/config/ClusterCapabilities.java
@@ -22,7 +22,7 @@
* Contains all the cluster capabilities this SDK supports (depending on the server version, the cluster may
* export more than these).
*
- * @deprecated In favor of {@link com.couchbase.client.core.topology.ClusterCapability
+ * @deprecated In favor of {@link com.couchbase.client.core.topology.ClusterCapability}
*/
@Deprecated
public enum ClusterCapabilities {
diff --git a/core-io/src/main/java/com/couchbase/client/core/config/ClusterConfig.java b/core-io/src/main/java/com/couchbase/client/core/config/ClusterConfig.java
index abf1d140d..71fb38e88 100644
--- a/core-io/src/main/java/com/couchbase/client/core/config/ClusterConfig.java
+++ b/core-io/src/main/java/com/couchbase/client/core/config/ClusterConfig.java
@@ -18,7 +18,11 @@
import com.couchbase.client.core.annotation.Stability;
import com.couchbase.client.core.service.ServiceType;
+import com.couchbase.client.core.topology.ClusterTopology;
+import com.couchbase.client.core.topology.ClusterTopologyWithBucket;
+import reactor.util.annotation.Nullable;
+import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
@@ -26,6 +30,8 @@
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
+import static com.couchbase.client.core.util.CbCollections.transform;
+
/**
* The {@link ClusterConfig} holds bucket and global configurations in a central place.
*/
@@ -49,6 +55,28 @@ public ClusterConfig() {
globalConfig = new AtomicReference<>();
}
+ @Stability.Internal
+ public Set bucketNames() {
+ return bucketConfigs.keySet();
+ }
+
+ @Stability.Internal
+ public @Nullable ClusterTopologyWithBucket bucketTopology(final String bucketName) {
+ BucketConfig bucketConfig = bucketConfigs.get(bucketName);
+ return bucketConfig == null ? null : bucketConfig.asClusterTopology();
+ }
+
+ @Stability.Internal
+ public Collection bucketTopologies() {
+ return transform(bucketConfigs.values(), BucketConfig::asClusterTopology);
+ }
+
+ @Stability.Internal
+ public @Nullable ClusterTopology globalTopology() {
+ GlobalConfig g = globalConfig();
+ return g == null ? null : g.asClusterTopology();
+ }
+
public BucketConfig bucketConfig(final String bucketName) {
return bucketConfigs.get(bucketName);
}
diff --git a/core-io/src/main/java/com/couchbase/client/core/config/ConfigVersion.java b/core-io/src/main/java/com/couchbase/client/core/config/ConfigVersion.java
index 7d648313b..beb70bde7 100644
--- a/core-io/src/main/java/com/couchbase/client/core/config/ConfigVersion.java
+++ b/core-io/src/main/java/com/couchbase/client/core/config/ConfigVersion.java
@@ -23,7 +23,7 @@
import java.util.Objects;
/**
- * @deprecated In favor of {@link com.couchbase.client.core.topology.TopologyRevision
+ * @deprecated In favor of {@link com.couchbase.client.core.topology.TopologyRevision}
*/
@Deprecated
@Stability.Internal
diff --git a/core-io/src/main/java/com/couchbase/client/core/config/CouchbaseBucketConfig.java b/core-io/src/main/java/com/couchbase/client/core/config/CouchbaseBucketConfig.java
index 529873d81..8366ce9d5 100644
--- a/core-io/src/main/java/com/couchbase/client/core/config/CouchbaseBucketConfig.java
+++ b/core-io/src/main/java/com/couchbase/client/core/config/CouchbaseBucketConfig.java
@@ -131,7 +131,8 @@ public CouchbaseBucketConfig(ClusterTopologyWithBucket cluster) {
getClusterCapabilities(cluster),
"",
getPortInfos(cluster),
- LegacyConfigHelper.toLegacy(cluster.revision())
+ LegacyConfigHelper.toLegacy(cluster.revision()),
+ cluster
);
CouchbaseBucketTopology bucket = (CouchbaseBucketTopology) cluster.bucket();
@@ -164,7 +165,7 @@ private static List toLegacyPartitions(
return transform(map.values(), it -> {
short[] replicas = new short[numberOfReplicas];
for (int i = 0; i < numberOfReplicas; i++) {
- replicas[i] = (short) it.nodeIndexForReplica(0).orElse(PARTITION_NOT_EXISTENT);
+ replicas[i] = (short) it.nodeIndexForReplica(i).orElse(PARTITION_NOT_EXISTENT);
}
return new Partition(
(short) it.nodeIndexForActive().orElse(PARTITION_NOT_EXISTENT),
diff --git a/core-io/src/main/java/com/couchbase/client/core/config/DefaultConfigurationProvider.java b/core-io/src/main/java/com/couchbase/client/core/config/DefaultConfigurationProvider.java
index ba71cfb8d..a772a80f1 100644
--- a/core-io/src/main/java/com/couchbase/client/core/config/DefaultConfigurationProvider.java
+++ b/core-io/src/main/java/com/couchbase/client/core/config/DefaultConfigurationProvider.java
@@ -58,7 +58,7 @@
import com.couchbase.client.core.msg.CancellationReason;
import com.couchbase.client.core.msg.ResponseStatus;
import com.couchbase.client.core.msg.kv.GetCollectionIdRequest;
-import com.couchbase.client.core.node.NodeIdentifier;
+import com.couchbase.client.core.topology.NodeIdentifier;
import com.couchbase.client.core.retry.BestEffortRetryStrategy;
import com.couchbase.client.core.service.ServiceType;
import com.couchbase.client.core.topology.ClusterTopology;
diff --git a/core-io/src/main/java/com/couchbase/client/core/config/GlobalConfig.java b/core-io/src/main/java/com/couchbase/client/core/config/GlobalConfig.java
index d12339942..a55a79ddc 100644
--- a/core-io/src/main/java/com/couchbase/client/core/config/GlobalConfig.java
+++ b/core-io/src/main/java/com/couchbase/client/core/config/GlobalConfig.java
@@ -16,12 +16,15 @@
package com.couchbase.client.core.config;
+import com.couchbase.client.core.annotation.Stability;
import com.couchbase.client.core.deps.com.fasterxml.jackson.annotation.JacksonInject;
import com.couchbase.client.core.deps.com.fasterxml.jackson.annotation.JsonCreator;
import com.couchbase.client.core.deps.com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.couchbase.client.core.deps.com.fasterxml.jackson.annotation.JsonProperty;
import com.couchbase.client.core.service.ServiceType;
+import com.couchbase.client.core.topology.ClusterIdentifier;
import com.couchbase.client.core.topology.ClusterTopology;
+import reactor.util.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
@@ -45,11 +48,17 @@ public class GlobalConfig {
private final ConfigVersion version;
private final List portInfos;
private final Map> clusterCapabilities;
+ private final @Nullable ClusterIdentifier clusterIdent;
+
+ // Null only if the GlobalConfig was created by a legacy config parser
+ @Nullable private final ClusterTopology clusterTopology;
public GlobalConfig(ClusterTopology topology) {
this.version = LegacyConfigHelper.toLegacy(topology.revision());
this.portInfos = LegacyConfigHelper.getPortInfos(topology);
this.clusterCapabilities = LegacyConfigHelper.getClusterCapabilities(topology);
+ this.clusterTopology = topology;
+ this.clusterIdent = topology.id();
}
@JsonCreator
@@ -63,9 +72,10 @@ public GlobalConfig(
this.version = new ConfigVersion(revEpoch, rev);
this.portInfos = enrichPortInfos(portInfos, origin);
this.clusterCapabilities = AbstractBucketConfig.convertClusterCapabilities(clusterCapabilities);
+ this.clusterTopology = null;
+ this.clusterIdent = null;
}
-
/**
* Helper method to enrich the port infos with a synthetic origin host if not present.
*
@@ -81,7 +91,7 @@ private List enrichPortInfos(final List portInfos, final Str
List enriched = new ArrayList<>(portInfos.size());
for (PortInfo portInfo : portInfos) {
if (portInfo.hostname() == null) {
- enriched.add(new PortInfo(portInfo.ports(), portInfo.sslPorts(), portInfo.alternateAddresses(), origin));
+ enriched.add(new PortInfo(portInfo.ports(), portInfo.sslPorts(), portInfo.alternateAddresses(), origin, portInfo.serverGroup()));
} else {
enriched.add(portInfo);
}
@@ -120,6 +130,10 @@ public Map> clusterCapabilities() {
return clusterCapabilities;
}
+ @Nullable public ClusterIdentifier clusterIdent() {
+ return clusterIdent;
+ }
+
/**
* The node/port infos for each node in the list.
*/
@@ -127,6 +141,17 @@ public List portInfos() {
return portInfos;
}
+ /**
+ * @throws IllegalStateException if this GlobalConfig was not created from a ClusterTopology.
+ */
+ @Stability.Internal
+ ClusterTopology asClusterTopology() {
+ if (clusterTopology == null) {
+ throw new IllegalStateException("This GlobalConfig instance was not created from a ClusterTopology.");
+ }
+ return clusterTopology;
+ }
+
@Override
public String toString() {
return "GlobalConfig{" +
diff --git a/core-io/src/main/java/com/couchbase/client/core/config/LegacyConfigHelper.java b/core-io/src/main/java/com/couchbase/client/core/config/LegacyConfigHelper.java
index 9755e9251..160c32f7d 100644
--- a/core-io/src/main/java/com/couchbase/client/core/config/LegacyConfigHelper.java
+++ b/core-io/src/main/java/com/couchbase/client/core/config/LegacyConfigHelper.java
@@ -53,7 +53,8 @@ static List getPortInfos(ClusterTopology topology) {
tlsPorts(topology, it),
emptyMap(), // The host is always accurate -- there is never an alternate.
it.host(),
- it.id().toLegacy()
+ it.id().toLegacy(),
+ it.serverGroup()
)
);
}
diff --git a/core-io/src/main/java/com/couchbase/client/core/config/MemcachedBucketConfig.java b/core-io/src/main/java/com/couchbase/client/core/config/MemcachedBucketConfig.java
index 904664b35..df43eafb0 100644
--- a/core-io/src/main/java/com/couchbase/client/core/config/MemcachedBucketConfig.java
+++ b/core-io/src/main/java/com/couchbase/client/core/config/MemcachedBucketConfig.java
@@ -91,7 +91,8 @@ public MemcachedBucketConfig(
LegacyConfigHelper.getClusterCapabilities(cluster),
"",
LegacyConfigHelper.getPortInfos(cluster),
- LegacyConfigHelper.toLegacy(cluster.revision())
+ LegacyConfigHelper.toLegacy(cluster.revision()),
+ cluster
);
this.ketamaRing = KetamaRing.create(
diff --git a/core-io/src/main/java/com/couchbase/client/core/config/NodeInfo.java b/core-io/src/main/java/com/couchbase/client/core/config/NodeInfo.java
index e2c0c69f1..3f30e739f 100644
--- a/core-io/src/main/java/com/couchbase/client/core/config/NodeInfo.java
+++ b/core-io/src/main/java/com/couchbase/client/core/config/NodeInfo.java
@@ -157,10 +157,18 @@ public String hostname() {
return hostname;
}
+ /**
+ * @deprecated In favor of {@link #id()}
+ */
+ @Deprecated
public NodeIdentifier identifier() {
return nodeIdentifier;
}
+ public com.couchbase.client.core.topology.NodeIdentifier id() {
+ return nodeIdentifier.asTopologyNodeIdentifier();
+ }
+
public Map services() {
return directServices;
}
diff --git a/core-io/src/main/java/com/couchbase/client/core/config/PartitionInfo.java b/core-io/src/main/java/com/couchbase/client/core/config/PartitionInfo.java
index 5c6a5f3a8..8cb87778d 100644
--- a/core-io/src/main/java/com/couchbase/client/core/config/PartitionInfo.java
+++ b/core-io/src/main/java/com/couchbase/client/core/config/PartitionInfo.java
@@ -30,7 +30,7 @@
* Represents the partition information for a bucket.
*
* @since 1.1.0
- * @deprecated In favor of {@link com.couchbase.client.core.topology.PartitionMap
+ * @deprecated In favor of {@link com.couchbase.client.core.topology.PartitionMap}
*/
@Deprecated
@JsonIgnoreProperties(ignoreUnknown = true)
diff --git a/core-io/src/main/java/com/couchbase/client/core/config/PortInfo.java b/core-io/src/main/java/com/couchbase/client/core/config/PortInfo.java
index 2c6f816c0..d5acd6297 100644
--- a/core-io/src/main/java/com/couchbase/client/core/config/PortInfo.java
+++ b/core-io/src/main/java/com/couchbase/client/core/config/PortInfo.java
@@ -21,6 +21,7 @@
import com.couchbase.client.core.deps.com.fasterxml.jackson.annotation.JsonProperty;
import com.couchbase.client.core.node.NodeIdentifier;
import com.couchbase.client.core.service.ServiceType;
+import reactor.util.annotation.Nullable;
import java.util.Collections;
import java.util.HashMap;
@@ -30,7 +31,7 @@
import static java.util.Objects.requireNonNull;
/**
- * @deprecated In favor of {@link com.couchbase.client.core.topology.HostAndServicePorts
+ * @deprecated In favor of {@link com.couchbase.client.core.topology.HostAndServicePorts}
*/
@Deprecated
@JsonIgnoreProperties(ignoreUnknown = true)
@@ -40,6 +41,7 @@ public class PortInfo {
private final Map sslPorts;
private final Map alternateAddresses;
private final String hostname;
+ private final @Nullable String serverGroup;
private final NodeIdentifier nodeIdentifier;
/**
@@ -54,12 +56,14 @@ public class PortInfo {
public PortInfo(
@JsonProperty("services") Map services,
@JsonProperty("hostname") String hostname,
- @JsonProperty("alternateAddresses") Map aa
+ @JsonProperty("alternateAddresses") Map aa,
+ @JsonProperty("serverGroup") String serverGroup
) {
ports = new HashMap<>();
sslPorts = new HashMap<>();
alternateAddresses = aa == null ? Collections.emptyMap() : aa;
this.hostname = hostname; // might be null when decoded from JSON, covered at a higher level
+ this.serverGroup = serverGroup;
extractPorts(services, ports, sslPorts);
@@ -75,11 +79,12 @@ public PortInfo(
* @param hostname the hostname of the port info (node).
*/
PortInfo(final Map ports, final Map sslPorts,
- final Map alternateAddresses, final String hostname) {
+ final Map alternateAddresses, final String hostname, final @Nullable String serverGroup) {
this.ports = requireNonNull(ports);
this.sslPorts = requireNonNull(sslPorts);
this.alternateAddresses = requireNonNull(alternateAddresses);
this.hostname = requireNonNull(hostname);
+ this.serverGroup = serverGroup;
this.nodeIdentifier = initNodeIdentifier(hostname, ports, sslPorts);
}
@@ -88,17 +93,27 @@ public PortInfo(
final Map sslPorts,
final Map alternateAddresses,
final String hostname,
- final NodeIdentifier nodeIdentifier
+ final NodeIdentifier nodeIdentifier,
+ final @Nullable String serverGroup
) {
this.ports = requireNonNull(ports);
this.sslPorts = requireNonNull(sslPorts);
this.alternateAddresses = requireNonNull(alternateAddresses);
this.hostname = requireNonNull(hostname);
this.nodeIdentifier = requireNonNull(nodeIdentifier);
+ this.serverGroup = serverGroup;
}
+ /**
+ * @deprecated In favor of {@link #id()}
+ */
+ @Deprecated
public NodeIdentifier identifier() {
- return nodeIdentifier;
+ return nodeIdentifier;
+ }
+
+ public com.couchbase.client.core.topology.NodeIdentifier id() {
+ return nodeIdentifier.asTopologyNodeIdentifier();
}
/**
@@ -183,6 +198,11 @@ public Map alternateAddresses() {
return alternateAddresses;
}
+ @Nullable
+ public String serverGroup() {
+ return serverGroup;
+ }
+
@Override
public String toString() {
return "PortInfo{"
@@ -190,6 +210,7 @@ public String toString() {
+ ", sslPorts=" + sslPorts
+ ", hostname='" + hostname
+ ", alternateAddresses=" + alternateAddresses
+ + ", serverGroup=" + serverGroup
+ '\'' + '}';
}
diff --git a/core-io/src/main/java/com/couchbase/client/core/config/loader/BaseBucketLoader.java b/core-io/src/main/java/com/couchbase/client/core/config/loader/BaseBucketLoader.java
index eb14ad0c3..5e4333b71 100644
--- a/core-io/src/main/java/com/couchbase/client/core/config/loader/BaseBucketLoader.java
+++ b/core-io/src/main/java/com/couchbase/client/core/config/loader/BaseBucketLoader.java
@@ -20,9 +20,9 @@
import com.couchbase.client.core.config.ProposedBucketConfigContext;
import com.couchbase.client.core.error.ConfigException;
import com.couchbase.client.core.error.SeedNodeOutdatedException;
-import com.couchbase.client.core.node.NodeIdentifier;
import com.couchbase.client.core.service.ServiceState;
import com.couchbase.client.core.service.ServiceType;
+import com.couchbase.client.core.topology.NodeIdentifier;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -82,7 +82,7 @@ public abstract class BaseBucketLoader implements BucketLoader {
* all non-config exceptions into config exceptions so that the upper level only needs to handle
* one specific exception type.
*
- * At this point, we are passing an {@link Optional#empty()} for alternate addresses when the
+ *
At this point, we are passing a null for server group info when the
* service is created, since we do not have a config to check against at this point. The config provider
* will take care of this at a later point in time, before the rest of the bootstrap happens.
*
@@ -98,8 +98,8 @@ public Mono load(final NodeIdentifier seed, final i
.then(ensureServiceConnected(seed, serviceType, Optional.of(bucket)))
.then(discoverConfig(seed, bucket))
.map(config -> new String(config, UTF_8))
- .map(config -> config.replace("$HOST", seed.address()))
- .map(config -> new ProposedBucketConfigContext(bucket, config, seed.address()))
+ .map(config -> config.replace("$HOST", seed.hostForNetworkConnections()))
+ .map(config -> new ProposedBucketConfigContext(bucket, config, seed.hostForNetworkConnections()))
.onErrorResume(ex -> Mono.error(ex instanceof ConfigException
? ex
: new ConfigException("Caught exception while loading config.", ex)
diff --git a/core-io/src/main/java/com/couchbase/client/core/config/loader/BucketLoader.java b/core-io/src/main/java/com/couchbase/client/core/config/loader/BucketLoader.java
index ffd3aa617..a0006ec97 100644
--- a/core-io/src/main/java/com/couchbase/client/core/config/loader/BucketLoader.java
+++ b/core-io/src/main/java/com/couchbase/client/core/config/loader/BucketLoader.java
@@ -17,7 +17,7 @@
package com.couchbase.client.core.config.loader;
import com.couchbase.client.core.config.ProposedBucketConfigContext;
-import com.couchbase.client.core.node.NodeIdentifier;
+import com.couchbase.client.core.topology.NodeIdentifier;
import reactor.core.publisher.Mono;
/**
diff --git a/core-io/src/main/java/com/couchbase/client/core/config/loader/ClusterManagerBucketLoader.java b/core-io/src/main/java/com/couchbase/client/core/config/loader/ClusterManagerBucketLoader.java
index d1d2b96ca..83f8f4f41 100644
--- a/core-io/src/main/java/com/couchbase/client/core/config/loader/ClusterManagerBucketLoader.java
+++ b/core-io/src/main/java/com/couchbase/client/core/config/loader/ClusterManagerBucketLoader.java
@@ -23,9 +23,9 @@
import com.couchbase.client.core.error.ConfigException;
import com.couchbase.client.core.error.NoAccessDuringConfigLoadException;
import com.couchbase.client.core.msg.manager.BucketConfigRequest;
-import com.couchbase.client.core.node.NodeIdentifier;
import com.couchbase.client.core.retry.BestEffortRetryStrategy;
import com.couchbase.client.core.service.ServiceType;
+import com.couchbase.client.core.topology.NodeIdentifier;
import reactor.core.publisher.Mono;
import static com.couchbase.client.core.logging.RedactableArgument.redactMeta;
diff --git a/core-io/src/main/java/com/couchbase/client/core/config/loader/GlobalLoader.java b/core-io/src/main/java/com/couchbase/client/core/config/loader/GlobalLoader.java
index f466892f7..144d4bce2 100644
--- a/core-io/src/main/java/com/couchbase/client/core/config/loader/GlobalLoader.java
+++ b/core-io/src/main/java/com/couchbase/client/core/config/loader/GlobalLoader.java
@@ -26,9 +26,9 @@
import com.couchbase.client.core.error.UnsupportedConfigMechanismException;
import com.couchbase.client.core.msg.ResponseStatus;
import com.couchbase.client.core.msg.kv.CarrierGlobalConfigRequest;
-import com.couchbase.client.core.node.NodeIdentifier;
import com.couchbase.client.core.retry.BestEffortRetryStrategy;
import com.couchbase.client.core.service.ServiceType;
+import com.couchbase.client.core.topology.NodeIdentifier;
import reactor.core.publisher.Mono;
import java.util.Optional;
@@ -53,9 +53,9 @@ public GlobalLoader(final Core core) {
/**
* Tries to load the global configuration.
*
- * Please note that at this point, we are passing an {@link Optional#empty()} for alternate addresses when the
+ *
Please note that at this point, we are passing a null for server group info when the
* service is created, since we do not have a config to check against at this point. The config provider
- * will take care of this at a later point in time, before the rest of the bootstrap happens.
+ * will take care of this at a later point in time, before the rest of the bootstrap happens.
*
* @param seed the seed node to load from.
* @param port the port number for the KV service.
@@ -66,8 +66,8 @@ public Mono load(final NodeIdentifier seed, final i
.ensureServiceAt(seed, ServiceType.KV, port, Optional.empty())
.then(discoverConfig(seed))
.map(config -> new String(config, UTF_8))
- .map(config -> config.replace("$HOST", seed.address()))
- .map(config -> new ProposedGlobalConfigContext(config, seed.address()))
+ .map(config -> config.replace("$HOST", seed.hostForNetworkConnections()))
+ .map(config -> new ProposedGlobalConfigContext(config, seed.hostForNetworkConnections()))
.onErrorResume(ex -> Mono.error(ex instanceof ConfigException
? ex
: new ConfigException("Caught exception while loading global config.", ex)
diff --git a/core-io/src/main/java/com/couchbase/client/core/config/loader/KeyValueBucketLoader.java b/core-io/src/main/java/com/couchbase/client/core/config/loader/KeyValueBucketLoader.java
index f35f8ef9b..cd4b1ee1a 100644
--- a/core-io/src/main/java/com/couchbase/client/core/config/loader/KeyValueBucketLoader.java
+++ b/core-io/src/main/java/com/couchbase/client/core/config/loader/KeyValueBucketLoader.java
@@ -25,9 +25,9 @@
import com.couchbase.client.core.io.CollectionIdentifier;
import com.couchbase.client.core.msg.ResponseStatus;
import com.couchbase.client.core.msg.kv.CarrierBucketConfigRequest;
-import com.couchbase.client.core.node.NodeIdentifier;
import com.couchbase.client.core.retry.BestEffortRetryStrategy;
import com.couchbase.client.core.service.ServiceType;
+import com.couchbase.client.core.topology.NodeIdentifier;
import reactor.core.publisher.Mono;
import java.util.Optional;
diff --git a/core-io/src/main/java/com/couchbase/client/core/config/refresher/GlobalRefresher.java b/core-io/src/main/java/com/couchbase/client/core/config/refresher/GlobalRefresher.java
index 8d05dc0bb..6b4a7f769 100644
--- a/core-io/src/main/java/com/couchbase/client/core/config/refresher/GlobalRefresher.java
+++ b/core-io/src/main/java/com/couchbase/client/core/config/refresher/GlobalRefresher.java
@@ -163,7 +163,7 @@ private Flux attemptUpdateGlobalConfig(final Flux fetchConfigPerNode(final String name,
ctx,
new CollectionIdentifier(name, Optional.empty(), Optional.empty()),
FailFastRetryStrategy.INSTANCE,
- nodeInfo.identifier(),
+ nodeInfo.id(),
currentVersion(name)
);
core.send(request);
diff --git a/core-io/src/main/java/com/couchbase/client/core/diagnostics/HealthPinger.java b/core-io/src/main/java/com/couchbase/client/core/diagnostics/HealthPinger.java
index 7ec808b5f..d7ed88287 100644
--- a/core-io/src/main/java/com/couchbase/client/core/diagnostics/HealthPinger.java
+++ b/core-io/src/main/java/com/couchbase/client/core/diagnostics/HealthPinger.java
@@ -19,11 +19,7 @@
import com.couchbase.client.core.Core;
import com.couchbase.client.core.Reactor;
import com.couchbase.client.core.annotation.Stability;
-import com.couchbase.client.core.config.BucketConfig;
import com.couchbase.client.core.config.ClusterConfig;
-import com.couchbase.client.core.config.GlobalConfig;
-import com.couchbase.client.core.config.NodeInfo;
-import com.couchbase.client.core.config.PortInfo;
import com.couchbase.client.core.endpoint.http.CoreCommonOptions;
import com.couchbase.client.core.endpoint.http.CoreHttpRequest;
import com.couchbase.client.core.error.TimeoutException;
@@ -32,22 +28,25 @@
import com.couchbase.client.core.msg.RequestTarget;
import com.couchbase.client.core.msg.kv.KvPingRequest;
import com.couchbase.client.core.msg.kv.KvPingResponse;
-import com.couchbase.client.core.node.NodeIdentifier;
-import com.couchbase.client.core.protostellar.CoreProtostellarUtil;
import com.couchbase.client.core.retry.RetryStrategy;
import com.couchbase.client.core.service.ServiceType;
+import com.couchbase.client.core.topology.BucketCapability;
+import com.couchbase.client.core.topology.ClusterTopology;
+import com.couchbase.client.core.topology.ClusterTopologyWithBucket;
+import com.couchbase.client.core.topology.HostAndServicePorts;
import com.couchbase.client.core.util.CbThrowables;
-import com.couchbase.client.core.util.HostAndPort;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.annotation.Nullable;
import java.time.Duration;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
@@ -65,7 +64,6 @@
import static com.couchbase.client.core.util.CbCollections.transformValues;
import static java.util.Collections.unmodifiableSet;
import static java.util.stream.Collectors.groupingBy;
-import static java.util.stream.Collectors.toSet;
/**
* The {@link HealthPinger} allows to "ping" individual services with real operations for their health.
@@ -86,6 +84,11 @@ public class HealthPinger {
ANALYTICS
));
+ private static final Set servicesThatRequireBucket = unmodifiableSet(EnumSet.of(
+ KV,
+ VIEWS
+ ));
+
@Stability.Internal
public static Mono ping(
final Core core,
@@ -143,85 +146,59 @@ static Set extractPingTargets(
final Optional bucketName,
final WaitUntilReadyHelper.WaitUntilReadyLogger log
) {
- final Set serviceTypes = isNullOrEmpty(serviceTypesOrEmpty)
+ final String bucket = bucketName.orElse(null);
+ final Set serviceTypeFilter = isNullOrEmpty(serviceTypesOrEmpty)
? EnumSet.allOf(ServiceType.class)
: EnumSet.copyOf(serviceTypesOrEmpty);
- serviceTypes.retainAll(pingableServices); // narrow to the ones we can actually ping
-
- Set targets = new HashSet<>();
- log.message("extractPingTargets: starting ping target extraction with candidate services: " + serviceTypes);
-
- if (!bucketName.isPresent()) {
- if (clusterConfig.globalConfig() != null) {
- GlobalConfig globalConfig = clusterConfig.globalConfig();
+ serviceTypeFilter.retainAll(pingableServices); // narrow to the ones we can actually ping
- log.message("extractPingTargets: getting ping targets from global config portInfos: " + globalConfig.portInfos());
- for (PortInfo portInfo : globalConfig.portInfos()) {
- for (ServiceType serviceType : portInfo.ports().keySet()) {
- if (serviceType == ServiceType.KV || serviceType == ServiceType.VIEWS) {
- // do not check bucket-level resources from a global level (null bucket name will not work)
- continue;
- }
- RequestTarget target = new RequestTarget(serviceType, portInfo.identifier(), null);
- log.message("extractPingTargets: adding target from global config: " + target);
- targets.add(target);
- }
- }
- log.message("extractPingTargets: ping targets after scanning global config: " + targets);
- } else {
- log.message("extractPingTargets: globalConfig is absent");
- }
- for (Map.Entry bucketConfig : clusterConfig.bucketConfigs().entrySet()) {
- log.message("extractPingTargets: getting targets from bucket config via global config for bucket " + bucketConfig.getKey() + " : " + bucketConfig.getValue());
+ log.message("extractPingTargets: starting ping target extraction with candidate services: " + serviceTypeFilter + " and bucket: " + bucket);
- for (NodeInfo nodeInfo : bucketConfig.getValue().nodes()) {
- for (ServiceType serviceType : nodeInfo.services().keySet()) {
- if (serviceType == ServiceType.KV || serviceType == ServiceType.VIEWS) {
- // do not check bucket-level resources from a global level (null bucket name will not work)
- continue;
- }
- RequestTarget target = new RequestTarget(serviceType, nodeInfo.identifier(), null);
- log.message("extractPingTargets: adding target from bucket config via global config: " + target);
- targets.add(new RequestTarget(serviceType, nodeInfo.identifier(), null));
- }
- }
+ final List topologiesToScan = new ArrayList<>();
+ if (bucket != null) {
+ ClusterTopologyWithBucket topology = clusterConfig.bucketTopology(bucket);
+ if (topology != null && !topology.bucket().hasCapability(BucketCapability.COUCHAPI)) {
+ serviceTypeFilter.remove(VIEWS);
}
+ topologiesToScan.add(topology);
} else {
- BucketConfig bucketConfig = clusterConfig.bucketConfig(bucketName.get());
- if (bucketConfig != null) {
- log.message("extractPingTargets: Getting targets from bucket config: " + bucketConfig);
- for (NodeInfo nodeInfo : bucketConfig.nodes()) {
- for (ServiceType serviceType : nodeInfo.services().keySet()) {
- RequestTarget target;
- if (serviceType != ServiceType.VIEWS && serviceType != ServiceType.KV) {
- target = new RequestTarget(serviceType, nodeInfo.identifier(), null);
- } else {
- target = new RequestTarget(serviceType, nodeInfo.identifier(), bucketName.get());
- }
+ serviceTypeFilter.removeAll(servicesThatRequireBucket); // narrow to the ones that can be pinged without a bucket
+ topologiesToScan.add(clusterConfig.globalTopology());
+ topologiesToScan.addAll(clusterConfig.bucketTopologies());
+ }
- log.message("extractPingTargets: adding target from bucket config: " + target);
- targets.add(target);
+ Set result = new HashSet<>();
+ topologiesToScan.stream()
+ .filter(Objects::nonNull) // global or specific bucket topology might be absent
+ .forEach(topology -> {
+ log.message("extractPingTargets: scanning " + describe(topology));
+ for (HostAndServicePorts node : topology.nodes()) {
+ for (ServiceType advertisedService : advertisedServices(node)) {
+ if (serviceTypeFilter.contains(advertisedService)) {
+ boolean serviceRequiresBucket = servicesThatRequireBucket.contains(advertisedService);
+ String targetBucketOrNull = serviceRequiresBucket ? bucket : null;
+ RequestTarget target = new RequestTarget(advertisedService, node.id(), targetBucketOrNull);
+ if (result.add(target)) {
+ log.message("extractPingTargets: found new target " + target);
+ }
+ }
}
}
- } else {
- log.message("extractPingTargets: Bucket name was present, but clusterConfig has no config for bucket " + bucketName);
- }
- }
-
- // Narrow the results to only pingable services the caller is interested in.
- targets = targets.stream()
- .filter(t -> serviceTypes.contains(t.serviceType()))
- .collect(toSet());
+ });
- log.message(
- "extractPingTargets: Finished. Returning filtered targets (grouped by node): " + formatGroupedByNode(targets)
- );
+ log.message("extractPingTargets: Finished. Returning filtered targets (grouped by node): " + formatGroupedByNode(result));
+ return result;
+ }
- return targets;
+ private static String describe(ClusterTopology topology) {
+ String bucketOrGlobal = (topology instanceof ClusterTopologyWithBucket)
+ ? "bucket '" + topology.requireBucket().bucket().name() + "'"
+ : "global";
+ return "topology from " + bucketOrGlobal + " ; nodes=" + topology.nodes();
}
- private static String format(NodeIdentifier id) {
- return new HostAndPort(id.address(), id.managerPort()).format();
+ private static Set advertisedServices(HostAndServicePorts node) {
+ return node.ports().keySet();
}
/**
@@ -230,7 +207,7 @@ private static String format(NodeIdentifier id) {
*/
static Map> formatGroupedByNode(Collection targets) {
Map> grouped = targets.stream()
- .collect(Collectors.groupingBy(requestTarget -> redactSystem(format(requestTarget.nodeIdentifier())).toString()));
+ .collect(Collectors.groupingBy(requestTarget -> redactSystem(requestTarget.nodeIdentifier()).toString()));
return transformValues(grouped, it -> transform(it, target -> target.serviceType().toString()));
}
diff --git a/core-io/src/main/java/com/couchbase/client/core/endpoint/DeferredCloseEndpoint.java b/core-io/src/main/java/com/couchbase/client/core/endpoint/DeferredCloseEndpoint.java
index f3bd578eb..23aa4b7cb 100644
--- a/core-io/src/main/java/com/couchbase/client/core/endpoint/DeferredCloseEndpoint.java
+++ b/core-io/src/main/java/com/couchbase/client/core/endpoint/DeferredCloseEndpoint.java
@@ -61,10 +61,21 @@ private void closeWhenDone() {
@Stability.Internal
@Override
public synchronized void markRequestCompletion() {
+ maybeResumeDisconnect();
super.markRequestCompletion();
- if (closeWhenDone && outstandingRequests() <= 0) {
+ }
+
+ @Stability.Internal
+ @Override
+ public synchronized void notifyChannelInactive() {
+ maybeResumeDisconnect();
+ super.notifyChannelInactive();
+ }
+
+ private void maybeResumeDisconnect() {
+ if (closeWhenDone) {
endpointContext.get().environment().eventBus().publish(new EndpointDisconnectResumedEvent(endpointContext.get()));
- closeChannel(this.channel);
+ super.disconnect();
closeWhenDone = false;
}
}
diff --git a/core-io/src/main/java/com/couchbase/client/core/endpoint/ProtostellarEndpoint.java b/core-io/src/main/java/com/couchbase/client/core/endpoint/ProtostellarEndpoint.java
index b686418e7..516496889 100644
--- a/core-io/src/main/java/com/couchbase/client/core/endpoint/ProtostellarEndpoint.java
+++ b/core-io/src/main/java/com/couchbase/client/core/endpoint/ProtostellarEndpoint.java
@@ -234,9 +234,9 @@ private ManagedChannel channel(ProtostellarContext ctx) {
// Retry strategies to be determined, but presumably we will need something custom rather than what GRPC provides
.disableRetry();
- if (ctx.environment().requestTracer() != null
- && ctx.environment().requestTracer() instanceof GrpcAwareRequestTracer) {
- ((GrpcAwareRequestTracer) ctx.environment().requestTracer()).registerGrpc(builder);
+ if (ctx.coreResources().requestTracer() != null
+ && ctx.coreResources().requestTracer() instanceof GrpcAwareRequestTracer) {
+ ((GrpcAwareRequestTracer) ctx.coreResources().requestTracer()).registerGrpc(builder);
}
// JVMCBC-1187: experimental code for performance testing that will be removed pre-GA.
diff --git a/core-io/src/main/java/com/couchbase/client/core/endpoint/http/CoreHttpRequest.java b/core-io/src/main/java/com/couchbase/client/core/endpoint/http/CoreHttpRequest.java
index 70a1c8c49..c58a0011f 100644
--- a/core-io/src/main/java/com/couchbase/client/core/endpoint/http/CoreHttpRequest.java
+++ b/core-io/src/main/java/com/couchbase/client/core/endpoint/http/CoreHttpRequest.java
@@ -40,9 +40,9 @@
import com.couchbase.client.core.msg.BaseRequest;
import com.couchbase.client.core.msg.NonChunkedHttpRequest;
import com.couchbase.client.core.msg.RequestTarget;
-import com.couchbase.client.core.node.NodeIdentifier;
import com.couchbase.client.core.retry.RetryStrategy;
import com.couchbase.client.core.service.ServiceType;
+import com.couchbase.client.core.topology.NodeIdentifier;
import com.couchbase.client.core.util.UrlQueryStringBuilder;
import reactor.util.annotation.Nullable;
@@ -161,7 +161,7 @@ public Map serviceContext() {
ctx.put("method", method.toString());
ctx.put("path", redactMeta(pathAndQueryString()));
if (target() != null) {
- ctx.put("target", redactSystem(target().address()));
+ ctx.put("target", redactSystem(target()));
}
if (bucket() != null) {
ctx.put("bucket", redactMeta(bucket()));
@@ -320,7 +320,7 @@ public Builder content(byte[] content, CharSequence contentType) {
}
public CoreHttpRequest build() {
- RequestSpan span = spanName == null ? null : CbTracing.newSpan(coreContext.environment().requestTracer(), spanName, options.parentSpan().orElse(null));
+ RequestSpan span = spanName == null ? null : coreContext.coreResources().requestTracer().requestSpan(spanName, options.parentSpan().orElse(null));
if (span != null && !CbTracing.isInternalSpan(span)) {
if (target.bucketName() != null) {
diff --git a/core-io/src/main/java/com/couchbase/client/core/env/BuilderPropertySetter.java b/core-io/src/main/java/com/couchbase/client/core/env/BuilderPropertySetter.java
index 81d0f37f3..f3f9bdd0e 100644
--- a/core-io/src/main/java/com/couchbase/client/core/env/BuilderPropertySetter.java
+++ b/core-io/src/main/java/com/couchbase/client/core/env/BuilderPropertySetter.java
@@ -44,11 +44,11 @@
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Function;
-import java.util.stream.Collectors;
import static com.couchbase.client.core.util.CbCollections.mapCopyOf;
import static com.couchbase.client.core.util.CbCollections.mapOf;
import static java.util.Objects.requireNonNull;
+import static java.util.stream.Collectors.toList;
@SuppressWarnings("rawtypes")
@Stability.Internal
@@ -60,16 +60,22 @@ public class BuilderPropertySetter {
// Escape hatch in case some accessors don't follow the convention.
private final Map irregularChildBuilderAccessors;
+ // Converts an input path component to match the Java method name,
+ // for translating case conventions.
+ private final Function pathComponentTransformer;
+
public BuilderPropertySetter() {
- this("Config", mapOf("ioEnvironment", "ioEnvironment"));
+ this("Config", mapOf("ioEnvironment", "ioEnvironment"), name -> name);
}
public BuilderPropertySetter(
String childBuilderAccessorSuffix,
- Map irregularChildBuilderAccessors
+ Map irregularChildBuilderAccessors,
+ Function pathComponentTransformer
) {
this.childBuilderAccessorSuffix = requireNonNull(childBuilderAccessorSuffix);
this.irregularChildBuilderAccessors = mapCopyOf(irregularChildBuilderAccessors);
+ this.pathComponentTransformer = requireNonNull(pathComponentTransformer);
}
/**
@@ -83,10 +89,11 @@ public void set(Object builder, Map properties) {
* @throws InvalidPropertyException if the property could not be applied to the builder
*/
public void set(Object builder, String propertyName, String propertyValue) {
-
-
try {
- final List propertyComponents = Arrays.asList(propertyName.split("\\.", -1));
+ final List propertyComponents = Arrays.stream(propertyName.split("\\.", -1))
+ .map(pathComponentTransformer)
+ .collect(toList());
+
final List pathToBuilder = propertyComponents.subList(0, propertyComponents.size() - 1);
final String setterName = propertyComponents.get(propertyComponents.size() - 1);
@@ -111,7 +118,7 @@ public void set(Object builder, String propertyName, String propertyValue) {
final List candidates = Arrays.stream(builder.getClass().getMethods())
.filter(m -> m.getName().equals(setterName))
.filter(m -> m.getParameterCount() == 1)
- .collect(Collectors.toList());
+ .collect(toList());
if (candidates.isEmpty()) {
throw InvalidArgumentException.fromMessage("No one-arg setter for property \"" + propertyName + "\" in " + builder.getClass());
@@ -256,7 +263,7 @@ private static E convertEnum(Class enumClass, String value)
try {
return (E) Enum.valueOf(enumClass, value);
} catch (IllegalArgumentException e) {
- List enumValueNames = Arrays.stream(enumClass.getEnumConstants()).map(Enum::name).collect(Collectors.toList());
+ List enumValueNames = Arrays.stream(enumClass.getEnumConstants()).map(Enum::name).collect(toList());
throw InvalidArgumentException.fromMessage("Expected one of " + enumValueNames + " but got \"" + value + "\"");
}
}
diff --git a/core-io/src/main/java/com/couchbase/client/core/env/CoreEnvironment.java b/core-io/src/main/java/com/couchbase/client/core/env/CoreEnvironment.java
index 7bef1913f..2ea5c4216 100644
--- a/core-io/src/main/java/com/couchbase/client/core/env/CoreEnvironment.java
+++ b/core-io/src/main/java/com/couchbase/client/core/env/CoreEnvironment.java
@@ -42,9 +42,12 @@
import com.couchbase.client.core.transaction.config.CoreTransactionsConfig;
import com.couchbase.client.core.transaction.forwards.CoreTransactionsSupportedExtensions;
import com.couchbase.client.core.transaction.util.CoreTransactionsSchedulers;
+import com.couchbase.client.core.util.ReactorOps;
+import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;
+import reactor.util.annotation.Nullable;
import java.time.Duration;
import java.util.ArrayList;
@@ -63,6 +66,7 @@
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
+import java.util.function.Supplier;
import static com.couchbase.client.core.env.OwnedOrExternal.external;
import static com.couchbase.client.core.env.OwnedOrExternal.owned;
@@ -75,7 +79,7 @@
* Note that unless you are using the core directly, you want to consider the child implementations for each
* language binding (i.e. the ClusterEnvironment for the java client).
*/
-public class CoreEnvironment implements AutoCloseable {
+public class CoreEnvironment implements ReactorOps, AutoCloseable {
private static final VersionAndGitHash coreVersion = VersionAndGitHash.from(Core.class);
private static final String CORE_AGENT_TITLE = "java-core";
@@ -112,6 +116,7 @@ public class CoreEnvironment implements AutoCloseable {
private final RetryStrategy retryStrategy;
private final OwnedOrExternal scheduler;
private final OwnedOrExternal executor;
+ @Nullable private final Supplier userScheduler;
private final int schedulerThreadCount;
private final OrphanReporter orphanReporter;
private final long maxNumRequestsInRetry;
@@ -120,6 +125,7 @@ public class CoreEnvironment implements AutoCloseable {
private final Set appliedProfiles;
private final CoreTransactionsSchedulers transactionsSchedulers = new CoreTransactionsSchedulers();
+ private final @Nullable String preferredServerGroup;
public static CoreEnvironment create() {
return builder().build();
@@ -138,6 +144,7 @@ protected CoreEnvironment(final Builder> builder) {
.orElse(owned(
Schedulers.newParallel("cb-comp", schedulerThreadCount, true))
);
+ this.userScheduler = builder.userScheduler;
// JVMCBC-1196: configuration options for the executor will be provided.
String executorMaxThreadCountRaw = System.getProperty("com.couchbase.protostellar.executorMaxThreadCount");
@@ -208,6 +215,7 @@ protected CoreEnvironment(final Builder> builder) {
}
this.requestCallbacks = Collections.unmodifiableList(builder.requestCallbacks);
+ this.preferredServerGroup = builder.preferredServerGroup;
checkInsecureTlsConfig();
}
@@ -351,6 +359,26 @@ public Scheduler scheduler() {
return scheduler.get();
}
+ /**
+ * Returns the supplier for the scheduler where Reactive API results should be published,
+ * or null if the user does not want to switch schedulers.
+ */
+ @Stability.Internal
+ @Nullable
+ public Supplier userScheduler() {
+ return userScheduler;
+ }
+
+ @Stability.Internal
+ public Mono publishOnUserScheduler(Mono mono) {
+ return userScheduler == null ? mono : Mono.defer(() -> mono.publishOn(userScheduler.get()));
+ }
+
+ @Stability.Internal
+ public Flux publishOnUserScheduler(Flux flux) {
+ return userScheduler == null ? flux : Flux.defer(() -> flux.publishOn(userScheduler.get()));
+ }
+
/**
* Returns the executor used to schedule non-reactive async tasks across the SDK.
*/
@@ -363,8 +391,13 @@ public Executor executor() {
* Returns the request tracer for response time observability.
*
* Note that this right now is unsupported, volatile API and subject to change!
+ *
+ * @deprecated consumers should use {@link com.couchbase.client.core.CoreResources} instead, as the RequestTracer returned
+ * from that adds useful additional spans that are specific to a Core, and cannot be added here (as this class can be
+ * shared between Clusters).
*/
@Stability.Volatile
+ @Deprecated
public RequestTracer requestTracer() {
return requestTracer.get();
}
@@ -420,6 +453,13 @@ public CoreTransactionsSchedulers transactionsSchedulers() {
return transactionsSchedulers;
}
+ /**
+ * The preferred server group to use for operations that support such.
+ */
+ public @Nullable String preferredServerGroup() {
+ return preferredServerGroup;
+ }
+
/**
* Shuts down this Environment with the default disconnect timeout.
*
@@ -587,6 +627,7 @@ public static class Builder> {
private LoggingMeterConfig.Builder loggingMeterConfig = new LoggingMeterConfig.Builder();
private OwnedOrExternal eventBus = null;
private OwnedOrExternal scheduler = null;
+ private Supplier userScheduler = null;
private int schedulerThreadCount = Schedulers.DEFAULT_POOL_SIZE;
private OwnedOrExternal requestTracer = null;
private OwnedOrExternal meter = null;
@@ -594,6 +635,7 @@ public static class Builder> {
private long maxNumRequestsInRetry = DEFAULT_MAX_NUM_REQUESTS_IN_RETRY;
private final List requestCallbacks = new ArrayList<>();
protected CoreTransactionsConfig transactionsConfig = null;
+ private String preferredServerGroup = null;
private final Set appliedProfiles = new LinkedHashSet<>();
@@ -892,6 +934,22 @@ public SELF thresholdLoggingTracerConfig(final ThresholdLoggingTracerConfig.Buil
return self();
}
+ /**
+ * Specifies the supplier the SDK uses to get the Scheduler for publishing Reactive API results.
+ *
+ * Defaults to null, which means reactive results are published immediately
+ * in a thread owned by the SDK -- typically the SDK's Netty event loop.
+ *
+ * The supplier is invoked once for every subscription, by the same thread that subscribes to the Mono/Flux.
+ *
+ * @return this {@link Builder} for chaining purposes.
+ */
+ @Stability.Volatile
+ public SELF publishOnScheduler(@Nullable final Supplier publishOnScheduler) {
+ this.userScheduler = publishOnScheduler;
+ return self();
+ }
+
public ThresholdLoggingTracerConfig.Builder thresholdLoggingTracerConfig() {
return thresholdLoggingTracerConfig;
}
@@ -1069,8 +1127,7 @@ public SELF retryStrategy(final RetryStrategy retryStrategy) {
public SELF requestTracer(final RequestTracer requestTracer) {
notNull(requestTracer, "RequestTracer");
- boolean ignoresAttributes = CbTracing.isInternalTracer(requestTracer);
- this.requestTracer = external(ignoresAttributes ? requestTracer : new RequestTracerWithCommonAttributes(requestTracer));
+ this.requestTracer = external(requestTracer);
return self();
}
@@ -1150,6 +1207,16 @@ public SELF applyProfile(final String profileName) {
+ registeredProfileNames());
}
+ /**
+ * Sets a preferred server group, that will be used for operations that support this feature.
+ *
+ * @return this {@link Builder} for chaining purposes.
+ */
+ public SELF preferredServerGroup(final @Nullable String preferredServerGroup) {
+ this.preferredServerGroup = preferredServerGroup;
+ return self();
+ }
+
/**
* You might wonder why callers can't use
* {@link #load(PropertyLoader)} to load system properties.
diff --git a/core-io/src/main/java/com/couchbase/client/core/env/IoConfig.java b/core-io/src/main/java/com/couchbase/client/core/env/IoConfig.java
index 187733d5b..c33a00aee 100644
--- a/core-io/src/main/java/com/couchbase/client/core/env/IoConfig.java
+++ b/core-io/src/main/java/com/couchbase/client/core/env/IoConfig.java
@@ -24,6 +24,7 @@
import com.couchbase.client.core.node.StandardMemcachedHashingStrategy;
import com.couchbase.client.core.service.AbstractPooledEndpointServiceConfig;
import com.couchbase.client.core.service.ServiceType;
+import reactor.util.annotation.Nullable;
import java.time.Duration;
import java.util.Arrays;
diff --git a/core-io/src/main/java/com/couchbase/client/core/env/PasswordAuthenticator.java b/core-io/src/main/java/com/couchbase/client/core/env/PasswordAuthenticator.java
index 88cd7e953..9e8aefa19 100644
--- a/core-io/src/main/java/com/couchbase/client/core/env/PasswordAuthenticator.java
+++ b/core-io/src/main/java/com/couchbase/client/core/env/PasswordAuthenticator.java
@@ -227,7 +227,7 @@ private Builder(String username, String password) {
}
/**
- * @deprecated Please use {@link Builder(String, String) or {@link Builder(Supplier)} instead.
+ * @deprecated Please use {@link Builder(String, String)} or {@link Builder(Supplier)} instead.
*/
@Deprecated
public Builder() {
diff --git a/core-io/src/main/java/com/couchbase/client/core/env/RequestTracerWithCommonAttributes.java b/core-io/src/main/java/com/couchbase/client/core/env/RequestTracerDecorator.java
similarity index 64%
rename from core-io/src/main/java/com/couchbase/client/core/env/RequestTracerWithCommonAttributes.java
rename to core-io/src/main/java/com/couchbase/client/core/env/RequestTracerDecorator.java
index c652eefaf..b4a11f5d6 100644
--- a/core-io/src/main/java/com/couchbase/client/core/env/RequestTracerWithCommonAttributes.java
+++ b/core-io/src/main/java/com/couchbase/client/core/env/RequestTracerDecorator.java
@@ -16,29 +16,41 @@
package com.couchbase.client.core.env;
+import com.couchbase.client.core.annotation.Stability;
import com.couchbase.client.core.cnc.RequestSpan;
import com.couchbase.client.core.cnc.RequestTracer;
import com.couchbase.client.core.cnc.TracingIdentifiers;
+import com.couchbase.client.core.topology.ClusterIdentifier;
import reactor.core.publisher.Mono;
+import reactor.util.annotation.Nullable;
import java.time.Duration;
+import java.util.function.Supplier;
import static java.util.Objects.requireNonNull;
/**
- * Applies a set of common attributes to spans created by this tracer.
+ * Wraps a {@link RequestTracer} to provide new attributes.
*/
-class RequestTracerWithCommonAttributes implements RequestTracer {
+@Stability.Internal
+public class RequestTracerDecorator implements RequestTracer {
private final RequestTracer wrapped;
+ private final Supplier clusterIdentSupplier;
- RequestTracerWithCommonAttributes(RequestTracer wrapped) {
+ public RequestTracerDecorator(RequestTracer wrapped, Supplier clusterIdentSupplier) {
this.wrapped = requireNonNull(wrapped);
+ this.clusterIdentSupplier = clusterIdentSupplier;
}
@Override
public RequestSpan requestSpan(String name, RequestSpan parent) {
RequestSpan span = wrapped.requestSpan(name, parent);
span.lowCardinalityAttribute(TracingIdentifiers.ATTR_SYSTEM, TracingIdentifiers.ATTR_SYSTEM_COUCHBASE);
+ ClusterIdentifier clusterIdent = clusterIdentSupplier.get();
+ if (clusterIdent != null) {
+ span.attribute(TracingIdentifiers.ATTR_CLUSTER_NAME, clusterIdent.clusterName());
+ span.attribute(TracingIdentifiers.ATTR_CLUSTER_UUID, clusterIdent.clusterUuid());
+ }
return span;
}
diff --git a/core-io/src/main/java/com/couchbase/client/core/error/DocumentUnretrievableException.java b/core-io/src/main/java/com/couchbase/client/core/error/DocumentUnretrievableException.java
index 03193c9f4..0b1a3d05d 100644
--- a/core-io/src/main/java/com/couchbase/client/core/error/DocumentUnretrievableException.java
+++ b/core-io/src/main/java/com/couchbase/client/core/error/DocumentUnretrievableException.java
@@ -28,4 +28,11 @@ public DocumentUnretrievableException(final ErrorContext ctx) {
super("No document retrievable with a successful status", ctx);
}
+ public DocumentUnretrievableException(final String message, final ErrorContext ctx) {
+ super(message, ctx);
+ }
+
+ public static DocumentUnretrievableException noReplicasSuitable() {
+ return new DocumentUnretrievableException("No suitable replicas were available. Note that it is advised to always have a try-catch fallback to e.g. a regular get, when using replica gets", null);
+ }
}
diff --git a/core-io/src/main/java/com/couchbase/client/core/io/netty/NonChunkedHttpMessageHandler.java b/core-io/src/main/java/com/couchbase/client/core/io/netty/NonChunkedHttpMessageHandler.java
index a0602d3ce..360a53ebd 100644
--- a/core-io/src/main/java/com/couchbase/client/core/io/netty/NonChunkedHttpMessageHandler.java
+++ b/core-io/src/main/java/com/couchbase/client/core/io/netty/NonChunkedHttpMessageHandler.java
@@ -163,7 +163,7 @@ public void write(final ChannelHandlerContext ctx, final Object msg, final Chann
encoded.headers().set(HttpHeaderNames.USER_AGENT, endpointContext.environment().userAgent().formattedLong());
dispatchTimingStart = System.nanoTime();
if (currentRequest.requestSpan() != null) {
- RequestTracer tracer = endpointContext.environment().requestTracer();
+ RequestTracer tracer = endpointContext.coreResources().requestTracer();
currentDispatchSpan = tracer.requestSpan(TracingIdentifiers.SPAN_DISPATCH, currentRequest.requestSpan());
if (!CbTracing.isInternalTracer(tracer)) {
diff --git a/core-io/src/main/java/com/couchbase/client/core/io/netty/chunk/ChunkedMessageHandler.java b/core-io/src/main/java/com/couchbase/client/core/io/netty/chunk/ChunkedMessageHandler.java
index b4516d002..d811880e6 100644
--- a/core-io/src/main/java/com/couchbase/client/core/io/netty/chunk/ChunkedMessageHandler.java
+++ b/core-io/src/main/java/com/couchbase/client/core/io/netty/chunk/ChunkedMessageHandler.java
@@ -165,7 +165,7 @@ public void write(final ChannelHandlerContext ctx, final Object msg, final Chann
chunkResponseParser.updateRequestContext(currentRequest.context());
dispatchTimingStart = System.nanoTime();
if (currentRequest.requestSpan() != null) {
- RequestTracer tracer = endpointContext.environment().requestTracer();
+ RequestTracer tracer = endpointContext.coreResources().requestTracer();
currentDispatchSpan = tracer.requestSpan(TracingIdentifiers.SPAN_DISPATCH, currentRequest.requestSpan());
if (!CbTracing.isInternalTracer(tracer)) {
diff --git a/core-io/src/main/java/com/couchbase/client/core/io/netty/kv/KeyValueMessageHandler.java b/core-io/src/main/java/com/couchbase/client/core/io/netty/kv/KeyValueMessageHandler.java
index eadcb8418..c4446dc0b 100644
--- a/core-io/src/main/java/com/couchbase/client/core/io/netty/kv/KeyValueMessageHandler.java
+++ b/core-io/src/main/java/com/couchbase/client/core/io/netty/kv/KeyValueMessageHandler.java
@@ -172,7 +172,7 @@ public KeyValueMessageHandler(final BaseEndpoint endpoint, final EndpointContext
this.compressionConfig = endpointContext.environment().compressionConfig();
this.eventBus = endpointContext.environment().eventBus();
this.bucketName = bucketName;
- this.isInternalTracer = CbTracing.isInternalTracer(endpointContext.environment().requestTracer());
+ this.isInternalTracer = CbTracing.isInternalTracer(endpointContext.coreResources().requestTracer());
}
/**
@@ -226,7 +226,7 @@ public void write(final ChannelHandlerContext ctx, final Object msg, final Chann
ctx.write(request.encode(ctx.alloc(), opaque, channelContext), promise);
writtenRequestDispatchTimings.put(opaque, (Long) System.nanoTime());
if (request.requestSpan() != null) {
- RequestTracer tracer = endpointContext.environment().requestTracer();
+ RequestTracer tracer = endpointContext.coreResources().requestTracer();
RequestSpan dispatchSpan = tracer.requestSpan(TracingIdentifiers.SPAN_DISPATCH, request.requestSpan());
if (!isInternalTracer) {
diff --git a/core-io/src/main/java/com/couchbase/client/core/msg/Request.java b/core-io/src/main/java/com/couchbase/client/core/msg/Request.java
index ccb6c3bcb..6a5288827 100644
--- a/core-io/src/main/java/com/couchbase/client/core/msg/Request.java
+++ b/core-io/src/main/java/com/couchbase/client/core/msg/Request.java
@@ -18,9 +18,9 @@
import com.couchbase.client.core.cnc.RequestSpan;
import com.couchbase.client.core.deps.io.netty.util.Timeout;
-import com.couchbase.client.core.node.NodeIdentifier;
import com.couchbase.client.core.retry.RetryStrategy;
import com.couchbase.client.core.service.ServiceType;
+import com.couchbase.client.core.topology.NodeIdentifier;
import reactor.util.annotation.Nullable;
import java.time.Duration;
diff --git a/core-io/src/main/java/com/couchbase/client/core/msg/RequestContext.java b/core-io/src/main/java/com/couchbase/client/core/msg/RequestContext.java
index 1c86e6bea..1d0e2dd22 100644
--- a/core-io/src/main/java/com/couchbase/client/core/msg/RequestContext.java
+++ b/core-io/src/main/java/com/couchbase/client/core/msg/RequestContext.java
@@ -25,7 +25,7 @@
import com.couchbase.client.core.cnc.metrics.NoopMeter;
import com.couchbase.client.core.env.Authenticator;
import com.couchbase.client.core.env.CoreEnvironment;
-import com.couchbase.client.core.node.NodeIdentifier;
+import com.couchbase.client.core.topology.NodeIdentifier;
import com.couchbase.client.core.retry.RetryReason;
import com.couchbase.client.core.util.HostAndPort;
import reactor.util.annotation.Nullable;
@@ -404,7 +404,7 @@ public void injectExportableParams(final Map input) {
if (lastDispatchedTo != null) {
input.put("lastDispatchedTo", redactSystem(lastDispatchedTo));
} else if (lastDispatchedToNode != null) {
- input.put("lastDispatchedTo", redactSystem(lastDispatchedToNode.address()));
+ input.put("lastDispatchedTo", redactSystem(lastDispatchedToNode));
}
if (lastDispatchedFrom != null) {
diff --git a/core-io/src/main/java/com/couchbase/client/core/msg/RequestTarget.java b/core-io/src/main/java/com/couchbase/client/core/msg/RequestTarget.java
index 055f035f1..f556d107a 100644
--- a/core-io/src/main/java/com/couchbase/client/core/msg/RequestTarget.java
+++ b/core-io/src/main/java/com/couchbase/client/core/msg/RequestTarget.java
@@ -17,8 +17,8 @@
package com.couchbase.client.core.msg;
import com.couchbase.client.core.annotation.Stability;
-import com.couchbase.client.core.node.NodeIdentifier;
import com.couchbase.client.core.service.ServiceType;
+import com.couchbase.client.core.topology.NodeIdentifier;
import java.util.Objects;
@@ -97,7 +97,7 @@ public NodeIdentifier nodeIdentifier() {
@Override
public String toString() {
- String base = serviceType + "@" + redactSystem(nodeIdentifier.address() + ":" + nodeIdentifier.managerPort());
+ String base = serviceType + "@" + redactSystem(nodeIdentifier);
return bucketName == null ? base : base + "/" + redactMeta(bucketName);
}
diff --git a/core-io/src/main/java/com/couchbase/client/core/msg/kv/CarrierBucketConfigRequest.java b/core-io/src/main/java/com/couchbase/client/core/msg/kv/CarrierBucketConfigRequest.java
index b1f5b8207..05b9b1dc7 100644
--- a/core-io/src/main/java/com/couchbase/client/core/msg/kv/CarrierBucketConfigRequest.java
+++ b/core-io/src/main/java/com/couchbase/client/core/msg/kv/CarrierBucketConfigRequest.java
@@ -24,8 +24,8 @@
import com.couchbase.client.core.io.netty.kv.KeyValueChannelContext;
import com.couchbase.client.core.msg.TargetedRequest;
import com.couchbase.client.core.msg.UnmonitoredRequest;
-import com.couchbase.client.core.node.NodeIdentifier;
import com.couchbase.client.core.retry.RetryStrategy;
+import com.couchbase.client.core.topology.NodeIdentifier;
import reactor.util.annotation.Nullable;
import java.time.Duration;
@@ -87,7 +87,7 @@ public boolean idempotent() {
public Map serviceContext() {
final Map ctx = super.serviceContext();
if (target != null) {
- ctx.put("target", redactSystem(target.address()));
+ ctx.put("target", redactSystem(target));
}
return ctx;
}
@@ -100,7 +100,7 @@ public String name() {
@Override
public String toString() {
return "CarrierBucketConfigRequest{" +
- "target=" + redactSystem(target.address()) +
+ "target=" + redactSystem(target) +
", bucket=" + redactMeta(collectionIdentifier().bucket()) +
", ifNewerThan=" + ifNewerThan +
'}';
diff --git a/core-io/src/main/java/com/couchbase/client/core/msg/kv/CarrierGlobalConfigRequest.java b/core-io/src/main/java/com/couchbase/client/core/msg/kv/CarrierGlobalConfigRequest.java
index 909dbea79..d7abcafb7 100644
--- a/core-io/src/main/java/com/couchbase/client/core/msg/kv/CarrierGlobalConfigRequest.java
+++ b/core-io/src/main/java/com/couchbase/client/core/msg/kv/CarrierGlobalConfigRequest.java
@@ -25,7 +25,7 @@
import com.couchbase.client.core.io.netty.kv.MemcacheProtocol;
import com.couchbase.client.core.msg.TargetedRequest;
import com.couchbase.client.core.msg.UnmonitoredRequest;
-import com.couchbase.client.core.node.NodeIdentifier;
+import com.couchbase.client.core.topology.NodeIdentifier;
import com.couchbase.client.core.retry.RetryStrategy;
import reactor.util.annotation.Nullable;
@@ -130,7 +130,7 @@ public boolean idempotent() {
public Map serviceContext() {
final Map ctx = super.serviceContext();
if (target != null) {
- ctx.put("target", redactSystem(target.address()));
+ ctx.put("target", redactSystem(target));
}
return ctx;
}
@@ -143,7 +143,7 @@ public String name() {
@Override
public String toString() {
return "CarrierGlobalConfigRequest{" +
- "target=" + redactSystem(target.address()) +
+ "target=" + redactSystem(target) +
", ifNewerThan=" + ifNewerThan +
'}';
}
diff --git a/core-io/src/main/java/com/couchbase/client/core/msg/kv/KeyValueRequest.java b/core-io/src/main/java/com/couchbase/client/core/msg/kv/KeyValueRequest.java
index 7fa5f7e68..1396b0c88 100644
--- a/core-io/src/main/java/com/couchbase/client/core/msg/kv/KeyValueRequest.java
+++ b/core-io/src/main/java/com/couchbase/client/core/msg/kv/KeyValueRequest.java
@@ -45,6 +45,19 @@ public interface KeyValueRequest extends Request, ScopedR
*/
void partition(short partition);
+ /**
+ * Returns the index of the replica set member this request targets.
+ *
+ * - 0 = primary ("active")
+ *
- 1 = first replica
+ *
- 2 = second replica
+ *
- 3 = third replica
+ *
+ */
+ default int replica() {
+ return 0;
+ }
+
/**
* Encode this request with the given allocator and opaque.
*
diff --git a/core-io/src/main/java/com/couchbase/client/core/msg/kv/KvPingRequest.java b/core-io/src/main/java/com/couchbase/client/core/msg/kv/KvPingRequest.java
index 364bd06cd..5f3159c55 100644
--- a/core-io/src/main/java/com/couchbase/client/core/msg/kv/KvPingRequest.java
+++ b/core-io/src/main/java/com/couchbase/client/core/msg/kv/KvPingRequest.java
@@ -22,14 +22,12 @@
import com.couchbase.client.core.io.netty.kv.KeyValueChannelContext;
import com.couchbase.client.core.io.netty.kv.MemcacheProtocol;
import com.couchbase.client.core.msg.TargetedRequest;
-import com.couchbase.client.core.node.NodeIdentifier;
import com.couchbase.client.core.retry.RetryStrategy;
+import com.couchbase.client.core.topology.NodeIdentifier;
import java.time.Duration;
import java.util.Map;
-import java.util.TreeMap;
-import static com.couchbase.client.core.logging.RedactableArgument.redactMeta;
import static com.couchbase.client.core.logging.RedactableArgument.redactSystem;
public class KvPingRequest extends NoopRequest implements TargetedRequest {
@@ -56,7 +54,7 @@ public NodeIdentifier target() {
public Map serviceContext() {
final Map ctx = super.serviceContext();
if (target != null) {
- ctx.put("target", redactSystem(target.address()));
+ ctx.put("target", redactSystem(target));
}
return ctx;
}
diff --git a/core-io/src/main/java/com/couchbase/client/core/msg/kv/MultiObserveViaCasRequest.java b/core-io/src/main/java/com/couchbase/client/core/msg/kv/MultiObserveViaCasRequest.java
index dea847b94..7a2bd5e6a 100644
--- a/core-io/src/main/java/com/couchbase/client/core/msg/kv/MultiObserveViaCasRequest.java
+++ b/core-io/src/main/java/com/couchbase/client/core/msg/kv/MultiObserveViaCasRequest.java
@@ -25,8 +25,8 @@
import com.couchbase.client.core.io.netty.kv.MemcacheProtocol;
import com.couchbase.client.core.msg.ResponseStatus;
import com.couchbase.client.core.msg.TargetedRequest;
-import com.couchbase.client.core.node.NodeIdentifier;
import com.couchbase.client.core.retry.RetryStrategy;
+import com.couchbase.client.core.topology.NodeIdentifier;
import com.couchbase.client.core.util.UnsignedLEB128;
import java.time.Duration;
@@ -136,7 +136,7 @@ public String name() {
@Override
public Map serviceContext() {
final Map parentCtx = super.serviceContext();
- parentCtx.put("target", target.address());
+ parentCtx.put("target", target);
parentCtx.put("numKeys", keys.size());
return parentCtx;
}
diff --git a/core-io/src/main/java/com/couchbase/client/core/msg/kv/ObserveViaCasRequest.java b/core-io/src/main/java/com/couchbase/client/core/msg/kv/ObserveViaCasRequest.java
index 6d5489d6a..f7af2775a 100644
--- a/core-io/src/main/java/com/couchbase/client/core/msg/kv/ObserveViaCasRequest.java
+++ b/core-io/src/main/java/com/couchbase/client/core/msg/kv/ObserveViaCasRequest.java
@@ -44,6 +44,7 @@ public ObserveViaCasRequest(final Duration timeout, final CoreContext ctx, Colle
this.replica = replica;
}
+ @Override
public int replica() {
return replica;
}
diff --git a/core-io/src/main/java/com/couchbase/client/core/msg/kv/ObserveViaSeqnoRequest.java b/core-io/src/main/java/com/couchbase/client/core/msg/kv/ObserveViaSeqnoRequest.java
index a89ec6e32..c2bde2f08 100644
--- a/core-io/src/main/java/com/couchbase/client/core/msg/kv/ObserveViaSeqnoRequest.java
+++ b/core-io/src/main/java/com/couchbase/client/core/msg/kv/ObserveViaSeqnoRequest.java
@@ -52,6 +52,7 @@ public ObserveViaSeqnoRequest(final Duration timeout, final CoreContext ctx, Col
}
}
+ @Override
public int replica() {
return replica;
}
diff --git a/core-io/src/main/java/com/couchbase/client/core/msg/kv/RangeScanCancelRequest.java b/core-io/src/main/java/com/couchbase/client/core/msg/kv/RangeScanCancelRequest.java
index 77f4a8d6b..f996851b6 100644
--- a/core-io/src/main/java/com/couchbase/client/core/msg/kv/RangeScanCancelRequest.java
+++ b/core-io/src/main/java/com/couchbase/client/core/msg/kv/RangeScanCancelRequest.java
@@ -54,7 +54,7 @@ public RangeScanCancelRequest(CoreRangeScanId id,
options.commonOptions().retryStrategy().orElse(ctx.environment().retryStrategy()),
null,
collectionIdentifier,
- ctx.environment().requestTracer().requestSpan(TracingIdentifiers.SPAN_REQUEST_KV_RANGE_SCAN_CANCEL, options.commonOptions().parentSpan().orElse(null)));
+ ctx.coreResources().requestTracer().requestSpan(TracingIdentifiers.SPAN_REQUEST_KV_RANGE_SCAN_CANCEL, options.commonOptions().parentSpan().orElse(null)));
this.id = id;
}
diff --git a/core-io/src/main/java/com/couchbase/client/core/msg/kv/RangeScanContinueRequest.java b/core-io/src/main/java/com/couchbase/client/core/msg/kv/RangeScanContinueRequest.java
index da45c1c74..4fbd90fb7 100644
--- a/core-io/src/main/java/com/couchbase/client/core/msg/kv/RangeScanContinueRequest.java
+++ b/core-io/src/main/java/com/couchbase/client/core/msg/kv/RangeScanContinueRequest.java
@@ -62,7 +62,7 @@ public RangeScanContinueRequest(CoreRangeScanId id,
options.commonOptions().retryStrategy().orElse(ctx.environment().retryStrategy()),
key,
collectionIdentifier,
- ctx.environment().requestTracer().requestSpan(TracingIdentifiers.SPAN_REQUEST_KV_RANGE_SCAN_CONTINUE, options.commonOptions().parentSpan().orElse(null)));
+ ctx.coreResources().requestTracer().requestSpan(TracingIdentifiers.SPAN_REQUEST_KV_RANGE_SCAN_CONTINUE, options.commonOptions().parentSpan().orElse(null)));
this.id = id;
this.itemLimit = options.batchItemLimit();
this.byteLimit = options.batchByteLimit();
diff --git a/core-io/src/main/java/com/couchbase/client/core/msg/kv/RangeScanCreateRequest.java b/core-io/src/main/java/com/couchbase/client/core/msg/kv/RangeScanCreateRequest.java
index ba924f97b..5c46ec40a 100644
--- a/core-io/src/main/java/com/couchbase/client/core/msg/kv/RangeScanCreateRequest.java
+++ b/core-io/src/main/java/com/couchbase/client/core/msg/kv/RangeScanCreateRequest.java
@@ -88,7 +88,7 @@ public static RangeScanCreateRequest forRangeScan(byte[] startTerm,
ctx,
options.commonOptions().retryStrategy().orElse(ctx.environment().retryStrategy()),
collectionIdentifier,
- ctx.environment().requestTracer().requestSpan(TracingIdentifiers.SPAN_REQUEST_KV_RANGE_SCAN_CREATE, options.commonOptions().parentSpan().orElse(null)),
+ ctx.coreResources().requestTracer().requestSpan(TracingIdentifiers.SPAN_REQUEST_KV_RANGE_SCAN_CREATE, options.commonOptions().parentSpan().orElse(null)),
partition,
Optional.ofNullable(consistencyMap.get(partition)));
}
@@ -111,7 +111,7 @@ public static RangeScanCreateRequest forSamplingScan(CoreSamplingScan samplingSc
ctx,
options.commonOptions().retryStrategy().orElse(ctx.environment().retryStrategy()),
collectionIdentifier,
- ctx.environment().requestTracer().requestSpan(TracingIdentifiers.SPAN_REQUEST_KV_RANGE_SCAN_CREATE, options.commonOptions().parentSpan().orElse(null)),
+ ctx.coreResources().requestTracer().requestSpan(TracingIdentifiers.SPAN_REQUEST_KV_RANGE_SCAN_CREATE, options.commonOptions().parentSpan().orElse(null)),
partition,
Optional.ofNullable(consistencyMap.get(partition)));
}
diff --git a/core-io/src/main/java/com/couchbase/client/core/msg/kv/ReplicaGetRequest.java b/core-io/src/main/java/com/couchbase/client/core/msg/kv/ReplicaGetRequest.java
index 47766cf6c..c681af08b 100644
--- a/core-io/src/main/java/com/couchbase/client/core/msg/kv/ReplicaGetRequest.java
+++ b/core-io/src/main/java/com/couchbase/client/core/msg/kv/ReplicaGetRequest.java
@@ -50,7 +50,8 @@ public ReplicaGetRequest(final String key, final Duration timeout,
}
}
- public short replica() {
+ @Override
+ public int replica() {
return replica;
}
diff --git a/core-io/src/main/java/com/couchbase/client/core/msg/kv/ReplicaSubdocGetRequest.java b/core-io/src/main/java/com/couchbase/client/core/msg/kv/ReplicaSubdocGetRequest.java
index 1b1082bef..3c719cabb 100644
--- a/core-io/src/main/java/com/couchbase/client/core/msg/kv/ReplicaSubdocGetRequest.java
+++ b/core-io/src/main/java/com/couchbase/client/core/msg/kv/ReplicaSubdocGetRequest.java
@@ -50,7 +50,8 @@ public static ReplicaSubdocGetRequest create(final Duration timeout, final CoreC
}
}
- public short replica() {
+ @Override
+ public int replica() {
return replica;
}
diff --git a/core-io/src/main/java/com/couchbase/client/core/msg/kv/SubdocGetRequest.java b/core-io/src/main/java/com/couchbase/client/core/msg/kv/SubdocGetRequest.java
index 9a4379068..64f5bc370 100644
--- a/core-io/src/main/java/com/couchbase/client/core/msg/kv/SubdocGetRequest.java
+++ b/core-io/src/main/java/com/couchbase/client/core/msg/kv/SubdocGetRequest.java
@@ -17,6 +17,7 @@
package com.couchbase.client.core.msg.kv;
import com.couchbase.client.core.CoreContext;
+import com.couchbase.client.core.annotation.Stability;
import com.couchbase.client.core.api.kv.CoreSubdocGetCommand;
import com.couchbase.client.core.cnc.RequestSpan;
import com.couchbase.client.core.cnc.TracingIdentifiers;
@@ -102,6 +103,20 @@ static List convertCommands(List commands) {
return result;
}
+ @Stability.Internal
+ public static List convertCommandsToCore(List commands) {
+ List result = new ArrayList<>(commands.size());
+ for (Command cmd : commands) {
+ result.add(new CoreSubdocGetCommand(
+ cmd.type,
+ cmd.path,
+ cmd.xattr,
+ cmd.binary
+ ));
+ }
+ return result;
+ }
+
@Override
public ByteBuf encode(ByteBufAllocator alloc, int opaque, KeyValueChannelContext ctx) {
ByteBuf key = null;
diff --git a/core-io/src/main/java/com/couchbase/client/core/msg/kv/SubdocGetResponse.java b/core-io/src/main/java/com/couchbase/client/core/msg/kv/SubdocGetResponse.java
index f8a64642f..ed36efaca 100644
--- a/core-io/src/main/java/com/couchbase/client/core/msg/kv/SubdocGetResponse.java
+++ b/core-io/src/main/java/com/couchbase/client/core/msg/kv/SubdocGetResponse.java
@@ -16,6 +16,10 @@
package com.couchbase.client.core.msg.kv;
+import com.couchbase.client.core.CoreKeyspace;
+import com.couchbase.client.core.annotation.Stability;
+import com.couchbase.client.core.api.kv.CoreKvResponseMetadata;
+import com.couchbase.client.core.api.kv.CoreSubdocGetResult;
import com.couchbase.client.core.error.CouchbaseException;
import com.couchbase.client.core.io.netty.kv.MemcacheProtocol;
import com.couchbase.client.core.msg.ResponseStatus;
@@ -70,4 +74,15 @@ public String toString() {
", isDeleted=" + isDeleted +
'}';
}
+
+ @Stability.Internal
+ public CoreSubdocGetResult toCore(CoreKeyspace keyspace, String key) {
+ return new CoreSubdocGetResult(keyspace,
+ key,
+ CoreKvResponseMetadata.from(flexibleExtras()),
+ Arrays.asList(values()),
+ cas(),
+ isDeleted()
+ );
+ }
}
diff --git a/core-io/src/main/java/com/couchbase/client/core/msg/manager/BucketConfigRequest.java b/core-io/src/main/java/com/couchbase/client/core/msg/manager/BucketConfigRequest.java
index d4569bff4..27e2b221e 100644
--- a/core-io/src/main/java/com/couchbase/client/core/msg/manager/BucketConfigRequest.java
+++ b/core-io/src/main/java/com/couchbase/client/core/msg/manager/BucketConfigRequest.java
@@ -25,8 +25,8 @@
import com.couchbase.client.core.endpoint.http.CoreHttpPath;
import com.couchbase.client.core.env.Authenticator;
import com.couchbase.client.core.msg.TargetedRequest;
-import com.couchbase.client.core.node.NodeIdentifier;
import com.couchbase.client.core.retry.RetryStrategy;
+import com.couchbase.client.core.topology.NodeIdentifier;
import java.time.Duration;
import java.util.Map;
@@ -84,7 +84,7 @@ public Map serviceContext() {
ctx.put("type", serviceType().ident());
ctx.put("bucket", redactMeta(bucketName));
if (target != null) {
- ctx.put("target", redactSystem(target.address()));
+ ctx.put("target", redactSystem(target));
}
return ctx;
}
diff --git a/core-io/src/main/java/com/couchbase/client/core/msg/query/QueryRequest.java b/core-io/src/main/java/com/couchbase/client/core/msg/query/QueryRequest.java
index 846508704..dbee82f3a 100644
--- a/core-io/src/main/java/com/couchbase/client/core/msg/query/QueryRequest.java
+++ b/core-io/src/main/java/com/couchbase/client/core/msg/query/QueryRequest.java
@@ -36,9 +36,9 @@
import com.couchbase.client.core.msg.BaseRequest;
import com.couchbase.client.core.msg.HttpRequest;
import com.couchbase.client.core.msg.ResponseStatus;
-import com.couchbase.client.core.node.NodeIdentifier;
import com.couchbase.client.core.retry.RetryStrategy;
import com.couchbase.client.core.service.ServiceType;
+import com.couchbase.client.core.topology.NodeIdentifier;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
diff --git a/core-io/src/main/java/com/couchbase/client/core/node/KeyValueLocator.java b/core-io/src/main/java/com/couchbase/client/core/node/KeyValueLocator.java
index 8ed561cd8..a48b864e9 100644
--- a/core-io/src/main/java/com/couchbase/client/core/node/KeyValueLocator.java
+++ b/core-io/src/main/java/com/couchbase/client/core/node/KeyValueLocator.java
@@ -30,14 +30,12 @@
import com.couchbase.client.core.msg.Response;
import com.couchbase.client.core.msg.kv.DurabilityLevel;
import com.couchbase.client.core.msg.kv.KeyValueRequest;
-import com.couchbase.client.core.msg.kv.ObserveViaSeqnoRequest;
import com.couchbase.client.core.msg.kv.PredeterminedPartitionRequest;
-import com.couchbase.client.core.msg.kv.ReplicaGetRequest;
-import com.couchbase.client.core.msg.kv.ReplicaSubdocGetRequest;
import com.couchbase.client.core.msg.kv.SyncDurabilityRequest;
import com.couchbase.client.core.retry.AuthErrorDecider;
import com.couchbase.client.core.retry.RetryOrchestrator;
import com.couchbase.client.core.retry.RetryReason;
+import com.couchbase.client.core.topology.NodeIdentifier;
import java.util.List;
import java.util.Optional;
@@ -155,7 +153,7 @@ private static void couchbaseBucket(final KeyValueRequest> request, final List
NodeInfo nodeInfo = config.nodeAtIndex(nodeId);
for (Node node : nodes) {
- if (node.identifier().equals(nodeInfo.identifier())) {
+ if (node.identifier().equals(nodeInfo.id())) {
node.send(request);
return;
}
@@ -204,15 +202,10 @@ private static int calculateNodeId(int partitionId, final KeyValueRequest> req
// having the request being stuck on the server side during rebalance.
boolean useFastForward = config.hasFastForwardMap() && request.rejectedWithNotMyVbucket() > 0;
- if (request instanceof ReplicaGetRequest) {
- return config.nodeIndexForReplica(partitionId, ((ReplicaGetRequest) request).replica() - 1, useFastForward);
- } else if (request instanceof ReplicaSubdocGetRequest) {
- return config.nodeIndexForReplica(partitionId, ((ReplicaSubdocGetRequest) request).replica() - 1, useFastForward);
- } else if (request instanceof ObserveViaSeqnoRequest && ((ObserveViaSeqnoRequest) request).replica() > 0) {
- return config.nodeIndexForReplica(partitionId, ((ObserveViaSeqnoRequest) request).replica() - 1, useFastForward);
- } else {
- return config.nodeIndexForActive(partitionId, useFastForward);
- }
+ int replica = request.replica();
+ return replica == 0
+ ? config.nodeIndexForActive(partitionId, useFastForward)
+ : config.nodeIndexForReplica(partitionId, replica - 1, useFastForward);
}
@@ -229,7 +222,7 @@ private static void memcacheBucket(final KeyValueRequest> request, final List<
return;
}
- NodeIdentifier identifier = config.nodeForKey(request.key()).identifier();
+ NodeIdentifier identifier = config.nodeForKey(request.key()).id();
request.partition((short) 0);
for (Node node : nodes) {
diff --git a/core-io/src/main/java/com/couchbase/client/core/node/Node.java b/core-io/src/main/java/com/couchbase/client/core/node/Node.java
index 0bcb95aa8..08e020123 100644
--- a/core-io/src/main/java/com/couchbase/client/core/node/Node.java
+++ b/core-io/src/main/java/com/couchbase/client/core/node/Node.java
@@ -54,10 +54,11 @@
import com.couchbase.client.core.service.ServiceType;
import com.couchbase.client.core.service.ViewService;
import com.couchbase.client.core.service.ViewServiceConfig;
+import com.couchbase.client.core.topology.NodeIdentifier;
+import com.couchbase.client.core.util.AtomicEnumSet;
import com.couchbase.client.core.util.CompositeStateful;
import com.couchbase.client.core.util.HostAndPort;
import com.couchbase.client.core.util.NanoTimestamp;
-import com.couchbase.client.core.util.AtomicEnumSet;
import com.couchbase.client.core.util.Stateful;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
diff --git a/core-io/src/main/java/com/couchbase/client/core/node/NodeContext.java b/core-io/src/main/java/com/couchbase/client/core/node/NodeContext.java
index 4647347c2..f459b97f2 100644
--- a/core-io/src/main/java/com/couchbase/client/core/node/NodeContext.java
+++ b/core-io/src/main/java/com/couchbase/client/core/node/NodeContext.java
@@ -17,6 +17,7 @@
package com.couchbase.client.core.node;
import com.couchbase.client.core.CoreContext;
+import com.couchbase.client.core.topology.NodeIdentifier;
import java.util.Map;
@@ -24,25 +25,27 @@
public class NodeContext extends CoreContext {
- /**
- * The hostname of this node.
- */
- private final NodeIdentifier nodeIdentifier;
+ private final com.couchbase.client.core.node.NodeIdentifier legacyNodeIdentifier;
public NodeContext(CoreContext ctx, NodeIdentifier nodeIdentifier) {
super(ctx.core(), ctx.id(), ctx.environment(), ctx.authenticator());
- this.nodeIdentifier = nodeIdentifier;
+ this.legacyNodeIdentifier = nodeIdentifier.toLegacy();
}
+ /**
+ * @deprecated This is the node's canonical hostname; it's not useful by itself,
+ * since it does not uniquely identify a node.
+ */
+ @Deprecated
public String remoteHostname() {
- return nodeIdentifier.address();
+ return legacyNodeIdentifier.address();
}
@Override
public void injectExportableParams(final Map input) {
super.injectExportableParams(input);
input.put("remote", redactSystem(remoteHostname()));
- input.put("managerPort", redactSystem(nodeIdentifier.managerPort()));
+ input.put("managerPort", redactSystem(legacyNodeIdentifier.managerPort()));
}
diff --git a/core-io/src/main/java/com/couchbase/client/core/node/NodeIdentifier.java b/core-io/src/main/java/com/couchbase/client/core/node/NodeIdentifier.java
index 9e7d4486d..3ba18345d 100644
--- a/core-io/src/main/java/com/couchbase/client/core/node/NodeIdentifier.java
+++ b/core-io/src/main/java/com/couchbase/client/core/node/NodeIdentifier.java
@@ -28,14 +28,17 @@
/**
* Uniquely identifies a node within the cluster, using the node's
* host and manager port from the default network.
+ *
+ * @deprecated In favor of {@link com.couchbase.client.core.topology.NodeIdentifier}
*/
+@Deprecated
public class NodeIdentifier {
private final String canonicalHost;
private final int canonicalManagerPort;
- // Nullable only when created by a
- @Nullable private final String hostForNetworkConnections;
+ // Null only when created by a legacy global/bucket config parser
+ @Nullable private final com.couchbase.client.core.topology.NodeIdentifier topologyNodeIdentifier;
@Deprecated
public NodeIdentifier(
@@ -44,15 +47,7 @@ public NodeIdentifier(
) {
this.canonicalHost = canonicalHost;
this.canonicalManagerPort = canonicalManagerPort;
- this.hostForNetworkConnections = null;
- }
-
- @Stability.Internal
- public static NodeIdentifier forBootstrap(String bootstrapHost, int bootstrapPort) {
- // This address isn't really "canonical", since it may be an "external" address.
- // If it's an external address, the node created from this identifier will be discarded
- // when the config with the _real_ canonical addresses is applied.
- return new NodeIdentifier(new HostAndPort(bootstrapHost, bootstrapPort), bootstrapHost);
+ this.topologyNodeIdentifier = null;
}
public NodeIdentifier(
@@ -61,7 +56,7 @@ public NodeIdentifier(
) {
this.canonicalHost = canonicalAddress.host();
this.canonicalManagerPort = canonicalAddress.port();
- this.hostForNetworkConnections = requireNonNull(hostForNetworkConnections);
+ this.topologyNodeIdentifier = new com.couchbase.client.core.topology.NodeIdentifier(canonicalHost, canonicalManagerPort, hostForNetworkConnections);
}
/**
@@ -72,13 +67,17 @@ public NodeIdentifier(
* @throws NoSuchElementException if this info is not available
*/
public String hostForNetworkConnections() throws NoSuchElementException {
- if (hostForNetworkConnections == null) {
- throw new NoSuchElementException(
- "This NodeIdentifier (" + this + ") doesn't have the host to use for network connections." +
- " It might have been created by a legacy config parser or some other component that did not specify it."
+ return asTopologyNodeIdentifier().hostForNetworkConnections();
+ }
+
+ @Stability.Internal
+ public com.couchbase.client.core.topology.NodeIdentifier asTopologyNodeIdentifier() {
+ if (topologyNodeIdentifier == null) {
+ throw new NoSuchElementException("This NodeIdentifier (" + this + ") doesn't have the host to use for network connections." +
+ " It might have been created by a legacy config parser or some other component that did not specify it."
);
}
- return hostForNetworkConnections;
+ return topologyNodeIdentifier;
}
/**
@@ -96,7 +95,7 @@ public int managerPort() {
public String toString() {
return "NodeIdentifier{" +
"canonicalAddress=" + redactSystem(canonicalHost + ":" + canonicalManagerPort) +
- ", hostForNetworkConnections=" + hostForNetworkConnections +
+ ", hostForNetworkConnections=" + (topologyNodeIdentifier == null ? null : hostForNetworkConnections()) +
"}";
}
diff --git a/core-io/src/main/java/com/couchbase/client/core/node/RoundRobinLocator.java b/core-io/src/main/java/com/couchbase/client/core/node/RoundRobinLocator.java
index 7994a1696..733c273d3 100644
--- a/core-io/src/main/java/com/couchbase/client/core/node/RoundRobinLocator.java
+++ b/core-io/src/main/java/com/couchbase/client/core/node/RoundRobinLocator.java
@@ -18,9 +18,7 @@
import com.couchbase.client.core.CoreContext;
import com.couchbase.client.core.cnc.events.node.NodeLocatorBugIdentifiedEvent;
-import com.couchbase.client.core.config.BucketConfig;
import com.couchbase.client.core.config.ClusterConfig;
-import com.couchbase.client.core.config.NodeInfo;
import com.couchbase.client.core.config.PortInfo;
import com.couchbase.client.core.error.FeatureNotAvailableException;
import com.couchbase.client.core.error.ServiceNotAvailableException;
@@ -32,6 +30,7 @@
import com.couchbase.client.core.retry.RetryOrchestrator;
import com.couchbase.client.core.retry.RetryReason;
import com.couchbase.client.core.service.ServiceType;
+import com.couchbase.client.core.topology.NodeIdentifier;
import java.util.ArrayList;
import java.util.List;
@@ -140,21 +139,14 @@ public void dispatch(final Request extends Response> request, final List
private boolean serviceShowsUpInConfig(final ClusterConfig clusterConfig) {
if (clusterConfig.globalConfig() != null) {
for (PortInfo portInfo : clusterConfig.globalConfig().portInfos()) {
- if (portInfo.ports().containsKey(serviceType)) {
+ if (portInfo.ports().containsKey(serviceType) || portInfo.sslPorts().containsKey(serviceType)) {
return true;
}
}
}
- for (BucketConfig bucketConfig : clusterConfig.bucketConfigs().values()) {
- for (NodeInfo nodeInfo : bucketConfig.nodes()) {
- if (nodeInfo.services().containsKey(serviceType)) {
- return true;
- }
- }
- }
-
- return false;
+ return clusterConfig.bucketConfigs().values().stream()
+ .anyMatch(it -> it.serviceEnabled(serviceType));
}
/**
diff --git a/core-io/src/main/java/com/couchbase/client/core/node/ViewLocator.java b/core-io/src/main/java/com/couchbase/client/core/node/ViewLocator.java
index caf0dee29..ceabdb914 100644
--- a/core-io/src/main/java/com/couchbase/client/core/node/ViewLocator.java
+++ b/core-io/src/main/java/com/couchbase/client/core/node/ViewLocator.java
@@ -68,7 +68,7 @@ protected boolean nodeCanBeUsed(final Node node, final Request extends Respons
BucketConfig bucketConfig = config.bucketConfig(bucket);
if (bucketConfig instanceof CouchbaseBucketConfig) {
return ((CouchbaseBucketConfig) bucketConfig)
- .hasPrimaryPartitionsOnNode(node.identifier());
+ .hasPrimaryPartitionsOnNode(node.identifier().toLegacy());
}
}
return false;
diff --git a/core-io/src/main/java/com/couchbase/client/core/protostellar/CoreProtostellarAccessors.java b/core-io/src/main/java/com/couchbase/client/core/protostellar/CoreProtostellarAccessors.java
index 5db822704..f69b47394 100644
--- a/core-io/src/main/java/com/couchbase/client/core/protostellar/CoreProtostellarAccessors.java
+++ b/core-io/src/main/java/com/couchbase/client/core/protostellar/CoreProtostellarAccessors.java
@@ -78,7 +78,7 @@ TSdkResult blocking(CoreProtostellar core,
long start = System.nanoTime();
RequestSpan dispatchSpan = createDispatchSpan(core, request, endpoint);
- AutoCloseable scope = activateSpan(Optional.empty(), dispatchSpan, core.context().environment().requestTracer());
+ AutoCloseable scope = activateSpan(Optional.empty(), dispatchSpan, core.context().coreResources().requestTracer());
try {
// Make the Protostellar call.
@@ -146,7 +146,7 @@ void asyncInternal(CompletableFuture ret,
RequestSpan dispatchSpan = createDispatchSpan(core, request, endpoint);
long start = System.nanoTime();
- AutoCloseable scope = activateSpan(Optional.empty(), dispatchSpan, core.context().environment().requestTracer());
+ AutoCloseable scope = activateSpan(Optional.empty(), dispatchSpan, core.context().coreResources().requestTracer());
// Make the Protostellar call.
ListenableFuture response = executeFutureGrpcCall.apply(endpoint);
@@ -244,7 +244,7 @@ void reactiveInternal(Sinks.One ret,
RequestSpan dispatchSpan = createDispatchSpan(core, request, endpoint);
long start = System.nanoTime();
- AutoCloseable scope = activateSpan(Optional.empty(), dispatchSpan, core.context().environment().requestTracer());
+ AutoCloseable scope = activateSpan(Optional.empty(), dispatchSpan, core.context().coreResources().requestTracer());
// Make the Protostellar call.
ListenableFuture response = executeFutureGrpcCall.apply(endpoint);
@@ -317,7 +317,7 @@ private static void handleDispatchSpan(@Nullable ProtostellarRequestBehaviour be
private static @Nullable RequestSpan createDispatchSpan(CoreProtostellar core,
ProtostellarRequest request,
ProtostellarEndpoint endpoint) {
- RequestTracer tracer = core.context().environment().requestTracer();
+ RequestTracer tracer = core.context().coreResources().requestTracer();
RequestSpan dispatchSpan;
if (!CbTracing.isInternalTracer(tracer)) {
dispatchSpan = tracer.requestSpan(TracingIdentifiers.SPAN_DISPATCH, request.span());
diff --git a/core-io/src/main/java/com/couchbase/client/core/protostellar/CoreProtostellarUtil.java b/core-io/src/main/java/com/couchbase/client/core/protostellar/CoreProtostellarUtil.java
index 65073f78f..2a3ace949 100644
--- a/core-io/src/main/java/com/couchbase/client/core/protostellar/CoreProtostellarUtil.java
+++ b/core-io/src/main/java/com/couchbase/client/core/protostellar/CoreProtostellarUtil.java
@@ -186,7 +186,7 @@ public static RequestSpan createSpan(CoreProtostellar core,
String spanName,
CoreDurability durability,
@Nullable RequestSpan parent) {
- RequestSpan span = CbTracing.newSpan(core.context().environment().requestTracer(), spanName, parent);
+ RequestSpan span = core.context().coreResources().requestTracer().requestSpan(spanName, parent);
if (!durability.isNone() && !durability.isLegacy()) {
switch (durability.levelIfSynchronous().get()) {
diff --git a/core-io/src/main/java/com/couchbase/client/core/protostellar/ProtostellarContext.java b/core-io/src/main/java/com/couchbase/client/core/protostellar/ProtostellarContext.java
index 3a9f430b5..3c60c178a 100644
--- a/core-io/src/main/java/com/couchbase/client/core/protostellar/ProtostellarContext.java
+++ b/core-io/src/main/java/com/couchbase/client/core/protostellar/ProtostellarContext.java
@@ -16,6 +16,7 @@
package com.couchbase.client.core.protostellar;
+import com.couchbase.client.core.CoreResources;
import com.couchbase.client.core.annotation.Stability;
import com.couchbase.client.core.cnc.AbstractContext;
import com.couchbase.client.core.env.Authenticator;
@@ -34,10 +35,12 @@ public final class ProtostellarContext extends AbstractContext {
private final CoreEnvironment env;
private final Authenticator authenticator;
+ private final CoreResources coreResources;
- public ProtostellarContext(final CoreEnvironment env, final Authenticator authenticator) {
+ public ProtostellarContext(final CoreEnvironment env, final Authenticator authenticator, final CoreResources coreResources) {
this.env = requireNonNull(env);
this.authenticator = requireNonNull(authenticator);
+ this.coreResources = requireNonNull(coreResources);
if (env.securityConfig().tlsEnabled() && !authenticator.supportsTls()) {
throw InvalidArgumentException.fromMessage("TLS enabled but the Authenticator does not support TLS!");
@@ -58,6 +61,10 @@ public CoreEnvironment environment() {
return env;
}
+ public CoreResources coreResources() {
+ return coreResources;
+ }
+
public Authenticator authenticator() {
return authenticator;
}
diff --git a/core-io/src/main/java/com/couchbase/client/core/protostellar/kv/CoreProtostellarKeyValueRequests.java b/core-io/src/main/java/com/couchbase/client/core/protostellar/kv/CoreProtostellarKeyValueRequests.java
index b15a60be4..6aae54a73 100644
--- a/core-io/src/main/java/com/couchbase/client/core/protostellar/kv/CoreProtostellarKeyValueRequests.java
+++ b/core-io/src/main/java/com/couchbase/client/core/protostellar/kv/CoreProtostellarKeyValueRequests.java
@@ -369,7 +369,7 @@ public static ProtostellarRequest upsertRequest(CoreProtostellar
}
private static ProtostellarCoreEncodedContent encodedContent(CoreProtostellar core, Supplier content, RequestSpan span, CompressionConfig compressionConfig) {
- RequestSpan encodeSpan = CbTracing.newSpan(core.context().environment().requestTracer(), TracingIdentifiers.SPAN_REQUEST_ENCODING, span);
+ RequestSpan encodeSpan = core.context().coreResources().requestTracer().requestSpan(TracingIdentifiers.SPAN_REQUEST_ENCODING, span);
long start = System.nanoTime();
CoreEncodedContent encoded;
boolean compressed = false;
diff --git a/core-io/src/main/java/com/couchbase/client/core/protostellar/kv/ProtostellarCoreKvOps.java b/core-io/src/main/java/com/couchbase/client/core/protostellar/kv/ProtostellarCoreKvOps.java
index 832bf2963..fce5d5ea5 100644
--- a/core-io/src/main/java/com/couchbase/client/core/protostellar/kv/ProtostellarCoreKvOps.java
+++ b/core-io/src/main/java/com/couchbase/client/core/protostellar/kv/ProtostellarCoreKvOps.java
@@ -32,6 +32,7 @@
import com.couchbase.client.core.api.kv.CoreSubdocGetResult;
import com.couchbase.client.core.api.kv.CoreSubdocMutateCommand;
import com.couchbase.client.core.api.kv.CoreSubdocMutateResult;
+import com.couchbase.client.core.api.kv.CoreReadPreference;
import com.couchbase.client.core.endpoint.http.CoreCommonOptions;
import com.couchbase.client.core.env.CompressionConfig;
import com.couchbase.client.core.error.FeatureNotAvailableException;
@@ -308,25 +309,25 @@ public Mono subdocGetReactive(CoreCommonOptions common, Str
@Override
- public Flux subdocGetAllReplicasReactive(CoreCommonOptions common, String key, List commands) {
+ public Flux subdocGetAllReplicasReactive(CoreCommonOptions common, String key, List commands, CoreReadPreference readPreference) {
// Protostellar subdoc-from-replica support is currently incomplete.
throw unsupported();
}
@Override
- public Mono subdocGetAnyReplicaReactive(CoreCommonOptions common, String key, List commands) {
+ public Mono subdocGetAnyReplicaReactive(CoreCommonOptions common, String key, List commands, CoreReadPreference readPreference) {
// Protostellar subdoc-from-replica support is currently incomplete.
throw unsupported();
}
@Override
- public Flux getAllReplicasReactive(CoreCommonOptions common, String key) {
+ public Flux getAllReplicasReactive(CoreCommonOptions common, String key, CoreReadPreference readPreference) {
// Protostellar get-from-replica support is currently incomplete. JVMCBC-1263.
throw unsupported();
}
@Override
- public Mono getAnyReplicaReactive(CoreCommonOptions common, String key) {
+ public Mono getAnyReplicaReactive(CoreCommonOptions common, String key, CoreReadPreference readPreference) {
// Protostellar get-from-replica support is currently incomplete. JVMCBC-1263.
throw unsupported();
}
diff --git a/core-io/src/main/java/com/couchbase/client/core/protostellar/query/ProtostellarCoreQueryOps.java b/core-io/src/main/java/com/couchbase/client/core/protostellar/query/ProtostellarCoreQueryOps.java
index 127b37ddf..d2072520d 100644
--- a/core-io/src/main/java/com/couchbase/client/core/protostellar/query/ProtostellarCoreQueryOps.java
+++ b/core-io/src/main/java/com/couchbase/client/core/protostellar/query/ProtostellarCoreQueryOps.java
@@ -39,11 +39,11 @@
import com.couchbase.client.core.logging.RedactableArgument;
import com.couchbase.client.core.msg.kv.MutationToken;
import com.couchbase.client.core.msg.query.QueryChunkRow;
-import com.couchbase.client.core.node.NodeIdentifier;
import com.couchbase.client.core.protostellar.CoreProtostellarAccessorsStreaming;
import com.couchbase.client.core.protostellar.CoreProtostellarErrorHandlingUtil;
import com.couchbase.client.core.protostellar.ProtostellarRequest;
import com.couchbase.client.core.service.ServiceType;
+import com.couchbase.client.core.topology.NodeIdentifier;
import com.couchbase.client.core.util.ProtostellarUtil;
import com.couchbase.client.protostellar.query.v1.QueryRequest;
import com.couchbase.client.protostellar.query.v1.QueryResponse;
diff --git a/core-io/src/main/java/com/couchbase/client/core/protostellar/query/ProtostellarCoreQueryResult.java b/core-io/src/main/java/com/couchbase/client/core/protostellar/query/ProtostellarCoreQueryResult.java
index 1ae97b549..d91388eeb 100644
--- a/core-io/src/main/java/com/couchbase/client/core/protostellar/query/ProtostellarCoreQueryResult.java
+++ b/core-io/src/main/java/com/couchbase/client/core/protostellar/query/ProtostellarCoreQueryResult.java
@@ -19,7 +19,7 @@
import com.couchbase.client.core.api.query.CoreQueryMetaData;
import com.couchbase.client.core.api.query.CoreQueryResult;
import com.couchbase.client.core.msg.query.QueryChunkRow;
-import com.couchbase.client.core.node.NodeIdentifier;
+import com.couchbase.client.core.topology.NodeIdentifier;
import com.couchbase.client.protostellar.query.v1.QueryResponse;
import java.util.List;
diff --git a/core-io/src/main/java/com/couchbase/client/core/protostellar/query/ProtostellarCoreReactiveQueryResult.java b/core-io/src/main/java/com/couchbase/client/core/protostellar/query/ProtostellarCoreReactiveQueryResult.java
index 47126cac2..6986b678d 100644
--- a/core-io/src/main/java/com/couchbase/client/core/protostellar/query/ProtostellarCoreReactiveQueryResult.java
+++ b/core-io/src/main/java/com/couchbase/client/core/protostellar/query/ProtostellarCoreReactiveQueryResult.java
@@ -19,7 +19,7 @@
import com.couchbase.client.core.api.query.CoreQueryMetaData;
import com.couchbase.client.core.api.query.CoreReactiveQueryResult;
import com.couchbase.client.core.msg.query.QueryChunkRow;
-import com.couchbase.client.core.node.NodeIdentifier;
+import com.couchbase.client.core.topology.NodeIdentifier;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
diff --git a/core-io/src/main/java/com/couchbase/client/core/service/Service.java b/core-io/src/main/java/com/couchbase/client/core/service/Service.java
index 3db3998ed..6e4b9dc0c 100644
--- a/core-io/src/main/java/com/couchbase/client/core/service/Service.java
+++ b/core-io/src/main/java/com/couchbase/client/core/service/Service.java
@@ -84,10 +84,7 @@ public interface Service extends Stateful {
* Returns the remote address for this service.
*/
default HostAndPort address() {
- return new HostAndPort(
- context().remoteHostname(),
- context().remotePort()
- );
+ return context().remote();
}
@Stability.Internal
diff --git a/core-io/src/main/java/com/couchbase/client/core/service/ServiceContext.java b/core-io/src/main/java/com/couchbase/client/core/service/ServiceContext.java
index b3a05ee5c..c86e186f5 100644
--- a/core-io/src/main/java/com/couchbase/client/core/service/ServiceContext.java
+++ b/core-io/src/main/java/com/couchbase/client/core/service/ServiceContext.java
@@ -17,23 +17,14 @@
package com.couchbase.client.core.service;
import com.couchbase.client.core.CoreContext;
+import com.couchbase.client.core.util.HostAndPort;
import java.util.Map;
import java.util.Optional;
-import static com.couchbase.client.core.logging.RedactableArgument.redactSystem;
-
public class ServiceContext extends CoreContext {
- /**
- * The hostname of this service.
- */
- private final String remoteHostname;
-
- /**
- * The port of this service.
- */
- private final int remotePort;
+ private final HostAndPort remote;
/**
* The service type of this context.
@@ -45,24 +36,35 @@ public class ServiceContext extends CoreContext {
public ServiceContext(CoreContext ctx, String remoteHostname, int remotePort,
ServiceType serviceType, Optional bucket) {
super(ctx.core(), ctx.id(), ctx.environment(), ctx.authenticator());
- this.remoteHostname = remoteHostname;
- this.remotePort = remotePort;
this.bucket = bucket;
this.serviceType = serviceType;
+ this.remote = new HostAndPort(remoteHostname, remotePort);
}
+ /**
+ * @deprecated In favor of {@link #remote()}
+ */
+ @Deprecated
public String remoteHostname() {
- return remoteHostname;
+ return remote.host();
}
+ /**
+ * @deprecated In favor of {@link #remote()}
+ */
+ @Deprecated
public int remotePort() {
- return remotePort;
+ return remote.port();
+ }
+
+ public HostAndPort remote() {
+ return remote;
}
@Override
public void injectExportableParams(final Map input) {
super.injectExportableParams(input);
- input.put("remote", redactSystem(remoteHostname() + ":" + remotePort()));
+ input.put("remote", remote.toString());
input.put("type", serviceType);
bucket.ifPresent(b -> input.put("bucket", b));
}
diff --git a/core-io/src/main/java/com/couchbase/client/core/service/kv/NodeIndexCalculator.java b/core-io/src/main/java/com/couchbase/client/core/service/kv/NodeIndexCalculator.java
new file mode 100644
index 000000000..e381c57d1
--- /dev/null
+++ b/core-io/src/main/java/com/couchbase/client/core/service/kv/NodeIndexCalculator.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2024 Couchbase, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.couchbase.client.core.service.kv;
+
+import com.couchbase.client.core.CoreContext;
+import com.couchbase.client.core.annotation.Stability;
+import com.couchbase.client.core.api.kv.CoreReadPreference;
+import com.couchbase.client.core.config.CouchbaseBucketConfig;
+import com.couchbase.client.core.config.PortInfo;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.nio.charset.StandardCharsets;
+
+import static com.couchbase.client.core.node.KeyValueLocator.partitionForKey;
+
+@Stability.Internal
+public class NodeIndexCalculator {
+ private final boolean[] allowedNodeIndexes;
+ private final CouchbaseBucketConfig topology;
+ private final static Logger logger = LoggerFactory.getLogger(NodeIndexCalculator.class);
+
+ public NodeIndexCalculator(CoreReadPreference readPreference, CouchbaseBucketConfig topology, CoreContext coreContext) {
+ boolean[] allowedNodeIndexesOut = new boolean[topology.nodes().size()];
+
+ for (int nodeIndex = 0; nodeIndex < topology.portInfos().size(); nodeIndex++) {
+ boolean canUseNode = true;
+ PortInfo node = topology.portInfos().get(nodeIndex);
+
+ if (readPreference == CoreReadPreference.PREFERRED_SERVER_GROUP) {
+ canUseNode = node.serverGroup() != null && node.serverGroup().equals(coreContext.environment().preferredServerGroup());
+ }
+
+ allowedNodeIndexesOut[nodeIndex] = canUseNode;
+ }
+
+ this.allowedNodeIndexes = allowedNodeIndexesOut;
+ this.topology = topology;
+ }
+
+ public boolean canUseNode(String documentId, int replicaIndex, boolean isActive) {
+ boolean useFastForward = false;
+ int partitionId = partitionForKey(documentId.getBytes(StandardCharsets.UTF_8), topology.numberOfPartitions());
+ int nodeIndex;
+
+ if (isActive) {
+ nodeIndex = topology.nodeIndexForActive(partitionId, useFastForward);
+ } else {
+ nodeIndex = topology.nodeIndexForReplica(partitionId, replicaIndex, useFastForward);
+ }
+
+ logger.trace("Checking whether doc can use node. doc={} isActive={} replica={} pid={} ni={} allowed={}", documentId, isActive, replicaIndex, partitionId, nodeIndex, allowedNodeIndexes);
+
+ return check(nodeIndex);
+ }
+
+ public boolean canUseNodeForActive(String documentId) {
+ return canUseNode(documentId, 0, true);
+ }
+
+ public boolean canUseNodeForReplica(String documentId, int replicaIndex) {
+ return canUseNode(documentId, replicaIndex, false);
+ }
+
+ public boolean check(int nodeIndex) {
+ if (nodeIndex < 0 || nodeIndex > allowedNodeIndexes.length) {
+ return false;
+ }
+ return allowedNodeIndexes[nodeIndex];
+ }
+}
diff --git a/core-io/src/main/java/com/couchbase/client/core/service/kv/Observe.java b/core-io/src/main/java/com/couchbase/client/core/service/kv/Observe.java
index ab84aedc9..ff6630090 100644
--- a/core-io/src/main/java/com/couchbase/client/core/service/kv/Observe.java
+++ b/core-io/src/main/java/com/couchbase/client/core/service/kv/Observe.java
@@ -54,7 +54,7 @@ public static Mono poll(final ObserveContext ctx) {
}
final RequestSpan parentSpan = ctx
- .environment()
+ .coreResources()
.requestTracer()
.requestSpan("observe", ctx.parentSpan());
@@ -79,7 +79,7 @@ private static Flux viaMutationToken(final int bucketReplicas, fina
List requests = new ArrayList<>();
if (ctx.persistTo() != ObservePersistTo.NONE) {
- final RequestSpan span = ctx.environment().requestTracer()
+ final RequestSpan span = ctx.coreResources().requestTracer()
.requestSpan(TracingIdentifiers.SPAN_REQUEST_KV_OBSERVE, parent);
requests.add(new ObserveViaSeqnoRequest(timeout, ctx, ctx.collectionIdentifier(), retryStrategy, 0, true,
mutationToken.partitionUUID(), id, span));
@@ -87,7 +87,7 @@ private static Flux viaMutationToken(final int bucketReplicas, fina
if (ctx.persistTo().touchesReplica() || ctx.replicateTo().touchesReplica()) {
for (short i = 1; i <= bucketReplicas; i++) {
- final RequestSpan span = ctx.environment().requestTracer()
+ final RequestSpan span = ctx.coreResources().requestTracer()
.requestSpan(TracingIdentifiers.SPAN_REQUEST_KV_OBSERVE, parent);
requests.add(new ObserveViaSeqnoRequest(timeout, ctx, ctx.collectionIdentifier(), retryStrategy, i, false,
mutationToken.partitionUUID(), id, span));
diff --git a/core-io/src/main/java/com/couchbase/client/core/service/kv/ReplicaHelper.java b/core-io/src/main/java/com/couchbase/client/core/service/kv/ReplicaHelper.java
index 7ab7f68af..c0e546568 100644
--- a/core-io/src/main/java/com/couchbase/client/core/service/kv/ReplicaHelper.java
+++ b/core-io/src/main/java/com/couchbase/client/core/service/kv/ReplicaHelper.java
@@ -24,6 +24,7 @@
import com.couchbase.client.core.api.kv.CoreKvResponseMetadata;
import com.couchbase.client.core.api.kv.CoreSubdocGetCommand;
import com.couchbase.client.core.api.kv.CoreSubdocGetResult;
+import com.couchbase.client.core.api.kv.CoreReadPreference;
import com.couchbase.client.core.cnc.RequestSpan;
import com.couchbase.client.core.cnc.TracingIdentifiers;
import com.couchbase.client.core.cnc.events.request.IndividualReplicaGetFailedEvent;
@@ -106,21 +107,21 @@ public static Flux getAllReplicasReactive(
final Duration timeout,
final RetryStrategy retryStrategy,
Map clientContext,
- RequestSpan parentSpan
+ RequestSpan parentSpan,
+ CoreReadPreference readPreference
) {
notNullOrEmpty(documentId, "Id", () -> ReducedKeyValueErrorContext.create(documentId, collectionIdentifier));
- CoreEnvironment env = core.context().environment();
- RequestSpan getAllSpan = env.requestTracer().requestSpan(TracingIdentifiers.SPAN_GET_ALL_REPLICAS, parentSpan);
+ RequestSpan getAllSpan = core.context().coreResources().requestTracer().requestSpan(TracingIdentifiers.SPAN_GET_ALL_REPLICAS, parentSpan);
return Reactor
- .toMono(() -> getAllReplicasRequests(core, collectionIdentifier, documentId, clientContext, retryStrategy, timeout, getAllSpan))
+ .toMono(() -> getAllReplicasRequests(core, collectionIdentifier, documentId, clientContext, retryStrategy, timeout, getAllSpan, readPreference))
.flux()
.flatMap(Flux::fromStream)
.flatMap(request -> Reactor
.wrap(request, get(core, request), true)
.onErrorResume(t -> {
- env.eventBus().publish(new IndividualReplicaGetFailedEvent(request.context()));
+ core.environment().eventBus().publish(new IndividualReplicaGetFailedEvent(request.context()));
return Mono.empty(); // Swallow any errors from individual replicas
})
.map(response -> new GetReplicaResponse(response, request instanceof ReplicaGetRequest))
@@ -147,14 +148,15 @@ public static Flux lookupInAllReplicasReactive(
final Duration timeout,
final RetryStrategy retryStrategy,
Map clientContext,
- RequestSpan parentSpan
+ RequestSpan parentSpan,
+ CoreReadPreference readPreference
) {
notNullOrEmpty(documentId, "Id", () -> ReducedKeyValueErrorContext.create(documentId, collectionIdentifier));
CoreEnvironment env = core.context().environment();
- RequestSpan getAllSpan = env.requestTracer().requestSpan(TracingIdentifiers.SPAN_LOOKUP_IN_ALL_REPLICAS, parentSpan);
+ RequestSpan getAllSpan = core.context().coreResources().requestTracer().requestSpan(TracingIdentifiers.SPAN_LOOKUP_IN_ALL_REPLICAS, parentSpan);
return Reactor
- .toMono(() -> lookupInAllReplicasRequests(core, collectionIdentifier, documentId, commands, clientContext, retryStrategy, timeout, getAllSpan))
+ .toMono(() -> lookupInAllReplicasRequests(core, collectionIdentifier, documentId, commands, clientContext, retryStrategy, timeout, getAllSpan, readPreference))
.flux()
.flatMap(Flux::fromStream)
.flatMap(request -> Reactor
@@ -184,12 +186,12 @@ public static CompletableFuture>> getAllReplicasAs
final RetryStrategy retryStrategy,
final Map clientContext,
final RequestSpan parentSpan,
+ final CoreReadPreference readPreference,
final Function responseMapper
) {
- CoreEnvironment env = core.context().environment();
- RequestSpan getAllSpan = env.requestTracer().requestSpan(TracingIdentifiers.SPAN_LOOKUP_IN_ALL_REPLICAS, parentSpan);
+ RequestSpan getAllSpan = core.context().coreResources().requestTracer().requestSpan(TracingIdentifiers.SPAN_LOOKUP_IN_ALL_REPLICAS, parentSpan);
- return getAllReplicasRequests(core, collectionIdentifier, documentId, clientContext, retryStrategy, timeout, getAllSpan)
+ return getAllReplicasRequests(core, collectionIdentifier, documentId, clientContext, retryStrategy, timeout, getAllSpan, readPreference)
.thenApply(stream ->
stream.map(request ->
get(core, request)
@@ -232,12 +234,12 @@ public static CompletableFuture>> lookupInAllRepli
final RetryStrategy retryStrategy,
final Map clientContext,
final RequestSpan parentSpan,
+ final CoreReadPreference readPreference,
final Function responseMapper
) {
- CoreEnvironment env = core.context().environment();
- RequestSpan getAllSpan = env.requestTracer().requestSpan(TracingIdentifiers.SPAN_GET_ALL_REPLICAS, parentSpan);
+ RequestSpan getAllSpan = core.context().coreResources().requestTracer().requestSpan(TracingIdentifiers.SPAN_GET_ALL_REPLICAS, parentSpan);
- return lookupInAllReplicasRequests(core, collectionIdentifier, documentId, commands, clientContext, retryStrategy, timeout, getAllSpan)
+ return lookupInAllReplicasRequests(core, collectionIdentifier, documentId, commands, clientContext, retryStrategy, timeout, getAllSpan, readPreference)
.thenApply(stream ->
stream.map(request ->
get(core, request)
@@ -269,13 +271,14 @@ public static CompletableFuture getAnyReplicaAsync(
final RetryStrategy retryStrategy,
final Map clientContext,
final RequestSpan parentSpan,
+ final CoreReadPreference readPreference,
final Function responseMapper) {
- RequestSpan getAnySpan = core.context().environment().requestTracer()
+ RequestSpan getAnySpan = core.context().coreResources().requestTracer()
.requestSpan(TracingIdentifiers.SPAN_GET_ANY_REPLICA, parentSpan);
CompletableFuture>> listOfFutures = getAllReplicasAsync(
- core, collectionIdentifier, documentId, timeout, retryStrategy, clientContext, getAnySpan, responseMapper
+ core, collectionIdentifier, documentId, timeout, retryStrategy, clientContext, getAnySpan, readPreference, responseMapper
);
// Aggregating the futures here will discard the individual errors, which we don't need
@@ -304,13 +307,14 @@ public static CompletableFuture lookupInAnyReplicaAsync(
final RetryStrategy retryStrategy,
final Map clientContext,
final RequestSpan parentSpan,
+ final CoreReadPreference readPreference,
final Function responseMapper) {
- RequestSpan getAnySpan = core.context().environment().requestTracer()
+ RequestSpan getAnySpan = core.context().coreResources().requestTracer()
.requestSpan(TracingIdentifiers.SPAN_LOOKUP_IN_ANY_REPLICA, parentSpan);
CompletableFuture>> listOfFutures = lookupInAllReplicasAsync(
- core, collectionIdentifier, documentId, commands, timeout, retryStrategy, clientContext, getAnySpan, responseMapper
+ core, collectionIdentifier, documentId, commands, timeout, retryStrategy, clientContext, getAnySpan, readPreference, responseMapper
);
// Aggregating the futures here will discard the individual errors, which we don't need
@@ -372,7 +376,8 @@ public static CompletableFuture> getAllReplicasRequests(
final Map clientContext,
final RetryStrategy retryStrategy,
final Duration timeout,
- final RequestSpan parent
+ final RequestSpan parent,
+ final CoreReadPreference readPreference
) {
notNullOrEmpty(documentId, "Id");
@@ -381,21 +386,32 @@ public static CompletableFuture> getAllReplicasRequests(
final BucketConfig config = core.clusterConfig().bucketConfig(collectionIdentifier.bucket());
if (config instanceof CouchbaseBucketConfig) {
- int numReplicas = ((CouchbaseBucketConfig) config).numberOfReplicas();
+ CouchbaseBucketConfig topology = (CouchbaseBucketConfig) config;
+ int numReplicas = topology.numberOfReplicas();
List requests = new ArrayList<>(numReplicas + 1);
+ NodeIndexCalculator allowedNodeIndexes = new NodeIndexCalculator(readPreference, topology, coreContext);
- RequestSpan span = environment.requestTracer().requestSpan(TracingIdentifiers.SPAN_REQUEST_KV_GET, parent);
- GetRequest activeRequest = new GetRequest(documentId, timeout, coreContext, collectionIdentifier, retryStrategy, span);
- activeRequest.context().clientContext(clientContext);
- requests.add(activeRequest);
+ if (allowedNodeIndexes.canUseNodeForActive(documentId)) {
+ RequestSpan span = coreContext.coreResources().requestTracer().requestSpan(TracingIdentifiers.SPAN_REQUEST_KV_GET, parent);
+ GetRequest activeRequest = new GetRequest(documentId, timeout, coreContext, collectionIdentifier, retryStrategy, span);
+ activeRequest.context().clientContext(clientContext);
+ requests.add(activeRequest);
+ }
for (short replica = 1; replica <= numReplicas; replica++) {
- RequestSpan replicaSpan = environment.requestTracer().requestSpan(TracingIdentifiers.SPAN_REQUEST_KV_GET_REPLICA, parent);
- ReplicaGetRequest replicaRequest = new ReplicaGetRequest(
+ if (allowedNodeIndexes.canUseNodeForReplica(documentId, replica - 1)) {
+ RequestSpan replicaSpan = coreContext.coreResources().requestTracer().requestSpan(TracingIdentifiers.SPAN_REQUEST_KV_GET_REPLICA, parent);
+ ReplicaGetRequest replicaRequest = new ReplicaGetRequest(
documentId, timeout, coreContext, collectionIdentifier, retryStrategy, replica, replicaSpan
- );
- replicaRequest.context().clientContext(clientContext);
- requests.add(replicaRequest);
+ );
+ replicaRequest.context().clientContext(clientContext);
+ requests.add(replicaRequest);
+ }
+ }
+ if (requests.isEmpty()) {
+ CompletableFuture> future = new CompletableFuture<>();
+ future.completeExceptionally(DocumentUnretrievableException.noReplicasSuitable());
+ return future;
}
return CompletableFuture.completedFuture(requests.stream());
} else if (config == null) {
@@ -404,7 +420,7 @@ public static CompletableFuture> getAllReplicasRequests(
final Duration retryDelay = Duration.ofMillis(100);
final CompletableFuture> future = new CompletableFuture<>();
coreContext.environment().timer().schedule(() -> {
- getAllReplicasRequests(core, collectionIdentifier, documentId, clientContext, retryStrategy, timeout.minus(retryDelay), parent).whenComplete((getRequestStream, throwable) -> {
+ getAllReplicasRequests(core, collectionIdentifier, documentId, clientContext, retryStrategy, timeout.minus(retryDelay), parent, readPreference).whenComplete((getRequestStream, throwable) -> {
if (throwable != null) {
future.completeExceptionally(throwable);
} else {
@@ -441,7 +457,8 @@ public static CompletableFuture> lookupInAllReplicasReq
final Map clientContext,
final RetryStrategy retryStrategy,
final Duration timeout,
- final RequestSpan parent
+ final RequestSpan parent,
+ final CoreReadPreference readPreference
) {
notNullOrEmpty(documentId, "Id");
@@ -450,26 +467,37 @@ public static CompletableFuture> lookupInAllReplicasReq
final BucketConfig config = core.clusterConfig().bucketConfig(collectionIdentifier.bucket());
if (config instanceof CouchbaseBucketConfig) {
+ CouchbaseBucketConfig topology = (CouchbaseBucketConfig) config;
if (!config.bucketCapabilities().contains(BucketCapabilities.SUBDOC_READ_REPLICA)) {
return failedFuture(FeatureNotAvailableException.subdocReadReplica());
}
- int numReplicas = ((CouchbaseBucketConfig) config).numberOfReplicas();
+ int numReplicas = topology.numberOfReplicas();
List requests = new ArrayList<>(numReplicas + 1);
+ NodeIndexCalculator allowedNodeIndexes = new NodeIndexCalculator(readPreference, topology, coreContext);
- RequestSpan span = environment.requestTracer().requestSpan(TracingIdentifiers.SPAN_REQUEST_KV_LOOKUP_IN, parent);
- SubdocGetRequest activeRequest = SubdocGetRequest.create(timeout, coreContext, collectionIdentifier, retryStrategy, documentId, (byte)0, commands, span);
- activeRequest.context().clientContext(clientContext);
- requests.add(activeRequest);
+ if (allowedNodeIndexes.canUseNodeForActive(documentId)) {
+ RequestSpan span = coreContext.coreResources().requestTracer().requestSpan(TracingIdentifiers.SPAN_REQUEST_KV_LOOKUP_IN, parent);
+ SubdocGetRequest activeRequest = SubdocGetRequest.create(timeout, coreContext, collectionIdentifier, retryStrategy, documentId, (byte) 0, commands, span);
+ activeRequest.context().clientContext(clientContext);
+ requests.add(activeRequest);
+ }
for (short replica = 1; replica <= numReplicas; replica++) {
- RequestSpan replicaSpan = environment.requestTracer().requestSpan(TracingIdentifiers.SPAN_LOOKUP_IN_ALL_REPLICAS, parent);
- ReplicaSubdocGetRequest replicaRequest = ReplicaSubdocGetRequest.create(
- timeout, coreContext, collectionIdentifier, retryStrategy, documentId, (byte)0, commands, replica, replicaSpan
- );
- replicaRequest.context().clientContext(clientContext);
- requests.add(replicaRequest);
+ if (allowedNodeIndexes.canUseNodeForReplica(documentId, replica - 1)) {
+ RequestSpan replicaSpan = coreContext.coreResources().requestTracer().requestSpan(TracingIdentifiers.SPAN_LOOKUP_IN_ALL_REPLICAS, parent);
+ ReplicaSubdocGetRequest replicaRequest = ReplicaSubdocGetRequest.create(
+ timeout, coreContext, collectionIdentifier, retryStrategy, documentId, (byte) 0, commands, replica, replicaSpan
+ );
+ replicaRequest.context().clientContext(clientContext);
+ requests.add(replicaRequest);
+ }
+ }
+ if (requests.isEmpty()) {
+ CompletableFuture> future = new CompletableFuture<>();
+ future.completeExceptionally(DocumentUnretrievableException.noReplicasSuitable());
+ return future;
}
return CompletableFuture.completedFuture(requests.stream());
} else if (config == null) {
@@ -478,7 +506,7 @@ public static CompletableFuture> lookupInAllReplicasReq
final Duration retryDelay = Duration.ofMillis(100);
final CompletableFuture> future = new CompletableFuture<>();
coreContext.environment().timer().schedule(() -> {
- lookupInAllReplicasRequests(core, collectionIdentifier, documentId, commands, clientContext, retryStrategy, timeout.minus(retryDelay), parent).whenComplete((getRequestStream, throwable) -> {
+ lookupInAllReplicasRequests(core, collectionIdentifier, documentId, commands, clientContext, retryStrategy, timeout.minus(retryDelay), parent, readPreference).whenComplete((getRequestStream, throwable) -> {
if (throwable != null) {
future.completeExceptionally(throwable);
} else {
diff --git a/core-io/src/main/java/com/couchbase/client/core/topology/ClusterIdentifier.java b/core-io/src/main/java/com/couchbase/client/core/topology/ClusterIdentifier.java
new file mode 100644
index 000000000..3f8c72eeb
--- /dev/null
+++ b/core-io/src/main/java/com/couchbase/client/core/topology/ClusterIdentifier.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2024 Couchbase, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.couchbase.client.core.topology;
+
+import com.couchbase.client.core.annotation.Stability;
+import com.couchbase.client.core.deps.com.fasterxml.jackson.databind.JsonNode;
+import com.couchbase.client.core.deps.com.fasterxml.jackson.databind.node.ObjectNode;
+import reactor.util.annotation.Nullable;
+
+import static com.couchbase.client.core.logging.RedactableArgument.redactMeta;
+
+@Stability.Internal
+public class ClusterIdentifier {
+ private final String clusterUuid;
+ private final String clusterName;
+
+ ClusterIdentifier(String clusterUuid, String clusterName) {
+ this.clusterUuid = clusterUuid;
+ this.clusterName = clusterName;
+ }
+
+ public static @Nullable ClusterIdentifier parse(ObjectNode config) {
+ JsonNode clusterUuid = config.path("clusterUUID");
+ JsonNode clusterName = config.path("clusterName");
+ if (clusterUuid.isMissingNode() || clusterName.isMissingNode()) {
+ return null;
+ }
+ return new ClusterIdentifier(clusterUuid.asText(), clusterName.asText());
+ }
+
+ public String clusterUuid() {
+ return clusterUuid;
+ }
+
+ public String clusterName() {
+ return clusterName;
+ }
+
+ @Override
+ public String toString() {
+ return "ClusterIdent{" +
+ "clusterUuid='" + clusterUuid + '\'' +
+ ", clusterName='" + redactMeta(clusterName) + '\'' +
+ '}';
+ }
+}
diff --git a/core-io/src/main/java/com/couchbase/client/core/topology/ClusterIdentifierUtil.java b/core-io/src/main/java/com/couchbase/client/core/topology/ClusterIdentifierUtil.java
new file mode 100644
index 000000000..033c7ade1
--- /dev/null
+++ b/core-io/src/main/java/com/couchbase/client/core/topology/ClusterIdentifierUtil.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2024 Couchbase, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.couchbase.client.core.topology;
+
+import com.couchbase.client.core.config.ClusterConfig;
+import reactor.util.annotation.Nullable;
+
+public class ClusterIdentifierUtil {
+ private ClusterIdentifierUtil() {}
+
+ public static @Nullable ClusterIdentifier fromConfig(@Nullable ClusterConfig config) {
+ return config == null ? null : config.globalConfig() == null ? null : config.globalConfig().clusterIdent();
+ }
+}
diff --git a/core-io/src/main/java/com/couchbase/client/core/topology/ClusterTopology.java b/core-io/src/main/java/com/couchbase/client/core/topology/ClusterTopology.java
index eda14a390..3858c9024 100644
--- a/core-io/src/main/java/com/couchbase/client/core/topology/ClusterTopology.java
+++ b/core-io/src/main/java/com/couchbase/client/core/topology/ClusterTopology.java
@@ -36,9 +36,11 @@ public class ClusterTopology {
private final NetworkResolution network;
private final Set capabilities;
private final List nodes;
+ @Nullable private final ClusterIdentifier clusterIdent;
public static ClusterTopology of(
TopologyRevision revision,
+ @Nullable ClusterIdentifier clusterIdent,
List nodes,
Set capabilities,
NetworkResolution network,
@@ -52,7 +54,8 @@ public static ClusterTopology of(
capabilities,
network,
portSelector,
- bucket
+ bucket,
+ clusterIdent
);
}
@@ -61,7 +64,8 @@ public static ClusterTopology of(
nodes,
capabilities,
network,
- portSelector
+ portSelector,
+ clusterIdent
);
}
@@ -70,7 +74,8 @@ protected ClusterTopology(
List nodes,
Set capabilities,
NetworkResolution network,
- PortSelector portSelector
+ PortSelector portSelector,
+ @Nullable ClusterIdentifier clusterIdent
) {
if (network.equals(NetworkResolution.AUTO)) {
throw new IllegalArgumentException("Must resolve 'auto' network before creating config.");
@@ -81,6 +86,7 @@ protected ClusterTopology(
this.capabilities = unmodifiableSet(newEnumSet(ClusterCapability.class, capabilities));
this.network = requireNonNull(network);
this.tls = requireNonNull(portSelector) == PortSelector.TLS;
+ this.clusterIdent = clusterIdent;
}
public TopologyRevision revision() {
@@ -114,12 +120,17 @@ public ClusterTopologyWithBucket requireBucket() {
throw new NoSuchElementException("Bucket topology is absent.");
}
+ @Nullable public ClusterIdentifier id() {
+ return clusterIdent;
+ }
+
@Override
public String toString() {
String bucket = this instanceof ClusterTopologyWithBucket ? this.requireBucket().bucket().toString() : "";
return "ClusterTopology{" +
"revision=" + revision +
+ ", clusterIdent=" + clusterIdent +
", tls=" + tls +
", network=" + network +
", capabilities=" + capabilities +
diff --git a/core-io/src/main/java/com/couchbase/client/core/topology/ClusterTopologyParser.java b/core-io/src/main/java/com/couchbase/client/core/topology/ClusterTopologyParser.java
index 78d20c455..a37194abe 100644
--- a/core-io/src/main/java/com/couchbase/client/core/topology/ClusterTopologyParser.java
+++ b/core-io/src/main/java/com/couchbase/client/core/topology/ClusterTopologyParser.java
@@ -123,8 +123,11 @@ resolvedNetwork, redactSystem(it)
BucketTopology bucket = BucketTopology.parse(clusterConfig, nodesReadyToServiceThisBucket, memcachedHashingStrategy);
+ ClusterIdentifier clusterIdent = ClusterIdentifier.parse(clusterConfig);
+
return ClusterTopology.of(
TopologyRevision.parse(clusterConfig),
+ clusterIdent,
resolvedNodes,
parseCapabilities(clusterConfig),
resolvedNetwork,
diff --git a/core-io/src/main/java/com/couchbase/client/core/topology/ClusterTopologyWithBucket.java b/core-io/src/main/java/com/couchbase/client/core/topology/ClusterTopologyWithBucket.java
index 53afd70cc..2c56aefc9 100644
--- a/core-io/src/main/java/com/couchbase/client/core/topology/ClusterTopologyWithBucket.java
+++ b/core-io/src/main/java/com/couchbase/client/core/topology/ClusterTopologyWithBucket.java
@@ -18,6 +18,7 @@
import com.couchbase.client.core.annotation.Stability;
import com.couchbase.client.core.env.NetworkResolution;
+import reactor.util.annotation.Nullable;
import java.util.List;
import java.util.Set;
@@ -40,9 +41,10 @@ public class ClusterTopologyWithBucket extends ClusterTopology {
Set capabilities,
NetworkResolution network,
PortSelector portSelector,
- BucketTopology bucket
+ BucketTopology bucket,
+ @Nullable ClusterIdentifier clusterIdent
) {
- super(revision, nodes, capabilities, network, portSelector);
+ super(revision, nodes, capabilities, network, portSelector, clusterIdent);
this.bucket = requireNonNull(bucket);
}
diff --git a/core-io/src/main/java/com/couchbase/client/core/topology/HostAndServicePorts.java b/core-io/src/main/java/com/couchbase/client/core/topology/HostAndServicePorts.java
index 5812197a6..7229d0fb0 100644
--- a/core-io/src/main/java/com/couchbase/client/core/topology/HostAndServicePorts.java
+++ b/core-io/src/main/java/com/couchbase/client/core/topology/HostAndServicePorts.java
@@ -50,6 +50,7 @@ public class HostAndServicePorts implements KetamaRingNode {
"",
emptyMap(),
new NodeIdentifier("", 0, ""),
+ null,
null
);
@@ -57,17 +58,20 @@ public class HostAndServicePorts implements KetamaRingNode {
private final Map ports;
private final NodeIdentifier id;
@Nullable private final HostAndPort ketamaAuthority;
+ @Nullable private final String serverGroup;
public HostAndServicePorts(
String host,
Map ports,
NodeIdentifier id,
- @Nullable HostAndPort ketamaAuthority
+ @Nullable HostAndPort ketamaAuthority,
+ @Nullable String serverGroup
) {
this.host = requireNonNull(host);
this.ports = unmodifiableMap(newEnumMap(ServiceType.class, ports));
this.id = requireNonNull(id);
this.ketamaAuthority = ketamaAuthority;
+ this.serverGroup = serverGroup;
}
public boolean inaccessible() {
@@ -105,6 +109,10 @@ public Map ports() {
return ports;
}
+ public @Nullable String serverGroup() {
+ return serverGroup;
+ }
+
public boolean has(ServiceType serviceType) {
return ports.containsKey(serviceType);
}
@@ -121,7 +129,7 @@ public HostAndServicePorts without(ServiceType service, ServiceType... moreServi
temp.remove(t);
}
- return new HostAndServicePorts(this.host, temp, this.id, this.ketamaAuthority);
+ return new HostAndServicePorts(this.host, temp, this.id, this.ketamaAuthority, this.serverGroup);
}
@Stability.Internal
@@ -129,7 +137,7 @@ public HostAndServicePorts withKetamaAuthority(@Nullable HostAndPort ketamaAutho
if (Objects.equals(this.ketamaAuthority, ketamaAuthority)) {
return this;
}
- return new HostAndServicePorts(this.host, this.ports, this.id, ketamaAuthority);
+ return new HostAndServicePorts(this.host, this.ports, this.id, ketamaAuthority, this.serverGroup);
}
boolean matches(SeedNode seedNode) {
diff --git a/core-io/src/main/java/com/couchbase/client/core/topology/HostAndServicePortsParser.java b/core-io/src/main/java/com/couchbase/client/core/topology/HostAndServicePortsParser.java
index 219d9c658..6b03b7007 100644
--- a/core-io/src/main/java/com/couchbase/client/core/topology/HostAndServicePortsParser.java
+++ b/core-io/src/main/java/com/couchbase/client/core/topology/HostAndServicePortsParser.java
@@ -54,13 +54,16 @@ public static Map parse(
) {
Map raw = parseIntermediate(json);
HostAndPort ketamaAuthority = getKetamaAuthority(raw);
+ String serverGroup = json.path("serverGroup").asText();
+ final String serverGroupFinal = serverGroup.isEmpty() ? null : serverGroup;
return transformValues(raw, value ->
new HostAndServicePorts(
value.host,
portSelector.selectPorts(value.rawServicePorts),
getId(value.host, raw),
- ketamaAuthority
+ ketamaAuthority,
+ serverGroupFinal
)
);
}
diff --git a/core-io/src/main/java/com/couchbase/client/core/topology/NodeIdentifier.java b/core-io/src/main/java/com/couchbase/client/core/topology/NodeIdentifier.java
index 403d839b7..e61720360 100644
--- a/core-io/src/main/java/com/couchbase/client/core/topology/NodeIdentifier.java
+++ b/core-io/src/main/java/com/couchbase/client/core/topology/NodeIdentifier.java
@@ -19,8 +19,6 @@
import com.couchbase.client.core.annotation.Stability;
import com.couchbase.client.core.util.HostAndPort;
-import java.util.Objects;
-
import static java.util.Objects.requireNonNull;
@Stability.Internal
@@ -28,8 +26,12 @@ public class NodeIdentifier {
private final HostAndPort canonical; // manager host:port on default network
private final String hostForNetworkConnections;
- public NodeIdentifier(String host, int port, String hostForNetworkConnections) {
- this(new HostAndPort(host, port), hostForNetworkConnections);
+ public NodeIdentifier(
+ String canonicalHost,
+ int canonicalPort,
+ String hostForNetworkConnections
+ ) {
+ this(new HostAndPort(canonicalHost, canonicalPort), hostForNetworkConnections);
}
public NodeIdentifier(HostAndPort canonical, String hostForNetworkConnections) {
@@ -37,6 +39,13 @@ public NodeIdentifier(HostAndPort canonical, String hostForNetworkConnections) {
this.hostForNetworkConnections = requireNonNull(hostForNetworkConnections);
}
+ public static NodeIdentifier forBootstrap(String bootstrapHost, int bootstrapPort) {
+ // This address isn't really "canonical", since it may be an "external" address.
+ // If it's an external address, the node created from this identifier will be discarded
+ // when the config with the _real_ canonical addresses is applied.
+ return new NodeIdentifier(new HostAndPort(bootstrapHost, bootstrapPort), bootstrapHost);
+ }
+
@Deprecated
public com.couchbase.client.core.node.NodeIdentifier toLegacy() {
return new com.couchbase.client.core.node.NodeIdentifier(canonical, hostForNetworkConnections);
@@ -56,12 +65,12 @@ public boolean equals(Object o) {
@Override
public int hashCode() {
- return Objects.hash(canonical);
+ return canonical.hashCode();
}
@Override
public String toString() {
- return "NodeID{" +
+ return "NodeIdentifier{" +
"canonical=" + canonical +
", hostForNetworkConnections='" + hostForNetworkConnections + '\'' +
'}';
diff --git a/core-io/src/main/java/com/couchbase/client/core/transaction/CoreTransactionAttemptContext.java b/core-io/src/main/java/com/couchbase/client/core/transaction/CoreTransactionAttemptContext.java
index 9bd1b6d45..d213158ba 100644
--- a/core-io/src/main/java/com/couchbase/client/core/transaction/CoreTransactionAttemptContext.java
+++ b/core-io/src/main/java/com/couchbase/client/core/transaction/CoreTransactionAttemptContext.java
@@ -19,18 +19,22 @@
import com.couchbase.client.core.Core;
import com.couchbase.client.core.annotation.Stability;
import com.couchbase.client.core.annotation.UsedBy;
+import com.couchbase.client.core.api.kv.CoreKvResponseMetadata;
+import com.couchbase.client.core.api.kv.CoreSubdocGetResult;
import com.couchbase.client.core.api.query.CoreQueryContext;
import com.couchbase.client.core.api.query.CoreQueryOps;
import com.couchbase.client.core.api.query.CoreQueryOptions;
import com.couchbase.client.core.api.query.CoreQueryOptionsTransactions;
import com.couchbase.client.core.api.query.CoreQueryResult;
-import com.couchbase.client.core.classic.query.ClassicCoreQueryResult;
import com.couchbase.client.core.api.query.CoreQueryStatus;
+import com.couchbase.client.core.classic.query.ClassicCoreQueryResult;
import com.couchbase.client.core.classic.query.ClassicCoreReactiveQueryResult;
import com.couchbase.client.core.cnc.Event;
import com.couchbase.client.core.cnc.RequestSpan;
import com.couchbase.client.core.cnc.RequestTracer;
import com.couchbase.client.core.cnc.TracingIdentifiers;
+import com.couchbase.client.core.cnc.events.transaction.IllegalDocumentStateEvent;
+import com.couchbase.client.core.cnc.events.transaction.TransactionLogEvent;
import com.couchbase.client.core.deps.com.fasterxml.jackson.core.JsonProcessingException;
import com.couchbase.client.core.deps.com.fasterxml.jackson.databind.JsonNode;
import com.couchbase.client.core.deps.com.fasterxml.jackson.databind.node.ArrayNode;
@@ -42,6 +46,7 @@
import com.couchbase.client.core.error.DecodingFailureException;
import com.couchbase.client.core.error.DocumentExistsException;
import com.couchbase.client.core.error.DocumentNotFoundException;
+import com.couchbase.client.core.error.DocumentUnretrievableException;
import com.couchbase.client.core.error.FeatureNotAvailableException;
import com.couchbase.client.core.error.context.ReducedKeyValueErrorContext;
import com.couchbase.client.core.error.transaction.ActiveTransactionRecordEntryNotFoundException;
@@ -74,13 +79,12 @@
import com.couchbase.client.core.msg.kv.InsertResponse;
import com.couchbase.client.core.msg.kv.SubdocCommandType;
import com.couchbase.client.core.msg.kv.SubdocGetRequest;
-import com.couchbase.client.core.msg.kv.SubdocGetResponse;
import com.couchbase.client.core.msg.kv.SubdocMutateRequest;
import com.couchbase.client.core.msg.kv.SubdocMutateResponse;
-import com.couchbase.client.core.node.NodeIdentifier;
import com.couchbase.client.core.retry.reactor.Jitter;
import com.couchbase.client.core.retry.reactor.Retry;
import com.couchbase.client.core.retry.reactor.RetryExhaustedException;
+import com.couchbase.client.core.topology.NodeIdentifier;
import com.couchbase.client.core.transaction.atr.ActiveTransactionRecordIds;
import com.couchbase.client.core.transaction.cleanup.CleanupRequest;
import com.couchbase.client.core.transaction.cleanup.CoreTransactionsCleanup;
@@ -94,11 +98,11 @@
import com.couchbase.client.core.transaction.components.OperationTypes;
import com.couchbase.client.core.transaction.components.TransactionLinks;
import com.couchbase.client.core.transaction.config.CoreMergedTransactionConfig;
+import com.couchbase.client.core.transaction.error.internal.ErrorClass;
import com.couchbase.client.core.transaction.forwards.CoreTransactionsExtension;
import com.couchbase.client.core.transaction.forwards.ForwardCompatibility;
import com.couchbase.client.core.transaction.forwards.ForwardCompatibilityStage;
import com.couchbase.client.core.transaction.log.CoreTransactionLogger;
-import com.couchbase.client.core.cnc.events.transaction.TransactionLogEvent;
import com.couchbase.client.core.transaction.support.AttemptState;
import com.couchbase.client.core.transaction.support.OptionsUtil;
import com.couchbase.client.core.transaction.support.SpanWrapper;
@@ -110,15 +114,13 @@
import com.couchbase.client.core.transaction.util.DebugUtil;
import com.couchbase.client.core.transaction.util.LockTokens;
import com.couchbase.client.core.transaction.util.LogDeferThrowable;
+import com.couchbase.client.core.transaction.util.MeteringUnits;
import com.couchbase.client.core.transaction.util.MonoBridge;
import com.couchbase.client.core.transaction.util.QueryUtil;
import com.couchbase.client.core.transaction.util.ReactiveLock;
import com.couchbase.client.core.transaction.util.ReactiveWaitGroup;
import com.couchbase.client.core.transaction.util.TransactionKVHandler;
import com.couchbase.client.core.transaction.util.TriFunction;
-import com.couchbase.client.core.transaction.error.internal.ErrorClass;
-import com.couchbase.client.core.cnc.events.transaction.IllegalDocumentStateEvent;
-import com.couchbase.client.core.transaction.util.MeteringUnits;
import com.couchbase.client.core.util.BucketConfigUtil;
import com.couchbase.client.core.util.CbPreconditions;
import reactor.core.publisher.Flux;
@@ -134,7 +136,6 @@
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
@@ -199,7 +200,7 @@ public TransactionQueryContext(NodeIdentifier queryTarget, @Nullable CoreQueryCo
public static final int STATE_BITS_POSITION_FINAL_ERROR = 4;
public static final int STATE_BITS_MASK_FINAL_ERROR = 0b1110000;
public static final int STATE_BITS_MASK_BITS = 0b0001111;
- public static final int UNSTAGING_PARALLELISM = 1000;
+ public static final int UNSTAGING_PARALLELISM = Integer.parseInt(System.getProperty("com.couchbase.transactions.unstagingParallelism", "1000"));;
private final AtomicInteger stateBits = new AtomicInteger(0);
@@ -438,7 +439,19 @@ private Mono> getInternal(CollectionIdentifie
if (queryModeLocked()) {
return getWithQueryLocked(collection, id, lockToken, span);
} else {
- return getWithKVLocked(collection, id, Optional.empty(), span, lockToken);
+ return getWithKVLocked(collection, id, Optional.empty(), span, lockToken, false);
+ }
+ }));
+ }
+
+ private Mono> getReplicaFromPreferredServerGroupInternal(CollectionIdentifier collection, String id, SpanWrapper pspan) {
+
+ return doKVOperation("get " + DebugUtil.docId(collection, id), pspan, CoreTransactionAttemptContextHooks.HOOK_GET, collection, id,
+ (operationId, span, lockToken) -> Mono.defer(() -> {
+ if (queryModeLocked()) {
+ return Mono.error(new FeatureNotAvailableException("getReplicaFromPreferredServerGroup cannot presently be used in a transaction that has previously involved the query service. It can however be used before any query call."));
+ } else {
+ return getWithKVLocked(collection, id, Optional.empty(), span, lockToken, true);
}
}));
}
@@ -447,12 +460,13 @@ private Mono> getWithKVLocked(CollectionIdent
String id,
Optional resolvingMissingATREntry,
SpanWrapper pspan,
- ReactiveLock.Waiter lockToken) {
+ ReactiveLock.Waiter lockToken,
+ boolean preferredReplicaMode) {
return Mono.defer(() -> {
assertLocked("getWithKV");
- LOGGER.info(attemptId, "getting doc {}, resolvingMissingATREntry={}", DebugUtil.docId(collection, id),
- resolvingMissingATREntry.orElse(""));
+ LOGGER.info(attemptId, "getting doc {}, resolvingMissingATREntry={}, preferredReplicaMode={}", DebugUtil.docId(collection, id),
+ resolvingMissingATREntry.orElse(""), preferredReplicaMode);
Optional ownWrite = checkForOwnWriteLocked(collection, id);
if (ownWrite.isPresent()) {
@@ -497,7 +511,8 @@ private Mono> getWithKVLocked(CollectionIdent
pspan,
resolvingMissingATREntry,
units,
- overall.supported()))
+ overall.supported(),
+ preferredReplicaMode))
.publishOn(scheduler())
@@ -509,7 +524,10 @@ private Mono> getWithKVLocked(CollectionIdent
LOGGER.warn(attemptId, "got error while getting doc {}{} in {}us: {}",
DebugUtil.docId(collection, id), DebugUtil.dbg(built), pspan.elapsedMicros(), dbg(err));
- if (err instanceof ForwardCompatibilityRequiresRetryException
+ if (err instanceof DocumentUnretrievableException) {
+ return Mono.error(err);
+ }
+ else if (err instanceof ForwardCompatibilityRequiresRetryException
|| err instanceof ForwardCompatibilityFailureException) {
TransactionOperationFailedException.Builder error = createError()
.cause(new ForwardCompatibilityFailureException());
@@ -536,7 +554,8 @@ else if (err instanceof ActiveTransactionRecordNotFoundException || err instance
id,
Optional.of(attemptIdToCheck),
pspan,
- newLockToken)
+ newLockToken,
+ preferredReplicaMode)
.onErrorResume(e ->
unlock(newLockToken, "relock error")
.then(Mono.error(e))));
@@ -689,6 +708,22 @@ public Mono get(CollectionIdentifier collection, Strin
});
}
+ public Mono getReplicaFromPreferredServerGroup(CollectionIdentifier collection, String id) {
+ return Mono.defer(() -> {
+ SpanWrapper span = SpanWrapperUtil.createOp(this, tracer(), collection, id, TracingIdentifiers.TRANSACTION_OP_GET_REPLICA_FROM_PREFERRED_SERVER_GROUP, attemptSpan);
+ return getReplicaFromPreferredServerGroupInternal(collection, id, span)
+ .doOnError(err -> span.finishWithErrorStatus())
+ .flatMap(doc -> {
+ span.finish();
+ if (doc.isPresent()) {
+ return Mono.just(doc.get());
+ } else {
+ return Mono.error(new DocumentUnretrievableException(ReducedKeyValueErrorContext.create(id)));
+ }
+ });
+ });
+ }
+
boolean hasExpiredClientSide(String place, Optional docId) {
boolean over = overall.hasExpiredClientSide();
boolean hook = hooks.hasExpiredClientSideHook.apply(this, place, docId);
@@ -1462,7 +1497,7 @@ long expiryRemainingMillis() {
private RequestTracer tracer() {
// Will go to the ThresholdRequestTracer by default. In future, may want our own default tracer.
- return core.context().environment().requestTracer();
+ return core.context().coreResources().requestTracer();
}
private byte[] serialize(Object in) {
@@ -1871,7 +1906,7 @@ private Mono handleDocExistsDuringStagedInsert(String
return hooks.beforeGetDocInExistsDuringStagedInsert.apply(this, id) // testing hook
- .then(DocumentGetter.justGetDoc(core, collection, id, kvTimeoutNonMutating(), pspan, true, logger(), units))
+ .then(DocumentGetter.justGetDoc(core, collection, id, kvTimeoutNonMutating(), pspan, true, logger(), units, false))
.publishOn(scheduler())
@@ -1894,18 +1929,18 @@ private Mono handleDocExistsDuringStagedInsert(String
.flatMap(v -> {
if (v.isPresent()) {
- Tuple2 results = v.get();
+ Tuple2 results = v.get();
CoreTransactionGetResult r = results.getT1();
- SubdocGetResponse lir = results.getT2();
+ CoreSubdocGetResult lir = results.getT2();
MeteringUnits built = addUnits(units.build());
LOGGER.info(attemptId, "{} doc {} exists inTransaction={} isDeleted={}{}",
- bp, DebugUtil.docId(collection, id), r.links(), lir.isDeleted(), DebugUtil.dbg(built));
+ bp, DebugUtil.docId(collection, id), r.links(), lir.tombstone(), DebugUtil.dbg(built));
return forwardCompatibilityCheck(ForwardCompatibilityStage.WRITE_WRITE_CONFLICT_INSERTING_GET, r.links().forwardCompatibility())
.then(Mono.defer(() -> {
- if (lir.isDeleted() && !r.links().isDocumentInTransaction()) {
+ if (lir.tombstone() && !r.links().isDocumentInTransaction()) {
LOGGER.info(attemptId, "{} doc {} is a regular tombstone without txn metadata, proceeding to overwrite",
bp, DebugUtil.docId(collection, id));
@@ -1969,12 +2004,12 @@ private Mono overwriteStagedInsert(String operationId,
SpanWrapper pspan,
String bp,
CoreTransactionGetResult r,
- SubdocGetResponse lir) {
+ CoreSubdocGetResult lir) {
return Mono.defer(() -> {
CbPreconditions.check(r.links().isDocumentInTransaction());
CbPreconditions.check(r.links().op().get().equals(OperationTypes.INSERT));
- if (lir.isDeleted()) {
+ if (lir.tombstone()) {
return createStagedInsert(operationId, collection, id, content, flags, pspan, Optional.of(r.cas()));
}
else {
@@ -2641,14 +2676,9 @@ private Mono commitDocsLocked(SpanWrapper span) {
long start = System.nanoTime();
return Flux.fromIterable(stagedMutationsLocked)
- .parallel(UNSTAGING_PARALLELISM)
- .runOn(scheduler())
-
- .concatMap(staged -> {
- return commitDocWrapperLocked(span, staged);
- })
+ .publishOn(scheduler())
- .sequential()
+ .flatMap(staged -> commitDocWrapperLocked(span, staged), UNSTAGING_PARALLELISM)
.then(Mono.defer(() -> {
long elapsed = TimeUnit.NANOSECONDS.toMicros(System.nanoTime() - start);
@@ -2839,6 +2869,10 @@ false, false, false, false, false, cas, staged.stagedUserFlags, durabilityLevel(
});
}
+ private void addUnits(CoreKvResponseMetadata meta) {
+ meteringUnitsBuilder.add(meta);
+ }
+
private void addUnits(@Nullable MemcacheProtocol.FlexibleExtras flexibleExtras) {
meteringUnitsBuilder.add(flexibleExtras);
}
@@ -2931,7 +2965,7 @@ private Mono handleDocChangedDuringCommit(SpanWrapper span,
}
})
.then(hooks.beforeDocChangedDuringCommit.apply(this, id)) // testing hook
- .then(DocumentGetter.getAsync(core, LOGGER, staged.collection, config, staged.id, attemptId, true, span, Optional.empty(), units, overall.supported()))
+ .then(DocumentGetter.getAsync(core, LOGGER, staged.collection, config, staged.id, attemptId, true, span, Optional.empty(), units, overall.supported(), false))
.publishOn(scheduler())
.onErrorResume(err -> {
ErrorClass ec = classify(err);
@@ -3001,7 +3035,7 @@ private Mono handleDocChangedDuringStaging(SpanWrapper span,
throwIfExpired(id, HOOK_STAGING_DOC_CHANGED);
})
.then(hooks.beforeDocChangedDuringStaging.apply(this, id)) // testing hook
- .then(DocumentGetter.getAsync(core, LOGGER, collection, config, id, attemptId, true, span, Optional.empty(), units, overall.supported()))
+ .then(DocumentGetter.getAsync(core, LOGGER, collection, config, id, attemptId, true, span, Optional.empty(), units, overall.supported(), false))
.publishOn(scheduler())
.onErrorResume(err -> {
MeteringUnits built = addUnits(units.build());
@@ -3089,7 +3123,7 @@ private Mono handleDocChangedDuringRollback(SpanWrapper span,
throwIfExpired(id, HOOK_ROLLBACK_DOC_CHANGED);
})
.then(hooks.beforeDocChangedDuringRollback.apply(this, id)) // testing hook
- .then(DocumentGetter.getAsync(core, LOGGER, collection, config, id, attemptId, true, span, Optional.empty(), units, overall.supported()))
+ .then(DocumentGetter.getAsync(core, LOGGER, collection, config, id, attemptId, true, span, Optional.empty(), units, overall.supported(), false))
.publishOn(scheduler())
.onErrorResume(err -> {
MeteringUnits built = addUnits(units.build());
@@ -3170,6 +3204,7 @@ private Mono atrCommitAmbiguityResolutionLocked(AtomicReference over
.then(hooks.beforeAtrCommitAmbiguityResolution.apply(this)) // testing hook
.then(TransactionKVHandler.lookupIn(core, atrCollection.get(), atrId.get(), kvTimeoutNonMutating(), false, OptionsUtil.createClientContext("atrCommitAmbiguityResolution"), span,
+ false,
Arrays.asList(
new SubdocGetRequest.Command(SubdocCommandType.GET, "attempts." + attemptId + "." + TransactionFields.ATR_FIELD_STATUS, true, 0)
)))
@@ -3178,15 +3213,14 @@ private Mono atrCommitAmbiguityResolutionLocked(AtomicReference over
.flatMap(result -> {
String status = null;
try {
- status = Mapper.reader().readValue(result.values()[0].value(), String.class);
+ status = Mapper.reader().readValue(result.field(0).value(), String.class);
} catch (IOException e) {
- LOGGER.info(attemptId, "failed to parse ATR {} status '{}'", getAtrDebug(atrCollection, atrId), new String(result.values()[0].value()));
+ LOGGER.info(attemptId, "failed to parse ATR {} status '{}'", getAtrDebug(atrCollection, atrId), new String(result.field(0).value()));
status = "UNKNOWN";
}
- addUnits(result.flexibleExtras());
- LOGGER.info(attemptId, "got status of ATR {}{}: '{}'", getAtrDebug(atrCollection, atrId),
- DebugUtil.dbg(result.flexibleExtras()), status);
+ addUnits(result.meta());
+ LOGGER.info(attemptId, "got status of ATR {}: '{}'", getAtrDebug(atrCollection, atrId), status);
AttemptState state = AttemptState.convert(status);
@@ -3611,19 +3645,16 @@ private Mono atrRollbackCompleteLocked(boolean isAppRollback, String prefi
private Mono rollbackDocsLocked(boolean isAppRollback, SpanWrapper span) {
return Mono.defer(() -> {
return Flux.fromIterable(stagedMutationsLocked)
- .parallel(UNSTAGING_PARALLELISM)
- .runOn(scheduler())
+ .publishOn(scheduler())
- .concatMap(staged -> {
+ .flatMap(staged -> {
switch (staged.type) {
case INSERT:
return rollbackStagedInsertLocked(isAppRollback, span, staged.collection, staged.id, staged.cas);
default:
return rollbackStagedReplaceOrRemoveLocked(isAppRollback, span, staged.collection, staged.id, staged.cas, staged.currentUserFlags);
}
- })
-
- .sequential()
+ }, UNSTAGING_PARALLELISM)
.doOnNext(v -> {
LOGGER.info(attemptId, "rollback - docs rolled back");
diff --git a/core-io/src/main/java/com/couchbase/client/core/transaction/CoreTransactionContext.java b/core-io/src/main/java/com/couchbase/client/core/transaction/CoreTransactionContext.java
index 6326e3158..a1e802153 100644
--- a/core-io/src/main/java/com/couchbase/client/core/transaction/CoreTransactionContext.java
+++ b/core-io/src/main/java/com/couchbase/client/core/transaction/CoreTransactionContext.java
@@ -57,7 +57,7 @@ public CoreTransactionContext(CoreContext coreContext,
CoreTransactionsCleanup cleanup) {
this.config = Objects.requireNonNull(config);
this.cleanup = Objects.requireNonNull(cleanup);
- RequestTracer tracer = coreContext.environment().requestTracer();
+ RequestTracer tracer = coreContext.coreResources().requestTracer();
SpanWrapper pspan = config.parentSpan().map(sp -> new SpanWrapper(sp)).orElse(null);
this.transactionSpan = SpanWrapper.create(tracer, TracingIdentifiers.TRANSACTION_OP, pspan);
SpanWrapperUtil.setAttributes(this.transactionSpan, null, null, null)
diff --git a/core-io/src/main/java/com/couchbase/client/core/transaction/CoreTransactionGetResult.java b/core-io/src/main/java/com/couchbase/client/core/transaction/CoreTransactionGetResult.java
index 6d20dbba5..79c755768 100644
--- a/core-io/src/main/java/com/couchbase/client/core/transaction/CoreTransactionGetResult.java
+++ b/core-io/src/main/java/com/couchbase/client/core/transaction/CoreTransactionGetResult.java
@@ -17,6 +17,7 @@
package com.couchbase.client.core.transaction;
import com.couchbase.client.core.annotation.Stability;
+import com.couchbase.client.core.api.kv.CoreSubdocGetResult;
import com.couchbase.client.core.deps.com.fasterxml.jackson.databind.JsonNode;
import com.couchbase.client.core.deps.com.fasterxml.jackson.databind.ObjectMapper;
import com.couchbase.client.core.io.CollectionIdentifier;
@@ -237,7 +238,7 @@ public static CoreTransactionGetResult createFrom(CoreTransactionGetResult doc,
@Stability.Internal
public static CoreTransactionGetResult createFrom(CollectionIdentifier collection,
String documentId,
- SubdocGetResponse doc) throws IOException {
+ CoreSubdocGetResult doc) throws IOException {
Optional atrId = Optional.empty();
Optional transactionId = Optional.empty();
Optional attemptId = Optional.empty();
@@ -259,16 +260,16 @@ public static CoreTransactionGetResult createFrom(CollectionIdentifier collectio
Optional op = Optional.empty();
// "txn.id"
- if (doc.values()[0].status().success()) {
- JsonNode id = MAPPER.readValue(doc.values()[0].value(), JsonNode.class);
+ if (doc.field(0).status().success()) {
+ JsonNode id = MAPPER.readValue(doc.field(0).value(), JsonNode.class);
transactionId = Optional.ofNullable(id.path("txn").textValue());
attemptId = Optional.ofNullable(id.path("atmpt").textValue());
operationId = Optional.ofNullable(id.path("op").textValue());
}
// "txn.atr"
- if (doc.values()[1].status().success()) {
- JsonNode atr = MAPPER.readValue(doc.values()[1].value(), JsonNode.class);
+ if (doc.field(1).status().success()) {
+ JsonNode atr = MAPPER.readValue(doc.field(1).value(), JsonNode.class);
atrId = Optional.ofNullable(atr.path("id").textValue());
atrBucketName = Optional.ofNullable(atr.path("bkt").textValue());
String scope = atr.path("scp").textValue();
@@ -287,24 +288,24 @@ public static CoreTransactionGetResult createFrom(CollectionIdentifier collectio
}
// "txn.op.type"
- if (doc.values()[2].status().success()) {
- op = Optional.of(Mapper.reader().readValue(doc.values()[2].value(), String.class));
+ if (doc.field(2).status().success()) {
+ op = Optional.of(Mapper.reader().readValue(doc.field(2).value(), String.class));
}
// "txn.op.stgd"
- if (doc.values()[3].status().success()) {
- byte[] raw = doc.values()[3].value();
+ if (doc.field(3).status().success()) {
+ byte[] raw = doc.field(3).value();
stagedContentJson = Optional.of(raw);
}
// "txn.op.crc32"
- if (doc.values()[4].status().success()) {
- crc32OfStaging = Optional.of(Mapper.reader().readValue(doc.values()[4].value(), String.class));
+ if (doc.field(4).status().success()) {
+ crc32OfStaging = Optional.of(Mapper.reader().readValue(doc.field(4).value(), String.class));
}
// "txn.restore"
- if (doc.values()[5].status().success()) {
- JsonNode restore = MAPPER.readValue(doc.values()[5].value(), JsonNode.class);
+ if (doc.field(5).status().success()) {
+ JsonNode restore = MAPPER.readValue(doc.field(5).value(), JsonNode.class);
casPreTxn = Optional.of(restore.path("CAS").textValue());
// Only present in 6.5+
revidPreTxn = Optional.of(restore.path("revid").textValue());
@@ -312,17 +313,17 @@ public static CoreTransactionGetResult createFrom(CollectionIdentifier collectio
}
// "txn.fc"
- if (doc.values()[6].status().success()) {
- JsonNode json = MAPPER.readValue(doc.values()[6].value(), JsonNode.class);
+ if (doc.field(6).status().success()) {
+ JsonNode json = MAPPER.readValue(doc.field(6).value(), JsonNode.class);
ForwardCompatibility fc = new ForwardCompatibility(json);
forwardCompatibility = Optional.of(fc);
}
- if (!doc.values()[7].status().success()) {
+ if (!doc.field(7).status().success()) {
throw new IllegalStateException("$document requested but not received");
}
// Read from $document
- JsonNode restore = MAPPER.readValue(doc.values()[7].value(), JsonNode.class);
+ JsonNode restore = MAPPER.readValue(doc.field(7).value(), JsonNode.class);
String casFromDocument = restore.path("CAS").textValue();
// Only present in 6.5+
String revidFromDocument = restore.path("revid").textValue();
@@ -333,14 +334,14 @@ public static CoreTransactionGetResult createFrom(CollectionIdentifier collectio
int currentUserFlags = restore.path("flags").intValue();
// "txn.op.bin"
- if (doc.values()[8].status().success()) {
- byte[] raw = doc.values()[8].value();
+ if (doc.field(8).status().success()) {
+ byte[] raw = doc.field(8).value();
stagedContentBinary = Optional.of(raw);
}
// "txn.aux"
- if (doc.values()[9].status().success()) {
- JsonNode aux = MAPPER.readValue(doc.values()[9].value(), JsonNode.class);
+ if (doc.field(9).status().success()) {
+ JsonNode aux = MAPPER.readValue(doc.field(9).value(), JsonNode.class);
if (aux.has("uf")) {
stagedUserFlags = Optional.of(aux.get("uf").intValue());
}
@@ -349,8 +350,8 @@ public static CoreTransactionGetResult createFrom(CollectionIdentifier collectio
byte[] bodyContent;
// body
- if (doc.values()[10].status().success()) {
- bodyContent = doc.values()[10].value();
+ if (doc.field(10).status().success()) {
+ bodyContent = doc.field(10).value();
}
else {
bodyContent = new byte[] {};
@@ -369,7 +370,7 @@ public static CoreTransactionGetResult createFrom(CollectionIdentifier collectio
revidPreTxn,
exptimePreTxn,
op,
- doc.isDeleted(),
+ doc.tombstone(),
crc32OfStaging,
forwardCompatibility,
operationId,
diff --git a/core-io/src/main/java/com/couchbase/client/core/transaction/CoreTransactionsReactive.java b/core-io/src/main/java/com/couchbase/client/core/transaction/CoreTransactionsReactive.java
index 69a49cc46..e6bbf90f9 100644
--- a/core-io/src/main/java/com/couchbase/client/core/transaction/CoreTransactionsReactive.java
+++ b/core-io/src/main/java/com/couchbase/client/core/transaction/CoreTransactionsReactive.java
@@ -28,15 +28,17 @@
import com.couchbase.client.core.cnc.RequestSpan;
import com.couchbase.client.core.cnc.TracingIdentifiers;
import com.couchbase.client.core.deps.com.fasterxml.jackson.databind.node.TextNode;
+import com.couchbase.client.core.error.transaction.RetryTransactionException;
+import com.couchbase.client.core.error.transaction.TransactionOperationFailedException;
import com.couchbase.client.core.error.transaction.internal.CoreTransactionCommitAmbiguousException;
import com.couchbase.client.core.error.transaction.internal.CoreTransactionExpiredException;
-import com.couchbase.client.core.error.transaction.TransactionOperationFailedException;
+import com.couchbase.client.core.error.transaction.internal.CoreTransactionFailedException;
import com.couchbase.client.core.msg.query.QueryChunkRow;
-import com.couchbase.client.core.node.NodeIdentifier;
import com.couchbase.client.core.retry.RetryReason;
import com.couchbase.client.core.retry.reactor.DefaultRetry;
import com.couchbase.client.core.retry.reactor.Jitter;
import com.couchbase.client.core.retry.reactor.RetryContext;
+import com.couchbase.client.core.topology.NodeIdentifier;
import com.couchbase.client.core.transaction.config.CoreMergedTransactionConfig;
import com.couchbase.client.core.transaction.config.CoreTransactionOptions;
import com.couchbase.client.core.transaction.config.CoreTransactionsConfig;
@@ -45,8 +47,6 @@
import com.couchbase.client.core.transaction.util.CoreTransactionAttemptContextHooks;
import com.couchbase.client.core.transaction.util.DebugUtil;
import com.couchbase.client.core.transaction.util.QueryUtil;
-import com.couchbase.client.core.error.transaction.RetryTransactionException;
-import com.couchbase.client.core.error.transaction.internal.CoreTransactionFailedException;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.annotation.Nullable;
diff --git a/core-io/src/main/java/com/couchbase/client/core/transaction/cleanup/ClientRecord.java b/core-io/src/main/java/com/couchbase/client/core/transaction/cleanup/ClientRecord.java
index 6813d7241..b4c7a933f 100644
--- a/core-io/src/main/java/com/couchbase/client/core/transaction/cleanup/ClientRecord.java
+++ b/core-io/src/main/java/com/couchbase/client/core/transaction/cleanup/ClientRecord.java
@@ -17,6 +17,7 @@
import com.couchbase.client.core.Core;
import com.couchbase.client.core.annotation.Stability;
+import com.couchbase.client.core.api.kv.CoreSubdocGetResult;
import com.couchbase.client.core.cnc.RequestTracer;
import com.couchbase.client.core.cnc.TracingIdentifiers;
import com.couchbase.client.core.deps.com.fasterxml.jackson.core.JsonProcessingException;
@@ -179,10 +180,10 @@ private Duration nonMutatingTimeout() {
return core.context().environment().timeoutConfig().kvTimeout();
}
- public static ClientRecordDetails parseClientRecord(SubdocGetResponse clientRecord, String clientUuid) {
+ public static ClientRecordDetails parseClientRecord(CoreSubdocGetResult clientRecord, String clientUuid) {
try {
- JsonNode records = Mapper.reader().readValue(clientRecord.values()[0].value(), JsonNode.class);
- JsonNode hlcRaw = Mapper.reader().readValue(clientRecord.values()[1].value(), JsonNode.class);
+ JsonNode records = Mapper.reader().readValue(clientRecord.field(0).value(), JsonNode.class);
+ JsonNode hlcRaw = Mapper.reader().readValue(clientRecord.field(1).value(), JsonNode.class);
ActiveTransactionRecord.ParsedHLC parsedHLC = new ActiveTransactionRecord.ParsedHLC(hlcRaw);
JsonNode clients = records.get("clients");
@@ -244,8 +245,9 @@ public static ClientRecordDetails parseClientRecord(SubdocGetResponse clientReco
}
}
- public Mono getClientRecord(CollectionIdentifier collection, @Nullable SpanWrapper span) {
+ public Mono getClientRecord(CollectionIdentifier collection, @Nullable SpanWrapper span) {
return TransactionKVHandler.lookupIn(core, collection, CLIENT_RECORD_DOC_ID, nonMutatingTimeout(), false, OptionsUtil.createClientContext("ClientRecord::getClientRecord"), span,
+ false,
Arrays.asList(
new SubdocGetRequest.Command(SubdocCommandType.GET, FIELD_RECORDS, true, 0),
new SubdocGetRequest.Command(SubdocCommandType.GET, "$vbucket.HLC", true, 1)
@@ -253,7 +255,7 @@ public Mono getClientRecord(CollectionIdentifier collection,
}
private RequestTracer tracer() {
- return core.context().environment().requestTracer();
+ return core.context().coreResources().requestTracer();
}
/*
diff --git a/core-io/src/main/java/com/couchbase/client/core/transaction/cleanup/LostCleanupDistributed.java b/core-io/src/main/java/com/couchbase/client/core/transaction/cleanup/LostCleanupDistributed.java
index 2f78d3488..ddb280e45 100644
--- a/core-io/src/main/java/com/couchbase/client/core/transaction/cleanup/LostCleanupDistributed.java
+++ b/core-io/src/main/java/com/couchbase/client/core/transaction/cleanup/LostCleanupDistributed.java
@@ -201,7 +201,7 @@ private static List atrsToHandle(int indexOfThisClient, int numActiveCli
}
private RequestTracer tracer() {
- return core.context().environment().requestTracer();
+ return core.context().coreResources().requestTracer();
}
/**
diff --git a/core-io/src/main/java/com/couchbase/client/core/transaction/cleanup/TransactionsCleaner.java b/core-io/src/main/java/com/couchbase/client/core/transaction/cleanup/TransactionsCleaner.java
index b928e32f6..24410b6df 100644
--- a/core-io/src/main/java/com/couchbase/client/core/transaction/cleanup/TransactionsCleaner.java
+++ b/core-io/src/main/java/com/couchbase/client/core/transaction/cleanup/TransactionsCleaner.java
@@ -17,6 +17,7 @@
import com.couchbase.client.core.Core;
import com.couchbase.client.core.annotation.Stability;
+import com.couchbase.client.core.api.kv.CoreSubdocGetResult;
import com.couchbase.client.core.cnc.Event;
import com.couchbase.client.core.cnc.RequestTracer;
import com.couchbase.client.core.cnc.TracingIdentifiers;
@@ -152,7 +153,7 @@ private Mono commitDocs(CoreTransactionLogger perEntryLog,
return hooks.beforeCommitDoc.apply(doc.id()) // Testing hook
.then(Mono.defer(() -> {
- if (lir.isDeleted()) {
+ if (lir.tombstone()) {
return TransactionKVHandler.insert(core, collection, doc.id(), content, doc.links().stagedUserFlags().orElse(CodecFlags.JSON_COMMON_FLAGS), kvDurableTimeout(),
req.durabilityLevel(), OptionsUtil.createClientContext("Cleaner::commitDocsInsert"), pspan);
} else {
@@ -163,7 +164,7 @@ private Mono commitDocs(CoreTransactionLogger perEntryLog,
);
return TransactionKVHandler.mutateIn(core, collection, doc.id(), kvDurableTimeout(),
false, false, false,
- lir.isDeleted(), false, doc.cas(), doc.links().stagedUserFlags().orElse(CodecFlags.JSON_COMMON_FLAGS),
+ lir.tombstone(), false, doc.cas(), doc.links().stagedUserFlags().orElse(CodecFlags.JSON_COMMON_FLAGS),
req.durabilityLevel(), OptionsUtil.createClientContext("Cleaner::commitDocs"), pspan,
commands);
}
@@ -192,7 +193,7 @@ private Mono removeTxnLinks(CoreTransactionLogger perEntryLog,
.then(TransactionKVHandler.mutateIn(core, collectionIdentifier, doc.id(), kvDurableTimeout(),
false, false, false,
- lir.isDeleted(), false, doc.cas(), doc.userFlags(),
+ lir.tombstone(), false, doc.cas(), doc.userFlags(),
req.durabilityLevel(), OptionsUtil.createClientContext("Cleaner::removeTxnLinks"), pspan, Arrays.asList(
new SubdocMutateRequest.Command(SubdocCommandType.DELETE, TransactionFields.TRANSACTION_INTERFACE_PREFIX_ONLY, Bytes.EMPTY_BYTE_ARRAY, false, true, false, 0)
)))
@@ -240,7 +241,7 @@ private Mono removeDocs(CoreTransactionLogger perEntryLog,
return hooks.beforeRemoveDoc.apply(doc.id())
.then(Mono.defer(() -> {
- if (lir.isDeleted()) {
+ if (lir.tombstone()) {
return TransactionKVHandler.mutateIn(core, collection, doc.id(), kvDurableTimeout(),
false, false, false,
true, false, doc.cas(), doc.userFlags(),
@@ -266,7 +267,7 @@ private Mono doPerDoc(CoreTransactionLogger perEntryLog,
List