diff --git a/.github/workflows/hermetic_library_generation.yaml b/.github/workflows/hermetic_library_generation.yaml
index 604b674bad9..c1be8aca557 100644
--- a/.github/workflows/hermetic_library_generation.yaml
+++ b/.github/workflows/hermetic_library_generation.yaml
@@ -37,7 +37,7 @@ jobs:
with:
fetch-depth: 0
token: ${{ secrets.CLOUD_JAVA_BOT_TOKEN }}
- - uses: googleapis/sdk-platform-java/.github/scripts@v2.51.0
+ - uses: googleapis/sdk-platform-java/.github/scripts@v2.51.1
if: env.SHOULD_RUN == 'true'
with:
base_ref: ${{ github.base_ref }}
diff --git a/.github/workflows/unmanaged_dependency_check.yaml b/.github/workflows/unmanaged_dependency_check.yaml
index f62b0af0eb1..6bf8adf0b53 100644
--- a/.github/workflows/unmanaged_dependency_check.yaml
+++ b/.github/workflows/unmanaged_dependency_check.yaml
@@ -17,6 +17,6 @@ jobs:
# repository
.kokoro/build.sh
- name: Unmanaged dependency check
- uses: googleapis/sdk-platform-java/java-shared-dependencies/unmanaged-dependency-check@google-cloud-shared-dependencies/v3.41.0
+ uses: googleapis/sdk-platform-java/java-shared-dependencies/unmanaged-dependency-check@google-cloud-shared-dependencies/v3.41.1
with:
bom-path: google-cloud-spanner-bom/pom.xml
diff --git a/.kokoro/presubmit/graalvm-native-17.cfg b/.kokoro/presubmit/graalvm-native-17.cfg
index 76ca3ee6daf..9705694f834 100644
--- a/.kokoro/presubmit/graalvm-native-17.cfg
+++ b/.kokoro/presubmit/graalvm-native-17.cfg
@@ -3,7 +3,7 @@
# Configure the docker image for kokoro-trampoline.
env_vars: {
key: "TRAMPOLINE_IMAGE"
- value: "gcr.io/cloud-devrel-public-resources/graalvm_sdk_platform_b:3.41.0"
+ value: "gcr.io/cloud-devrel-public-resources/graalvm_sdk_platform_b:3.41.1"
}
env_vars: {
diff --git a/.kokoro/presubmit/graalvm-native.cfg b/.kokoro/presubmit/graalvm-native.cfg
index 644bb62ce7d..b070666b48e 100644
--- a/.kokoro/presubmit/graalvm-native.cfg
+++ b/.kokoro/presubmit/graalvm-native.cfg
@@ -3,7 +3,7 @@
# Configure the docker image for kokoro-trampoline.
env_vars: {
key: "TRAMPOLINE_IMAGE"
- value: "gcr.io/cloud-devrel-public-resources/graalvm_sdk_platform_a:3.41.0"
+ value: "gcr.io/cloud-devrel-public-resources/graalvm_sdk_platform_a:3.41.1"
}
env_vars: {
diff --git a/README.md b/README.md
index a64487c221e..0718c5d7de8 100644
--- a/README.md
+++ b/README.md
@@ -41,7 +41,7 @@ If you are using Maven without the BOM, add this to your dependencies:
com.google.cloud
google-cloud-spanner
- 6.81.1
+ 6.84.0
```
diff --git a/benchmarks/pom.xml b/benchmarks/pom.xml
index 27f32c0e9c8..a1d0bf57483 100644
--- a/benchmarks/pom.xml
+++ b/benchmarks/pom.xml
@@ -92,7 +92,7 @@
com.google.cloud
google-cloud-spanner
- 6.81.1
+ 6.84.0
commons-cli
diff --git a/generation_config.yaml b/generation_config.yaml
index 05aa6491e91..28f408df692 100644
--- a/generation_config.yaml
+++ b/generation_config.yaml
@@ -1,5 +1,5 @@
gapic_generator_version: 2.51.0
-googleapis_commitish: 52e410823122cf44d265c3beecb86c773db248a2
+googleapis_commitish: 00196e2a68b16a864c57db2e870822875a7f1198
libraries_bom_version: 26.52.0
libraries:
- api_shortname: spanner
diff --git a/google-cloud-spanner-bom/pom.xml b/google-cloud-spanner-bom/pom.xml
index 0416dc94151..ddd725c1473 100644
--- a/google-cloud-spanner-bom/pom.xml
+++ b/google-cloud-spanner-bom/pom.xml
@@ -8,7 +8,7 @@
com.google.cloud
sdk-platform-java-config
- 3.41.0
+ 3.41.1
Google Cloud Spanner BOM
diff --git a/google-cloud-spanner/pom.xml b/google-cloud-spanner/pom.xml
index 2e93e6e5227..35616131fea 100644
--- a/google-cloud-spanner/pom.xml
+++ b/google-cloud-spanner/pom.xml
@@ -270,7 +270,7 @@
com.google.api.grpc
proto-google-cloud-monitoring-v3
- 3.55.0
+ 3.57.0
com.google.auth
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractReadContext.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractReadContext.java
index abcaa41e32c..ea21266e19d 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractReadContext.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractReadContext.java
@@ -58,6 +58,7 @@
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicLong;
+import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
@@ -67,6 +68,7 @@
*/
abstract class AbstractReadContext
implements ReadContext, AbstractResultSet.Listener, SessionTransaction {
+ private static final Logger logger = Logger.getLogger(AbstractReadContext.class.getName());
abstract static class Builder, T extends AbstractReadContext> {
private SessionImpl session;
@@ -797,7 +799,6 @@ CloseableIterator startStream(
isRouteToLeader());
session.markUsed(clock.instant());
stream.setCall(call, request.getTransaction().hasBegin());
- call.request(prefetchChunks);
return stream;
}
@@ -952,6 +953,15 @@ ResultSet readInternalWithOptions(
} else if (defaultDirectedReadOptions != null) {
builder.setDirectedReadOptions(defaultDirectedReadOptions);
}
+ if (readOptions.hasLockHint()) {
+ if (isReadOnly()) {
+ logger.warning(
+ "Lock hint is only supported for ReadWrite transactions. "
+ + "Overriding lock hint to default unspecified.");
+ } else {
+ builder.setLockHint(readOptions.lockHint());
+ }
+ }
final int prefetchChunks =
readOptions.hasPrefetchChunks() ? readOptions.prefetchChunks() : defaultPrefetchChunks;
ResumableStreamIterator stream =
@@ -992,7 +1002,6 @@ CloseableIterator startStream(
isRouteToLeader());
session.markUsed(clock.instant());
stream.setCall(call, /* withBeginTransaction = */ builder.getTransaction().hasBegin());
- call.request(prefetchChunks);
return stream;
}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java
index 3dca970f96e..f9f5062d201 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java
@@ -158,6 +158,9 @@ interface CloseableIterator extends Iterator {
default boolean initiateStreaming(AsyncResultSet.StreamMessageListener streamMessageListener) {
return false;
}
+
+ /** it requests the initial prefetch chunks from gRPC stream */
+ default void requestPrefetchChunks() {};
}
static double valueProtoToFloat64(com.google.protobuf.Value proto) {
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/GrpcStreamIterator.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/GrpcStreamIterator.java
index 60a52b78f25..e4196e2ac63 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/GrpcStreamIterator.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/GrpcStreamIterator.java
@@ -16,6 +16,7 @@
package com.google.cloud.spanner;
+import com.google.api.core.InternalApi;
import com.google.api.gax.rpc.ApiCallContext;
import com.google.cloud.spanner.AbstractResultSet.CloseableIterator;
import com.google.cloud.spanner.spi.v1.SpannerRpc;
@@ -39,6 +40,7 @@ class GrpcStreamIterator extends AbstractIterator
implements CloseableIterator {
private static final Logger logger = Logger.getLogger(GrpcStreamIterator.class.getName());
static final PartialResultSet END_OF_STREAM = PartialResultSet.newBuilder().build();
+ private final int prefetchChunks;
private AsyncResultSet.StreamMessageListener streamMessageListener;
private final ConsumerImpl consumer;
@@ -60,6 +62,7 @@ class GrpcStreamIterator extends AbstractIterator
GrpcStreamIterator(
Statement statement, int prefetchChunks, boolean cancelQueryWhenClientIsClosed) {
this.statement = statement;
+ this.prefetchChunks = prefetchChunks;
this.consumer = new ConsumerImpl(cancelQueryWhenClientIsClosed);
// One extra to allow for END_OF_STREAM message.
this.stream = new LinkedBlockingQueue<>(prefetchChunks + 1);
@@ -102,6 +105,13 @@ public void close(@Nullable String message) {
}
}
+ @Override
+ @InternalApi
+ public void requestPrefetchChunks() {
+ Preconditions.checkState(call != null, "The StreamingCall object is not initialized");
+ call.request(prefetchChunks);
+ }
+
@Override
public boolean isWithBeginTransaction() {
return withBeginTransaction;
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java
index 9c3257586fb..c062e89ec2b 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java
@@ -18,6 +18,7 @@
import com.google.common.base.Preconditions;
import com.google.spanner.v1.DirectedReadOptions;
+import com.google.spanner.v1.ReadRequest.LockHint;
import com.google.spanner.v1.ReadRequest.OrderBy;
import com.google.spanner.v1.RequestOptions.Priority;
import java.io.Serializable;
@@ -75,6 +76,25 @@ public static RpcOrderBy fromProto(OrderBy proto) {
}
}
+ public enum RpcLockHint {
+ UNSPECIFIED(LockHint.LOCK_HINT_UNSPECIFIED),
+ SHARED(LockHint.LOCK_HINT_SHARED),
+ EXCLUSIVE(LockHint.LOCK_HINT_EXCLUSIVE);
+
+ private final LockHint proto;
+
+ RpcLockHint(LockHint proto) {
+ this.proto = Preconditions.checkNotNull(proto);
+ }
+
+ public static RpcLockHint fromProto(LockHint proto) {
+ for (RpcLockHint e : RpcLockHint.values()) {
+ if (e.proto.equals(proto)) return e;
+ }
+ return RpcLockHint.UNSPECIFIED;
+ }
+ }
+
/** Marker interface to mark options applicable to both Read and Query operations */
public interface ReadAndQueryOption extends ReadOption, QueryOption {}
@@ -160,6 +180,10 @@ public static ReadOption orderBy(RpcOrderBy orderBy) {
return new OrderByOption(orderBy);
}
+ public static ReadOption lockHint(RpcLockHint orderBy) {
+ return new LockHintOption(orderBy);
+ }
+
/**
* Specifying this will allow the client to prefetch up to {@code prefetchChunks} {@code
* PartialResultSet} chunks for read and query. The data size of each chunk depends on the server
@@ -469,6 +493,7 @@ void appendToOptions(Options options) {
private DirectedReadOptions directedReadOptions;
private DecodeMode decodeMode;
private RpcOrderBy orderBy;
+ private RpcLockHint lockHint;
// Construction is via factory methods below.
private Options() {}
@@ -605,6 +630,14 @@ OrderBy orderBy() {
return orderBy == null ? null : orderBy.proto;
}
+ boolean hasLockHint() {
+ return lockHint != null;
+ }
+
+ LockHint lockHint() {
+ return lockHint == null ? null : lockHint.proto;
+ }
+
@Override
public String toString() {
StringBuilder b = new StringBuilder();
@@ -661,6 +694,9 @@ public String toString() {
if (orderBy != null) {
b.append("orderBy: ").append(orderBy).append(' ');
}
+ if (lockHint != null) {
+ b.append("lockHint: ").append(lockHint).append(' ');
+ }
return b.toString();
}
@@ -700,7 +736,8 @@ public boolean equals(Object o) {
&& Objects.equals(withExcludeTxnFromChangeStreams(), that.withExcludeTxnFromChangeStreams())
&& Objects.equals(dataBoostEnabled(), that.dataBoostEnabled())
&& Objects.equals(directedReadOptions(), that.directedReadOptions())
- && Objects.equals(orderBy(), that.orderBy());
+ && Objects.equals(orderBy(), that.orderBy())
+ && Objects.equals(lockHint(), that.lockHint());
}
@Override
@@ -760,6 +797,9 @@ public int hashCode() {
if (orderBy != null) {
result = 31 * result + orderBy.hashCode();
}
+ if (lockHint != null) {
+ result = 31 * result + lockHint.hashCode();
+ }
return result;
}
@@ -853,6 +893,19 @@ void appendToOptions(Options options) {
}
}
+ static class LockHintOption extends InternalOption implements ReadOption {
+ private final RpcLockHint lockHint;
+
+ LockHintOption(RpcLockHint lockHint) {
+ this.lockHint = lockHint;
+ }
+
+ @Override
+ void appendToOptions(Options options) {
+ options.lockHint = lockHint;
+ }
+ }
+
static final class DataBoostQueryOption extends InternalOption implements ReadAndQueryOption {
private final Boolean dataBoostEnabled;
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResumableStreamIterator.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResumableStreamIterator.java
index 39165da2d38..793f3bcbe32 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResumableStreamIterator.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResumableStreamIterator.java
@@ -326,6 +326,7 @@ private void startGrpcStreaming() {
// When start a new stream set the Span as current to make the gRPC Span a child of
// this Span.
stream = checkNotNull(startStream(resumeToken, streamMessageListener));
+ stream.requestPrefetchChunks();
}
}
}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java
index 6e96828b495..1645ad464ba 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java
@@ -71,9 +71,13 @@
import io.grpc.ExperimentalApi;
import io.grpc.ManagedChannelBuilder;
import io.grpc.MethodDescriptor;
+import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts;
+import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder;
+import io.grpc.netty.shaded.io.netty.handler.ssl.SslContext;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.common.Attributes;
+import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
@@ -952,6 +956,7 @@ public static class Builder
private boolean enableEndToEndTracing = SpannerOptions.environment.isEnableEndToEndTracing();
private boolean enableBuiltInMetrics = SpannerOptions.environment.isEnableBuiltInMetrics();
private String monitoringHost = SpannerOptions.environment.getMonitoringHost();
+ private SslContext mTLSContext = null;
private static String createCustomClientLibToken(String token) {
return token + " " + ServiceOptions.getGoogApiClientLibName();
@@ -1485,6 +1490,27 @@ public Builder setEmulatorHost(String emulatorHost) {
return this;
}
+ /**
+ * Configures mTLS authentication using the provided client certificate and key files. mTLS is
+ * only supported for external spanner hosts.
+ *
+ * @param clientCertificate Path to the client certificate file.
+ * @param clientCertificateKey Path to the client private key file.
+ * @throws SpannerException If an error occurs while configuring the mTLS context
+ */
+ @ExperimentalApi("https://github.com/googleapis/java-spanner/pull/3574")
+ public Builder useClientCert(String clientCertificate, String clientCertificateKey) {
+ try {
+ this.mTLSContext =
+ GrpcSslContexts.forClient()
+ .keyManager(new File(clientCertificate), new File(clientCertificateKey))
+ .build();
+ } catch (Exception e) {
+ throw SpannerExceptionFactory.asSpannerException(e);
+ }
+ return this;
+ }
+
/**
* Sets OpenTelemetry object to be used for Spanner Metrics and Traces. GlobalOpenTelemetry will
* be used as fallback if this options is not set.
@@ -1594,6 +1620,15 @@ public SpannerOptions build() {
// As we are using plain text, we should never send any credentials.
this.setCredentials(NoCredentials.getInstance());
}
+ if (mTLSContext != null) {
+ this.setChannelConfigurator(
+ builder -> {
+ if (builder instanceof NettyChannelBuilder) {
+ ((NettyChannelBuilder) builder).sslContext(mTLSContext);
+ }
+ return builder;
+ });
+ }
if (this.numChannels == null) {
this.numChannels =
this.grpcGcpExtensionEnabled ? GRPC_GCP_ENABLED_DEFAULT_CHANNELS : DEFAULT_CHANNELS;
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TraceWrapper.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TraceWrapper.java
index 3e01eec1fb2..606a54fe8b7 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TraceWrapper.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TraceWrapper.java
@@ -16,6 +16,7 @@
package com.google.cloud.spanner;
+import com.google.api.gax.core.GaxProperties;
import com.google.cloud.spanner.Options.TagOption;
import com.google.cloud.spanner.Options.TransactionOption;
import com.google.cloud.spanner.SpannerOptions.TracingFramework;
@@ -46,6 +47,12 @@ class TraceWrapper {
private static final AttributeKey> DB_STATEMENT_ARRAY_KEY =
AttributeKey.stringArrayKey("db.statement");
private static final AttributeKey DB_TABLE_NAME_KEY = AttributeKey.stringKey("db.table");
+ private static final AttributeKey GCP_CLIENT_SERVICE_KEY =
+ AttributeKey.stringKey("gcp.client.service");
+ private static final AttributeKey GCP_CLIENT_VERSION_KEY =
+ AttributeKey.stringKey("gcp.client.version");
+ private static final AttributeKey GCP_CLIENT_REPO_KEY =
+ AttributeKey.stringKey("gcp.client.repo");
private static final AttributeKey THREAD_NAME_KEY = AttributeKey.stringKey("thread.name");
private final Tracer openCensusTracer;
@@ -204,6 +211,9 @@ Attributes createCommonAttributes(DatabaseId db) {
AttributesBuilder builder = Attributes.builder();
builder.put(DB_NAME_KEY, db.getDatabase());
builder.put(INSTANCE_NAME_KEY, db.getInstanceId().getInstance());
+ builder.put(GCP_CLIENT_SERVICE_KEY, "spanner");
+ builder.put(GCP_CLIENT_REPO_KEY, "googleapis/java-spanner");
+ builder.put(GCP_CLIENT_VERSION_KEY, GaxProperties.getLibraryVersion(TraceWrapper.class));
return builder.build();
}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java
index 6e991816ab6..d205699bb9e 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java
@@ -20,6 +20,8 @@
import static com.google.cloud.spanner.connection.ConnectionProperties.AUTO_CONFIG_EMULATOR;
import static com.google.cloud.spanner.connection.ConnectionProperties.AUTO_PARTITION_MODE;
import static com.google.cloud.spanner.connection.ConnectionProperties.CHANNEL_PROVIDER;
+import static com.google.cloud.spanner.connection.ConnectionProperties.CLIENT_CERTIFICATE;
+import static com.google.cloud.spanner.connection.ConnectionProperties.CLIENT_KEY;
import static com.google.cloud.spanner.connection.ConnectionProperties.CREDENTIALS_PROVIDER;
import static com.google.cloud.spanner.connection.ConnectionProperties.CREDENTIALS_URL;
import static com.google.cloud.spanner.connection.ConnectionProperties.DATABASE_ROLE;
@@ -225,6 +227,8 @@ public String[] getValidValues() {
static final boolean DEFAULT_USE_VIRTUAL_THREADS = false;
static final boolean DEFAULT_USE_VIRTUAL_GRPC_TRANSPORT_THREADS = false;
static final String DEFAULT_CREDENTIALS = null;
+ static final String DEFAULT_CLIENT_CERTIFICATE = null;
+ static final String DEFAULT_CLIENT_KEY = null;
static final String DEFAULT_OAUTH_TOKEN = null;
static final Integer DEFAULT_MIN_SESSIONS = null;
static final Integer DEFAULT_MAX_SESSIONS = null;
@@ -263,6 +267,10 @@ public String[] getValidValues() {
private static final String DEFAULT_EMULATOR_HOST = "http://localhost:9010";
/** Use plain text is only for local testing purposes. */
static final String USE_PLAIN_TEXT_PROPERTY_NAME = "usePlainText";
+ /** Client certificate path to establish mTLS */
+ static final String CLIENT_CERTIFICATE_PROPERTY_NAME = "clientCertificate";
+ /** Client key path to establish mTLS */
+ static final String CLIENT_KEY_PROPERTY_NAME = "clientKey";
/** Name of the 'autocommit' connection property. */
public static final String AUTOCOMMIT_PROPERTY_NAME = "autocommit";
/** Name of the 'readonly' connection property. */
@@ -434,6 +442,12 @@ static boolean isEnableTransactionalConnectionStateForPostgreSQL() {
USE_PLAIN_TEXT_PROPERTY_NAME,
"Use a plain text communication channel (i.e. non-TLS) for communicating with the server (true/false). Set this value to true for communication with the Cloud Spanner emulator.",
DEFAULT_USE_PLAIN_TEXT),
+ ConnectionProperty.createStringProperty(
+ CLIENT_CERTIFICATE_PROPERTY_NAME,
+ "Specifies the file path to the client certificate required for establishing an mTLS connection."),
+ ConnectionProperty.createStringProperty(
+ CLIENT_KEY_PROPERTY_NAME,
+ "Specifies the file path to the client private key required for establishing an mTLS connection."),
ConnectionProperty.createStringProperty(
USER_AGENT_PROPERTY_NAME,
"The custom user-agent property name to use when communicating with Cloud Spanner. This property is intended for internal library usage, and should not be set by applications."),
@@ -1291,6 +1305,14 @@ boolean isUsePlainText() {
|| getInitialConnectionPropertyValue(USE_PLAIN_TEXT);
}
+ String getClientCertificate() {
+ return getInitialConnectionPropertyValue(CLIENT_CERTIFICATE);
+ }
+
+ String getClientCertificateKey() {
+ return getInitialConnectionPropertyValue(CLIENT_KEY);
+ }
+
/**
* The (custom) user agent string to use for this connection. If null
, then the
* default JDBC user agent string will be used.
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionProperties.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionProperties.java
index fd40efa8f4a..6f2628e5a04 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionProperties.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionProperties.java
@@ -22,6 +22,8 @@
import static com.google.cloud.spanner.connection.ConnectionOptions.AUTO_BATCH_DML_UPDATE_COUNT_VERIFICATION_PROPERTY_NAME;
import static com.google.cloud.spanner.connection.ConnectionOptions.AUTO_PARTITION_MODE_PROPERTY_NAME;
import static com.google.cloud.spanner.connection.ConnectionOptions.CHANNEL_PROVIDER_PROPERTY_NAME;
+import static com.google.cloud.spanner.connection.ConnectionOptions.CLIENT_CERTIFICATE_PROPERTY_NAME;
+import static com.google.cloud.spanner.connection.ConnectionOptions.CLIENT_KEY_PROPERTY_NAME;
import static com.google.cloud.spanner.connection.ConnectionOptions.CREDENTIALS_PROPERTY_NAME;
import static com.google.cloud.spanner.connection.ConnectionOptions.CREDENTIALS_PROVIDER_PROPERTY_NAME;
import static com.google.cloud.spanner.connection.ConnectionOptions.DATABASE_ROLE_PROPERTY_NAME;
@@ -33,6 +35,8 @@
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_AUTO_BATCH_DML_UPDATE_COUNT_VERIFICATION;
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_AUTO_PARTITION_MODE;
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_CHANNEL_PROVIDER;
+import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_CLIENT_CERTIFICATE;
+import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_CLIENT_KEY;
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_CREDENTIALS;
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_DATABASE_ROLE;
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_DATA_BOOST_ENABLED;
@@ -192,6 +196,20 @@ public class ConnectionProperties {
BooleanConverter.INSTANCE,
Context.STARTUP);
+ static final ConnectionProperty CLIENT_CERTIFICATE =
+ create(
+ CLIENT_CERTIFICATE_PROPERTY_NAME,
+ "Specifies the file path to the client certificate required for establishing an mTLS connection.",
+ DEFAULT_CLIENT_CERTIFICATE,
+ StringValueConverter.INSTANCE,
+ Context.STARTUP);
+ static final ConnectionProperty CLIENT_KEY =
+ create(
+ CLIENT_KEY_PROPERTY_NAME,
+ "Specifies the file path to the client private key required for establishing an mTLS connection.",
+ DEFAULT_CLIENT_KEY,
+ StringValueConverter.INSTANCE,
+ Context.STARTUP);
static final ConnectionProperty CREDENTIALS_URL =
create(
CREDENTIALS_PROPERTY_NAME,
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java
index 81246e41938..9558947156c 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java
@@ -161,6 +161,8 @@ static class SpannerPoolKey {
private final Boolean enableExtendedTracing;
private final Boolean enableApiTracing;
private final boolean enableEndToEndTracing;
+ private final String clientCertificate;
+ private final String clientCertificateKey;
@VisibleForTesting
static SpannerPoolKey of(ConnectionOptions options) {
@@ -192,6 +194,8 @@ private SpannerPoolKey(ConnectionOptions options) throws IOException {
this.enableExtendedTracing = options.isEnableExtendedTracing();
this.enableApiTracing = options.isEnableApiTracing();
this.enableEndToEndTracing = options.isEndToEndTracingEnabled();
+ this.clientCertificate = options.getClientCertificate();
+ this.clientCertificateKey = options.getClientCertificateKey();
}
@Override
@@ -214,7 +218,9 @@ public boolean equals(Object o) {
&& Objects.equals(this.openTelemetry, other.openTelemetry)
&& Objects.equals(this.enableExtendedTracing, other.enableExtendedTracing)
&& Objects.equals(this.enableApiTracing, other.enableApiTracing)
- && Objects.equals(this.enableEndToEndTracing, other.enableEndToEndTracing);
+ && Objects.equals(this.enableEndToEndTracing, other.enableEndToEndTracing)
+ && Objects.equals(this.clientCertificate, other.clientCertificate)
+ && Objects.equals(this.clientCertificateKey, other.clientCertificateKey);
}
@Override
@@ -233,7 +239,9 @@ public int hashCode() {
this.openTelemetry,
this.enableExtendedTracing,
this.enableApiTracing,
- this.enableEndToEndTracing);
+ this.enableEndToEndTracing,
+ this.clientCertificate,
+ this.clientCertificateKey);
}
}
@@ -393,6 +401,9 @@ Spanner createSpanner(SpannerPoolKey key, ConnectionOptions options) {
// Set a custom channel configurator to allow http instead of https.
builder.setChannelConfigurator(ManagedChannelBuilder::usePlaintext);
}
+ if (key.clientCertificate != null && key.clientCertificateKey != null) {
+ builder.useClientCert(key.clientCertificate, key.clientCertificateKey);
+ }
if (options.getConfigurator() != null) {
options.getConfigurator().configure(builder);
}
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AbstractReadContextTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AbstractReadContextTest.java
index ce7d6b300d1..8b53bd7efff 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AbstractReadContextTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AbstractReadContextTest.java
@@ -34,6 +34,7 @@
import com.google.spanner.v1.ExecuteSqlRequest.QueryMode;
import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions;
import com.google.spanner.v1.ReadRequest;
+import com.google.spanner.v1.ReadRequest.LockHint;
import com.google.spanner.v1.ReadRequest.OrderBy;
import com.google.spanner.v1.RequestOptions;
import com.google.spanner.v1.RequestOptions.Priority;
@@ -241,6 +242,21 @@ public void testGetReadRequestBuilderWithOrderBy() {
assertEquals(OrderBy.ORDER_BY_NO_ORDER, request.getOrderBy());
}
+ @Test
+ public void testGetReadRequestBuilderWithLockHint() {
+ ReadRequest request =
+ ReadRequest.newBuilder()
+ .setSession(
+ SessionName.of("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]").toString())
+ .setTransaction(TransactionSelector.newBuilder().build())
+ .setTable("table110115790")
+ .setIndex("index100346066")
+ .addAllColumns(new ArrayList())
+ .setLockHintValue(2)
+ .build();
+ assertEquals(LockHint.LOCK_HINT_EXCLUSIVE, request.getLockHint());
+ }
+
@Test
public void testGetExecuteBatchDmlRequestBuilderWithPriority() {
ExecuteBatchDmlRequest.Builder request =
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java
index 1ee60509d55..1523a62fd7e 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java
@@ -52,6 +52,7 @@
import com.google.cloud.spanner.AsyncTransactionManager.TransactionContextFuture;
import com.google.cloud.spanner.MockSpannerServiceImpl.SimulatedExecutionTime;
import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult;
+import com.google.cloud.spanner.Options.RpcLockHint;
import com.google.cloud.spanner.Options.RpcOrderBy;
import com.google.cloud.spanner.Options.RpcPriority;
import com.google.cloud.spanner.Options.TransactionOption;
@@ -90,6 +91,7 @@
import com.google.spanner.v1.ExecuteSqlRequest.QueryMode;
import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions;
import com.google.spanner.v1.ReadRequest;
+import com.google.spanner.v1.ReadRequest.LockHint;
import com.google.spanner.v1.ReadRequest.OrderBy;
import com.google.spanner.v1.RequestOptions.Priority;
import com.google.spanner.v1.ResultSetMetadata;
@@ -1754,6 +1756,53 @@ public void testExecuteReadWithOrderByOption() {
assertEquals(OrderBy.ORDER_BY_NO_ORDER, request.getOrderBy());
}
+ @Test
+ public void testUnsupportedTransactionWithLockHintOption() {
+ DatabaseClient client =
+ spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE));
+ try (ResultSet resultSet =
+ client
+ .singleUse()
+ .read(
+ READ_TABLE_NAME,
+ KeySet.singleKey(Key.of(1L)),
+ READ_COLUMN_NAMES,
+ Options.lockHint(RpcLockHint.EXCLUSIVE))) {
+ consumeResults(resultSet);
+ }
+
+ List requests = mockSpanner.getRequestsOfType(ReadRequest.class);
+ assertThat(requests).hasSize(1);
+ ReadRequest request = requests.get(0);
+ // lock hint is only supported in ReadWriteTransaction
+ assertEquals(LockHint.LOCK_HINT_UNSPECIFIED, request.getLockHint());
+ }
+
+ @Test
+ public void testReadWriteTransactionWithLockHint() {
+ DatabaseClient client =
+ spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE));
+
+ TransactionRunner runner = client.readWriteTransaction();
+ runner.run(
+ transaction -> {
+ try (ResultSet resultSet =
+ transaction.read(
+ READ_TABLE_NAME,
+ KeySet.singleKey(Key.of(1L)),
+ READ_COLUMN_NAMES,
+ Options.lockHint(RpcLockHint.EXCLUSIVE))) {
+ consumeResults(resultSet);
+ }
+ return null;
+ });
+
+ List requests = mockSpanner.getRequestsOfType(ReadRequest.class);
+ assertThat(requests).hasSize(1);
+ ReadRequest request = requests.get(0);
+ assertEquals(LockHint.LOCK_HINT_EXCLUSIVE, request.getLockHint());
+ }
+
@Test
public void testExecuteReadWithDirectedReadOptions() {
DatabaseClient client =
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/OpenTelemetrySpanTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/OpenTelemetrySpanTest.java
index 27e9cdb31f9..cbc449815e7 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/OpenTelemetrySpanTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/OpenTelemetrySpanTest.java
@@ -21,6 +21,7 @@
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
+import com.google.api.gax.core.GaxProperties;
import com.google.api.gax.grpc.testing.LocalChannelProvider;
import com.google.cloud.NoCredentials;
import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult;
@@ -871,6 +872,13 @@ private static void verifySpans(List actualSpanItems, List expec
private static void verifyCommonAttributes(SpanData span) {
assertEquals(span.getAttributes().get(AttributeKey.stringKey("instance.name")), "my-instance");
assertEquals(span.getAttributes().get(AttributeKey.stringKey("db.name")), "my-database");
+ assertEquals(span.getAttributes().get(AttributeKey.stringKey("gcp.client.service")), "spanner");
+ assertEquals(
+ span.getAttributes().get(AttributeKey.stringKey("gcp.client.repo")),
+ "googleapis/java-spanner");
+ assertEquals(
+ span.getAttributes().get(AttributeKey.stringKey("gcp.client.version")),
+ GaxProperties.getLibraryVersion(TraceWrapper.class));
}
private static void verifyTableAttributes(SpanData span) {
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/OptionsTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/OptionsTest.java
index 38b7a121731..f391088589f 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/OptionsTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/OptionsTest.java
@@ -25,11 +25,13 @@
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
+import com.google.cloud.spanner.Options.RpcLockHint;
import com.google.cloud.spanner.Options.RpcOrderBy;
import com.google.cloud.spanner.Options.RpcPriority;
import com.google.spanner.v1.DirectedReadOptions;
import com.google.spanner.v1.DirectedReadOptions.IncludeReplicas;
import com.google.spanner.v1.DirectedReadOptions.ReplicaSelection;
+import com.google.spanner.v1.ReadRequest.LockHint;
import com.google.spanner.v1.ReadRequest.OrderBy;
import com.google.spanner.v1.RequestOptions.Priority;
import org.junit.Test;
@@ -83,7 +85,8 @@ public void allOptionsPresent() {
Options.prefetchChunks(1),
Options.dataBoostEnabled(true),
Options.directedRead(DIRECTED_READ_OPTIONS),
- Options.orderBy(RpcOrderBy.NO_ORDER));
+ Options.orderBy(RpcOrderBy.NO_ORDER),
+ Options.lockHint(Options.RpcLockHint.SHARED));
assertThat(options.hasLimit()).isTrue();
assertThat(options.limit()).isEqualTo(10);
assertThat(options.hasPrefetchChunks()).isTrue();
@@ -92,6 +95,7 @@ public void allOptionsPresent() {
assertTrue(options.dataBoostEnabled());
assertTrue(options.hasDirectedReadOptions());
assertTrue(options.hasOrderBy());
+ assertTrue(options.hasLockHint());
assertEquals(DIRECTED_READ_OPTIONS, options.directedReadOptions());
}
@@ -107,6 +111,7 @@ public void allOptionsAbsent() {
assertThat(options.hasDataBoostEnabled()).isFalse();
assertThat(options.hasDirectedReadOptions()).isFalse();
assertThat(options.hasOrderBy()).isFalse();
+ assertThat(options.hasLockHint()).isFalse();
assertNull(options.withExcludeTxnFromChangeStreams());
assertThat(options.toString()).isEqualTo("");
assertThat(options.equals(options)).isTrue();
@@ -189,7 +194,8 @@ public void readOptionsTest() {
Options.tag(tag),
Options.dataBoostEnabled(true),
Options.directedRead(DIRECTED_READ_OPTIONS),
- Options.orderBy(RpcOrderBy.NO_ORDER));
+ Options.orderBy(RpcOrderBy.NO_ORDER),
+ Options.lockHint(RpcLockHint.SHARED));
assertThat(options.toString())
.isEqualTo(
@@ -207,11 +213,15 @@ public void readOptionsTest() {
+ " "
+ "orderBy: "
+ RpcOrderBy.NO_ORDER
+ + " "
+ + "lockHint: "
+ + RpcLockHint.SHARED
+ " ");
assertThat(options.tag()).isEqualTo(tag);
assertEquals(dataBoost, options.dataBoostEnabled());
assertEquals(DIRECTED_READ_OPTIONS, options.directedReadOptions());
assertEquals(OrderBy.ORDER_BY_NO_ORDER, options.orderBy());
+ assertEquals(LockHint.LOCK_HINT_SHARED, options.lockHint());
}
@Test
@@ -373,6 +383,14 @@ public void testReadOptionsOrderBy() {
assertEquals("orderBy: " + orderBy + " ", options.toString());
}
+ @Test
+ public void testReadOptionsLockHint() {
+ RpcLockHint lockHint = RpcLockHint.SHARED;
+ Options options = Options.fromReadOptions(Options.lockHint(lockHint));
+ assertTrue(options.hasLockHint());
+ assertEquals("lockHint: " + lockHint + " ", options.toString());
+ }
+
@Test
public void testReadOptionsWithOrderByEquality() {
Options optionsWithNoOrderBy1 = Options.fromReadOptions(Options.orderBy(RpcOrderBy.NO_ORDER));
@@ -383,6 +401,19 @@ public void testReadOptionsWithOrderByEquality() {
assertFalse(optionsWithNoOrderBy1.equals(optionsWithPkOrder));
}
+ @Test
+ public void testReadOptionsWithLockHintEquality() {
+ Options optionsWithSharedLockHint1 =
+ Options.fromReadOptions(Options.lockHint(RpcLockHint.SHARED));
+ Options optionsWithSharedLockHint2 =
+ Options.fromReadOptions(Options.lockHint(RpcLockHint.SHARED));
+ assertEquals(optionsWithSharedLockHint1, optionsWithSharedLockHint2);
+
+ Options optionsWithExclusiveLock =
+ Options.fromReadOptions(Options.lockHint(RpcLockHint.EXCLUSIVE));
+ assertNotEquals(optionsWithSharedLockHint1, optionsWithExclusiveLock);
+ }
+
@Test
public void testQueryOptionsPriority() {
RpcPriority priority = RpcPriority.MEDIUM;
diff --git a/pom.xml b/pom.xml
index 9945c18137e..30f948fee40 100644
--- a/pom.xml
+++ b/pom.xml
@@ -14,7 +14,7 @@
com.google.cloud
sdk-platform-java-config
- 3.41.0
+ 3.41.1
diff --git a/samples/install-without-bom/pom.xml b/samples/install-without-bom/pom.xml
index 0d6406bd321..548ffdf8fd2 100644
--- a/samples/install-without-bom/pom.xml
+++ b/samples/install-without-bom/pom.xml
@@ -33,7 +33,7 @@
com.google.cloud
google-cloud-spanner
- 6.81.1
+ 6.84.0