diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ClientSideStatementValueConverters.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ClientSideStatementValueConverters.java index 1bea4306e1a..fd078c99b6a 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ClientSideStatementValueConverters.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ClientSideStatementValueConverters.java @@ -19,6 +19,8 @@ import static com.google.cloud.spanner.connection.ReadOnlyStalenessUtil.parseTimeUnit; import static com.google.cloud.spanner.connection.ReadOnlyStalenessUtil.toChronoUnit; +import com.google.api.gax.core.CredentialsProvider; +import com.google.cloud.spanner.Dialect; import com.google.cloud.spanner.ErrorCode; import com.google.cloud.spanner.Options.RpcPriority; import com.google.cloud.spanner.SpannerException; @@ -31,6 +33,8 @@ import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.spanner.v1.DirectedReadOptions; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; import java.time.Duration; import java.time.temporal.ChronoUnit; import java.util.Base64; @@ -73,7 +77,11 @@ private E get(String value) { /** Converter from string to {@link Boolean} */ static class BooleanConverter implements ClientSideStatementValueConverter { + static final BooleanConverter INSTANCE = new BooleanConverter(); + private BooleanConverter() {} + + /** Constructor that is needed for reflection. */ public BooleanConverter(String allowedValues) {} @Override @@ -142,7 +150,11 @@ public Boolean convert(String value) { /** Converter from string to a non-negative integer. */ static class NonNegativeIntegerConverter implements ClientSideStatementValueConverter { + static final NonNegativeIntegerConverter INSTANCE = new NonNegativeIntegerConverter(); + + private NonNegativeIntegerConverter() {} + /** Constructor needed for reflection. */ public NonNegativeIntegerConverter(String allowedValues) {} @Override @@ -167,6 +179,9 @@ public Integer convert(String value) { /** Converter from string to {@link Duration}. */ static class DurationConverter implements ClientSideStatementValueConverter { + static final DurationConverter INSTANCE = + new DurationConverter("('(\\d{1,19})(s|ms|us|ns)'|\\d{1,19}|NULL)"); + private final String resetValue; private final Pattern allowedValues; @@ -227,6 +242,10 @@ public PgDurationConverter(String allowedValues) { /** Converter from string to possible values for read only staleness ({@link TimestampBound}). */ static class ReadOnlyStalenessConverter implements ClientSideStatementValueConverter { + static final ReadOnlyStalenessConverter INSTANCE = + new ReadOnlyStalenessConverter( + "'((STRONG)|(MIN_READ_TIMESTAMP)[\\t ]+((\\d{4})-(\\d{2})-(\\d{2})([Tt](\\d{2}):(\\d{2}):(\\d{2})(\\.\\d{1,9})?)([Zz]|([+-])(\\d{2}):(\\d{2})))|(READ_TIMESTAMP)[\\t ]+((\\d{4})-(\\d{2})-(\\d{2})([Tt](\\d{2}):(\\d{2}):(\\d{2})(\\.\\d{1,9})?)([Zz]|([+-])(\\d{2}):(\\d{2})))|(MAX_STALENESS)[\\t ]+((\\d{1,19})(s|ms|us|ns))|(EXACT_STALENESS)[\\t ]+((\\d{1,19})(s|ms|us|ns)))'"); + private final Pattern allowedValues; private final CaseInsensitiveEnumMap values = new CaseInsensitiveEnumMap<>(Mode.class); @@ -337,9 +356,14 @@ public DirectedReadOptions convert(String value) { /** Converter for converting strings to {@link AutocommitDmlMode} values. */ static class AutocommitDmlModeConverter implements ClientSideStatementValueConverter { + static final AutocommitDmlModeConverter INSTANCE = new AutocommitDmlModeConverter(); + private final CaseInsensitiveEnumMap values = new CaseInsensitiveEnumMap<>(AutocommitDmlMode.class); + private AutocommitDmlModeConverter() {} + + /** Constructor needed for reflection. */ public AutocommitDmlModeConverter(String allowedValues) {} @Override @@ -353,7 +377,35 @@ public AutocommitDmlMode convert(String value) { } } + static class ConnectionStateTypeConverter + implements ClientSideStatementValueConverter { + static final ConnectionStateTypeConverter INSTANCE = new ConnectionStateTypeConverter(); + + private final CaseInsensitiveEnumMap values = + new CaseInsensitiveEnumMap<>(ConnectionState.Type.class); + + private ConnectionStateTypeConverter() {} + + /** Constructor that is needed for reflection. */ + public ConnectionStateTypeConverter(String allowedValues) {} + + @Override + public Class getParameterClass() { + return ConnectionState.Type.class; + } + + @Override + public ConnectionState.Type convert(String value) { + return values.get(value); + } + } + static class StringValueConverter implements ClientSideStatementValueConverter { + static final StringValueConverter INSTANCE = new StringValueConverter(); + + private StringValueConverter() {} + + /** Constructor needed for reflection. */ public StringValueConverter(String allowedValues) {} @Override @@ -481,6 +533,8 @@ public PgTransactionMode convert(String value) { /** Converter for converting strings to {@link RpcPriority} values. */ static class RpcPriorityConverter implements ClientSideStatementValueConverter { + static final RpcPriorityConverter INSTANCE = new RpcPriorityConverter("(HIGH|MEDIUM|LOW|NULL)"); + private final CaseInsensitiveEnumMap values = new CaseInsensitiveEnumMap<>(RpcPriority.class); private final Pattern allowedValues; @@ -512,9 +566,14 @@ public RpcPriority convert(String value) { /** Converter for converting strings to {@link SavepointSupport} values. */ static class SavepointSupportConverter implements ClientSideStatementValueConverter { + static final SavepointSupportConverter INSTANCE = new SavepointSupportConverter(); + private final CaseInsensitiveEnumMap values = new CaseInsensitiveEnumMap<>(SavepointSupport.class); + private SavepointSupportConverter() {} + + /** Constructor needed for reflection. */ public SavepointSupportConverter(String allowedValues) {} @Override @@ -528,6 +587,30 @@ public SavepointSupport convert(String value) { } } + /** Converter for converting strings to {@link DdlInTransactionMode} values. */ + static class DdlInTransactionModeConverter + implements ClientSideStatementValueConverter { + static final DdlInTransactionModeConverter INSTANCE = new DdlInTransactionModeConverter(); + + private final CaseInsensitiveEnumMap values = + new CaseInsensitiveEnumMap<>(DdlInTransactionMode.class); + + private DdlInTransactionModeConverter() {} + + /** Constructor needed for reflection. */ + public DdlInTransactionModeConverter(String allowedValues) {} + + @Override + public Class getParameterClass() { + return DdlInTransactionMode.class; + } + + @Override + public DdlInTransactionMode convert(String value) { + return values.get(value); + } + } + static class ExplainCommandConverter implements ClientSideStatementValueConverter { @Override public Class getParameterClass() { @@ -588,4 +671,71 @@ public String convert(String filePath) { return filePath; } } + + static class CredentialsProviderConverter + implements ClientSideStatementValueConverter { + static final CredentialsProviderConverter INSTANCE = new CredentialsProviderConverter(); + + private CredentialsProviderConverter() {} + + @Override + public Class getParameterClass() { + return CredentialsProvider.class; + } + + @Override + public CredentialsProvider convert(String credentialsProviderName) { + if (!Strings.isNullOrEmpty(credentialsProviderName)) { + try { + Class clazz = + (Class) Class.forName(credentialsProviderName); + Constructor constructor = clazz.getDeclaredConstructor(); + return constructor.newInstance(); + } catch (ClassNotFoundException classNotFoundException) { + throw SpannerExceptionFactory.newSpannerException( + ErrorCode.INVALID_ARGUMENT, + "Unknown or invalid CredentialsProvider class name: " + credentialsProviderName, + classNotFoundException); + } catch (NoSuchMethodException noSuchMethodException) { + throw SpannerExceptionFactory.newSpannerException( + ErrorCode.INVALID_ARGUMENT, + "Credentials provider " + + credentialsProviderName + + " does not have a public no-arg constructor.", + noSuchMethodException); + } catch (InvocationTargetException + | InstantiationException + | IllegalAccessException exception) { + throw SpannerExceptionFactory.newSpannerException( + ErrorCode.INVALID_ARGUMENT, + "Failed to create an instance of " + + credentialsProviderName + + ": " + + exception.getMessage(), + exception); + } + } + return null; + } + } + + /** Converter for converting strings to {@link Dialect} values. */ + static class DialectConverter implements ClientSideStatementValueConverter { + static final DialectConverter INSTANCE = new DialectConverter(); + + private final CaseInsensitiveEnumMap values = + new CaseInsensitiveEnumMap<>(Dialect.class); + + private DialectConverter() {} + + @Override + public Class getParameterClass() { + return Dialect.class; + } + + @Override + public Dialect convert(String value) { + return values.get(value); + } + } } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java index b481c67bb45..407742678d9 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java @@ -17,7 +17,28 @@ package com.google.cloud.spanner.connection; import static com.google.cloud.spanner.SpannerApiFutures.get; +import static com.google.cloud.spanner.connection.ConnectionOptions.isEnableTransactionalConnectionStateForPostgreSQL; import static com.google.cloud.spanner.connection.ConnectionPreconditions.checkValidIdentifier; +import static com.google.cloud.spanner.connection.ConnectionProperties.AUTOCOMMIT; +import static com.google.cloud.spanner.connection.ConnectionProperties.AUTOCOMMIT_DML_MODE; +import static com.google.cloud.spanner.connection.ConnectionProperties.AUTO_PARTITION_MODE; +import static com.google.cloud.spanner.connection.ConnectionProperties.DATA_BOOST_ENABLED; +import static com.google.cloud.spanner.connection.ConnectionProperties.DDL_IN_TRANSACTION_MODE; +import static com.google.cloud.spanner.connection.ConnectionProperties.DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE; +import static com.google.cloud.spanner.connection.ConnectionProperties.DIRECTED_READ; +import static com.google.cloud.spanner.connection.ConnectionProperties.KEEP_TRANSACTION_ALIVE; +import static com.google.cloud.spanner.connection.ConnectionProperties.MAX_COMMIT_DELAY; +import static com.google.cloud.spanner.connection.ConnectionProperties.MAX_PARTITIONED_PARALLELISM; +import static com.google.cloud.spanner.connection.ConnectionProperties.MAX_PARTITIONS; +import static com.google.cloud.spanner.connection.ConnectionProperties.OPTIMIZER_STATISTICS_PACKAGE; +import static com.google.cloud.spanner.connection.ConnectionProperties.OPTIMIZER_VERSION; +import static com.google.cloud.spanner.connection.ConnectionProperties.READONLY; +import static com.google.cloud.spanner.connection.ConnectionProperties.READ_ONLY_STALENESS; +import static com.google.cloud.spanner.connection.ConnectionProperties.RETRY_ABORTS_INTERNALLY; +import static com.google.cloud.spanner.connection.ConnectionProperties.RETURN_COMMIT_STATS; +import static com.google.cloud.spanner.connection.ConnectionProperties.RPC_PRIORITY; +import static com.google.cloud.spanner.connection.ConnectionProperties.SAVEPOINT_SUPPORT; +import static com.google.cloud.spanner.connection.ConnectionProperties.TRACING_PREFIX; import com.google.api.core.ApiFuture; import com.google.api.core.ApiFutures; @@ -50,13 +71,16 @@ import com.google.cloud.spanner.TimestampBound.Mode; import com.google.cloud.spanner.connection.AbstractStatementParser.ParsedStatement; import com.google.cloud.spanner.connection.AbstractStatementParser.StatementType; +import com.google.cloud.spanner.connection.ConnectionProperty.Context; +import com.google.cloud.spanner.connection.ConnectionState.Type; import com.google.cloud.spanner.connection.StatementExecutor.StatementTimeout; import com.google.cloud.spanner.connection.StatementResult.ResultType; import com.google.cloud.spanner.connection.UnitOfWork.CallType; +import com.google.cloud.spanner.connection.UnitOfWork.EndTransactionCallback; import com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; +import com.google.common.base.Suppliers; import com.google.common.util.concurrent.MoreExecutors; import com.google.spanner.v1.DirectedReadOptions; import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions; @@ -92,7 +116,6 @@ /** Implementation for {@link Connection}, the generic Spanner connection API (not JDBC). */ class ConnectionImpl implements Connection { private static final String INSTRUMENTATION_SCOPE = "cloud.google.com/java"; - private static final String DEFAULT_TRACING_PREFIX = "CloudSpanner"; private static final String SINGLE_USE_TRANSACTION = "SingleUseTransaction"; private static final String READ_ONLY_TRANSACTION = "ReadOnlyTransaction"; private static final String READ_WRITE_TRANSACTION = "ReadWriteTransaction"; @@ -210,16 +233,11 @@ static UnitOfWorkType of(TransactionMode transactionMode) { private final Spanner spanner; private final Tracer tracer; - private final String tracingPrefix; private final Attributes openTelemetryAttributes; private final DdlClient ddlClient; private final DatabaseClient dbClient; private final BatchClient batchClient; - private boolean autocommit; - private boolean readOnly; - private boolean returnCommitStats; - private boolean delayTransactionStartUntilFirstWrite; - private boolean keepTransactionAlive; + private final ConnectionState connectionState; private UnitOfWork currentUnitOfWork = null; /** @@ -236,46 +254,13 @@ static UnitOfWorkType of(TransactionMode transactionMode) { private BatchMode batchMode; private UnitOfWorkType unitOfWorkType; private final Stack transactionStack = new Stack<>(); - private boolean retryAbortsInternally; private final List transactionRetryListeners = new ArrayList<>(); - private AutocommitDmlMode autocommitDmlMode = AutocommitDmlMode.TRANSACTIONAL; - private TimestampBound readOnlyStaleness = TimestampBound.strong(); - /** - * autoPartitionMode will force this connection to execute all queries as partitioned queries. If - * a query cannot be executed as a partitioned query, for example if it is not partitionable, then - * the query will fail. This mode is intended for integrations with frameworks that should always - * use partitioned queries, and that do not support executing custom SQL statements. This setting - * can be used in combination with the dataBoostEnabled flag to force all queries to use data - * boost. - */ - private boolean autoPartitionMode; - /** - * dataBoostEnabled=true will cause all partitionedQueries to use data boost. All other queries - * and other statements ignore this flag. - */ - private boolean dataBoostEnabled; - /** - * maxPartitions determines the maximum number of partitions that will be used for partitioned - * queries. All other statements ignore this variable. - */ - private int maxPartitions; - /** - * maxPartitionedParallelism determines the maximum number of threads that will be used to execute - * partitions in parallel when executing a partitioned query on this connection. - */ - private int maxPartitionedParallelism; - - private DirectedReadOptions directedReadOptions = null; - private QueryOptions queryOptions = QueryOptions.getDefaultInstance(); - private RpcPriority rpcPriority = null; - private SavepointSupport savepointSupport = SavepointSupport.FAIL_AFTER_ROLLBACK; - private DdlInTransactionMode ddlInTransactionMode; + // The following properties are not 'normal' connection properties, but transient properties that + // are automatically reset after executing a transaction or statement. private String transactionTag; private String statementTag; private boolean excludeTxnFromChangeStreams; - - private Duration maxCommitDelay; private byte[] protoDescriptors; private String protoDescriptorsFilePath; @@ -297,8 +282,6 @@ static UnitOfWorkType of(TransactionMode transactionMode) { .getTracer( INSTRUMENTATION_SCOPE, GaxProperties.getLibraryVersion(spanner.getOptions().getClass())); - this.tracingPrefix = - MoreObjects.firstNonNull(options.getTracingPrefix(), DEFAULT_TRACING_PREFIX); this.openTelemetryAttributes = createOpenTelemetryAttributes(options.getDatabaseId()); if (options.isAutoConfigEmulator()) { EmulatorUtil.maybeCreateInstanceAndDatabase( @@ -307,9 +290,18 @@ static UnitOfWorkType of(TransactionMode transactionMode) { this.dbClient = spanner.getDatabaseClient(options.getDatabaseId()); this.batchClient = spanner.getBatchClient(options.getDatabaseId()); this.ddlClient = createDdlClient(); + this.connectionState = + new ConnectionState( + options.getInitialConnectionPropertyValues(), + Suppliers.memoize( + () -> + isEnableTransactionalConnectionStateForPostgreSQL() + && getDialect() == Dialect.POSTGRESQL + ? Type.TRANSACTIONAL + : Type.NON_TRANSACTIONAL)); // (Re)set the state of the connection to the default. - reset(); + setDefaultTransactionOptions(); } /** Constructor only for test purposes. */ @@ -326,14 +318,16 @@ static UnitOfWorkType of(TransactionMode transactionMode) { new StatementExecutor(options.isUseVirtualThreads(), Collections.emptyList()); this.spannerPool = Preconditions.checkNotNull(spannerPool); this.options = Preconditions.checkNotNull(options); - this.ddlInTransactionMode = options.getDdlInTransactionMode(); this.spanner = spannerPool.getSpanner(options, this); this.tracer = OpenTelemetry.noop().getTracer(INSTRUMENTATION_SCOPE); - this.tracingPrefix = DEFAULT_TRACING_PREFIX; this.openTelemetryAttributes = Attributes.empty(); this.ddlClient = Preconditions.checkNotNull(ddlClient); this.dbClient = Preconditions.checkNotNull(dbClient); this.batchClient = Preconditions.checkNotNull(batchClient); + this.connectionState = + new ConnectionState( + options.getInitialConnectionPropertyValues(), + Suppliers.ofInstance(Type.NON_TRANSACTIONAL)); setReadOnly(options.isReadOnly()); setAutocommit(options.isAutocommit()); setReturnCommitStats(options.isReturnCommitStats()); @@ -375,6 +369,11 @@ static Attributes createOpenTelemetryAttributes(DatabaseId databaseId) { return attributesBuilder.build(); } + @VisibleForTesting + ConnectionState.Type getConnectionStateType() { + return this.connectionState.getType(); + } + @Override public void close() { try { @@ -423,35 +422,45 @@ public ApiFuture closeAsync() { return ApiFutures.immediateFuture(null); } + private Context getCurrentContext() { + return Context.USER; + } + /** * Resets the state of this connection to the default state in the {@link ConnectionOptions} of * this connection. */ public void reset() { - ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - - this.retryAbortsInternally = options.isRetryAbortsInternally(); - this.readOnly = options.isReadOnly(); - this.autocommit = options.isAutocommit(); - this.queryOptions = - QueryOptions.getDefaultInstance().toBuilder().mergeFrom(options.getQueryOptions()).build(); - this.rpcPriority = options.getRPCPriority(); - this.ddlInTransactionMode = options.getDdlInTransactionMode(); - this.returnCommitStats = options.isReturnCommitStats(); - this.delayTransactionStartUntilFirstWrite = options.isDelayTransactionStartUntilFirstWrite(); - this.keepTransactionAlive = options.isKeepTransactionAlive(); - this.dataBoostEnabled = options.isDataBoostEnabled(); - this.autoPartitionMode = options.isAutoPartitionMode(); - this.maxPartitions = options.getMaxPartitions(); - this.maxPartitionedParallelism = options.getMaxPartitionedParallelism(); - this.maxCommitDelay = options.getMaxCommitDelay(); - - this.autocommitDmlMode = AutocommitDmlMode.TRANSACTIONAL; - this.readOnlyStaleness = TimestampBound.strong(); + reset(getCurrentContext(), isInTransaction()); + } + + private void reset(Context context, boolean inTransaction) { + ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); + + // TODO: Replace all of these with a resetAll in ConnectionState. + this.connectionState.resetValue(RETRY_ABORTS_INTERNALLY, context, inTransaction); + this.connectionState.resetValue(AUTOCOMMIT, context, inTransaction); + this.connectionState.resetValue(READONLY, context, inTransaction); + this.connectionState.resetValue(READ_ONLY_STALENESS, context, inTransaction); + this.connectionState.resetValue(OPTIMIZER_VERSION, context, inTransaction); + this.connectionState.resetValue(OPTIMIZER_STATISTICS_PACKAGE, context, inTransaction); + this.connectionState.resetValue(RPC_PRIORITY, context, inTransaction); + this.connectionState.resetValue(DDL_IN_TRANSACTION_MODE, context, inTransaction); + this.connectionState.resetValue(RETURN_COMMIT_STATS, context, inTransaction); + this.connectionState.resetValue( + DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE, context, inTransaction); + this.connectionState.resetValue(KEEP_TRANSACTION_ALIVE, context, inTransaction); + this.connectionState.resetValue(AUTO_PARTITION_MODE, context, inTransaction); + this.connectionState.resetValue(DATA_BOOST_ENABLED, context, inTransaction); + this.connectionState.resetValue(MAX_PARTITIONS, context, inTransaction); + this.connectionState.resetValue(MAX_PARTITIONED_PARALLELISM, context, inTransaction); + this.connectionState.resetValue(MAX_COMMIT_DELAY, context, inTransaction); + + this.connectionState.resetValue(AUTOCOMMIT_DML_MODE, context, inTransaction); this.statementTag = null; this.statementTimeout = new StatementExecutor.StatementTimeout(); - this.directedReadOptions = null; - this.savepointSupport = SavepointSupport.FAIL_AFTER_ROLLBACK; + this.connectionState.resetValue(DIRECTED_READ, context, inTransaction); + this.connectionState.resetValue(SAVEPOINT_SUPPORT, context, inTransaction); this.protoDescriptors = null; this.protoDescriptorsFilePath = null; @@ -495,6 +504,39 @@ public boolean isClosed() { return closed; } + private T getConnectionPropertyValue( + com.google.cloud.spanner.connection.ConnectionProperty property) { + return this.connectionState.getValue(property).getValue(); + } + + private void setConnectionPropertyValue(ConnectionProperty property, T value) { + setConnectionPropertyValue(property, value, /* local = */ false); + } + + private void setConnectionPropertyValue( + ConnectionProperty property, T value, boolean local) { + if (local) { + setLocalConnectionPropertyValue(property, value); + } else { + this.connectionState.setValue(property, value, getCurrentContext(), isInTransaction()); + } + } + + /** + * Sets a connection property value only for the duration of the current transaction. The effects + * of this will be undone once the transaction ends, regardless whether the transaction is + * committed or rolled back. 'Local' properties are supported for both {@link + * com.google.cloud.spanner.connection.ConnectionState.Type#TRANSACTIONAL} and {@link + * com.google.cloud.spanner.connection.ConnectionState.Type#NON_TRANSACTIONAL} connection states. + * + *

NOTE: This feature is not yet exposed in the public API. + */ + private void setLocalConnectionPropertyValue(ConnectionProperty property, T value) { + ConnectionPreconditions.checkState( + isInTransaction(), "SET LOCAL statements are only supported in transactions"); + this.connectionState.setLocalValue(property, value); + } + @Override public void setAutocommit(boolean autocommit) { ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); @@ -509,14 +551,24 @@ public void setAutocommit(boolean autocommit) { "Cannot set autocommit while in a temporary transaction"); ConnectionPreconditions.checkState( !transactionBeginMarked, "Cannot set autocommit when a transaction has begun"); - this.autocommit = autocommit; + setConnectionPropertyValue(AUTOCOMMIT, autocommit); + if (autocommit) { + // Commit the current transaction state if we went from autocommit=false to autocommit=true. + // Otherwise, we get the strange situation that autocommit=true cannot be committed, as we no + // longer have a transaction. Note that all the above state checks essentially mean that + // autocommit can only be set before a transaction has actually started, and not in the + // middle of a transaction. + this.connectionState.commit(); + } clearLastTransactionAndSetDefaultTransactionOptions(); // Reset the readOnlyStaleness value if it is no longer compatible with the new autocommit // value. - if (!autocommit - && (readOnlyStaleness.getMode() == Mode.MAX_STALENESS - || readOnlyStaleness.getMode() == Mode.MIN_READ_TIMESTAMP)) { - readOnlyStaleness = TimestampBound.strong(); + if (!autocommit) { + TimestampBound readOnlyStaleness = getReadOnlyStaleness(); + if (readOnlyStaleness.getMode() == Mode.MAX_STALENESS + || readOnlyStaleness.getMode() == Mode.MIN_READ_TIMESTAMP) { + setConnectionPropertyValue(READ_ONLY_STALENESS, TimestampBound.strong()); + } } } @@ -527,7 +579,7 @@ public boolean isAutocommit() { } private boolean internalIsAutocommit() { - return this.autocommit; + return getConnectionPropertyValue(AUTOCOMMIT); } @Override @@ -541,14 +593,14 @@ public void setReadOnly(boolean readOnly) { "Cannot set read-only while in a temporary transaction"); ConnectionPreconditions.checkState( !transactionBeginMarked, "Cannot set read-only when a transaction has begun"); - this.readOnly = readOnly; + setConnectionPropertyValue(READONLY, readOnly); clearLastTransactionAndSetDefaultTransactionOptions(); } @Override public boolean isReadOnly() { ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - return this.readOnly; + return getConnectionPropertyValue(READONLY); } private void clearLastTransactionAndSetDefaultTransactionOptions() { @@ -567,7 +619,7 @@ public void setAutocommitDmlMode(AutocommitDmlMode mode) { "Cannot set autocommit DML mode while not in autocommit mode or while a transaction is active"); ConnectionPreconditions.checkState( !isReadOnly(), "Cannot set autocommit DML mode for a read-only connection"); - this.autocommitDmlMode = mode; + setConnectionPropertyValue(AUTOCOMMIT_DML_MODE, mode); } @Override @@ -575,7 +627,7 @@ public AutocommitDmlMode getAutocommitDmlMode() { ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); ConnectionPreconditions.checkState( !isBatchActive(), "Cannot get autocommit DML mode while in a batch"); - return this.autocommitDmlMode; + return getConnectionPropertyValue(AUTOCOMMIT_DML_MODE); } @Override @@ -593,14 +645,14 @@ public void setReadOnlyStaleness(TimestampBound staleness) { isAutocommit() && !inTransaction, "MAX_STALENESS and MIN_READ_TIMESTAMP are only allowed in autocommit mode"); } - this.readOnlyStaleness = staleness; + setConnectionPropertyValue(READ_ONLY_STALENESS, staleness); } @Override public TimestampBound getReadOnlyStaleness() { ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); ConnectionPreconditions.checkState(!isBatchActive(), "Cannot get read-only while in a batch"); - return this.readOnlyStaleness; + return getConnectionPropertyValue(READ_ONLY_STALENESS); } @Override @@ -609,57 +661,63 @@ public void setDirectedRead(DirectedReadOptions directedReadOptions) { ConnectionPreconditions.checkState( !isTransactionStarted(), "Cannot set directed read options when a transaction has been started"); - this.directedReadOptions = directedReadOptions; + setConnectionPropertyValue(DIRECTED_READ, directedReadOptions); } @Override public DirectedReadOptions getDirectedRead() { ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - return this.directedReadOptions; + return getConnectionPropertyValue(DIRECTED_READ); } @Override public void setOptimizerVersion(String optimizerVersion) { Preconditions.checkNotNull(optimizerVersion); ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - this.queryOptions = queryOptions.toBuilder().setOptimizerVersion(optimizerVersion).build(); + setConnectionPropertyValue(OPTIMIZER_VERSION, optimizerVersion); } @Override public String getOptimizerVersion() { ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - return this.queryOptions.getOptimizerVersion(); + return getConnectionPropertyValue(OPTIMIZER_VERSION); } @Override public void setOptimizerStatisticsPackage(String optimizerStatisticsPackage) { Preconditions.checkNotNull(optimizerStatisticsPackage); ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - this.queryOptions = - queryOptions.toBuilder().setOptimizerStatisticsPackage(optimizerStatisticsPackage).build(); + setConnectionPropertyValue(OPTIMIZER_STATISTICS_PACKAGE, optimizerStatisticsPackage); } @Override public String getOptimizerStatisticsPackage() { ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - return this.queryOptions.getOptimizerStatisticsPackage(); + return getConnectionPropertyValue(OPTIMIZER_STATISTICS_PACKAGE); + } + + private QueryOptions buildQueryOptions() { + return QueryOptions.newBuilder() + .setOptimizerVersion(getConnectionPropertyValue(OPTIMIZER_VERSION)) + .setOptimizerStatisticsPackage(getConnectionPropertyValue(OPTIMIZER_STATISTICS_PACKAGE)) + .build(); } @Override public void setRPCPriority(RpcPriority rpcPriority) { ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - this.rpcPriority = rpcPriority; + setConnectionPropertyValue(RPC_PRIORITY, rpcPriority); } @Override public RpcPriority getRPCPriority() { ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - return this.rpcPriority; + return getConnectionPropertyValue(RPC_PRIORITY); } @Override public DdlInTransactionMode getDdlInTransactionMode() { - return this.ddlInTransactionMode; + return getConnectionPropertyValue(DDL_IN_TRANSACTION_MODE); } @Override @@ -669,7 +727,7 @@ public void setDdlInTransactionMode(DdlInTransactionMode ddlInTransactionMode) { !isBatchActive(), "Cannot set DdlInTransactionMode while in a batch"); ConnectionPreconditions.checkState( !isTransactionStarted(), "Cannot set DdlInTransactionMode while a transaction is active"); - this.ddlInTransactionMode = Preconditions.checkNotNull(ddlInTransactionMode); + setConnectionPropertyValue(DDL_IN_TRANSACTION_MODE, ddlInTransactionMode); } @Override @@ -856,13 +914,13 @@ private void checkSetRetryAbortsInternallyAvailable() { @Override public boolean isRetryAbortsInternally() { ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - return retryAbortsInternally; + return getConnectionPropertyValue(RETRY_ABORTS_INTERNALLY); } @Override public void setRetryAbortsInternally(boolean retryAbortsInternally) { checkSetRetryAbortsInternallyAvailable(); - this.retryAbortsInternally = retryAbortsInternally; + setConnectionPropertyValue(RETRY_ABORTS_INTERNALLY, retryAbortsInternally); } @Override @@ -908,6 +966,10 @@ private boolean internalIsTransactionStarted() { && this.currentUnitOfWork.getState() == UnitOfWorkState.STARTED; } + private boolean hasTransactionalChanges() { + return internalIsTransactionStarted() || this.connectionState.hasTransactionalChanges(); + } + @Override public Timestamp getReadTimestamp() { ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); @@ -951,26 +1013,31 @@ CommitResponse getCommitResponseOrNull() { @Override public void setReturnCommitStats(boolean returnCommitStats) { + setReturnCommitStats(returnCommitStats, /* local = */ false); + } + + @VisibleForTesting + void setReturnCommitStats(boolean returnCommitStats, boolean local) { ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - this.returnCommitStats = returnCommitStats; + setConnectionPropertyValue(RETURN_COMMIT_STATS, returnCommitStats, local); } @Override public boolean isReturnCommitStats() { ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - return this.returnCommitStats; + return getConnectionPropertyValue(RETURN_COMMIT_STATS); } @Override public void setMaxCommitDelay(Duration maxCommitDelay) { ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - this.maxCommitDelay = maxCommitDelay; + setConnectionPropertyValue(MAX_COMMIT_DELAY, maxCommitDelay); } @Override public Duration getMaxCommitDelay() { ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - return this.maxCommitDelay; + return getConnectionPropertyValue(MAX_COMMIT_DELAY); } @Override @@ -980,13 +1047,14 @@ public void setDelayTransactionStartUntilFirstWrite( ConnectionPreconditions.checkState( !isTransactionStarted(), "Cannot set DelayTransactionStartUntilFirstWrite while a transaction is active"); - this.delayTransactionStartUntilFirstWrite = delayTransactionStartUntilFirstWrite; + setConnectionPropertyValue( + DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE, delayTransactionStartUntilFirstWrite); } @Override public boolean isDelayTransactionStartUntilFirstWrite() { ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - return this.delayTransactionStartUntilFirstWrite; + return getConnectionPropertyValue(DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE); } @Override @@ -994,13 +1062,13 @@ public void setKeepTransactionAlive(boolean keepTransactionAlive) { ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); ConnectionPreconditions.checkState( !isTransactionStarted(), "Cannot set KeepTransactionAlive while a transaction is active"); - this.keepTransactionAlive = keepTransactionAlive; + setConnectionPropertyValue(KEEP_TRANSACTION_ALIVE, keepTransactionAlive); } @Override public boolean isKeepTransactionAlive() { ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - return this.keepTransactionAlive; + return getConnectionPropertyValue(KEEP_TRANSACTION_ALIVE); } /** Resets this connection to its default transaction options. */ @@ -1046,10 +1114,22 @@ private interface EndTransactionMethod { ApiFuture endAsync(CallType callType, UnitOfWork t); } - private static final class Commit implements EndTransactionMethod { + private final class Commit implements EndTransactionMethod { @Override public ApiFuture endAsync(CallType callType, UnitOfWork t) { - return t.commitAsync(callType); + return t.commitAsync( + callType, + new EndTransactionCallback() { + @Override + public void onSuccess() { + ConnectionImpl.this.connectionState.commit(); + } + + @Override + public void onFailure() { + ConnectionImpl.this.connectionState.rollback(); + } + }); } } @@ -1070,10 +1150,22 @@ private ApiFuture commitAsync(CallType callType) { return endCurrentTransactionAsync(callType, commit); } - private static final class Rollback implements EndTransactionMethod { + private final class Rollback implements EndTransactionMethod { @Override public ApiFuture endAsync(CallType callType, UnitOfWork t) { - return t.rollbackAsync(callType); + return t.rollbackAsync( + callType, + new EndTransactionCallback() { + @Override + public void onSuccess() { + ConnectionImpl.this.connectionState.rollback(); + } + + @Override + public void onFailure() { + ConnectionImpl.this.connectionState.rollback(); + } + }); } } @@ -1102,7 +1194,7 @@ private ApiFuture endCurrentTransactionAsync( statementTag == null, "Statement tags are not supported for COMMIT or ROLLBACK"); ApiFuture res; try { - if (isTransactionStarted()) { + if (hasTransactionalChanges()) { res = endTransactionMethod.endAsync(callType, getCurrentUnitOfWorkOrStartNewUnitOfWork()); } else { this.currentUnitOfWork = null; @@ -1120,7 +1212,7 @@ private ApiFuture endCurrentTransactionAsync( @Override public SavepointSupport getSavepointSupport() { - return this.savepointSupport; + return getConnectionPropertyValue(SAVEPOINT_SUPPORT); } @Override @@ -1130,12 +1222,13 @@ public void setSavepointSupport(SavepointSupport savepointSupport) { !isBatchActive(), "Cannot set SavepointSupport while in a batch"); ConnectionPreconditions.checkState( !isTransactionStarted(), "Cannot set SavepointSupport while a transaction is active"); - this.savepointSupport = savepointSupport; + setConnectionPropertyValue(SAVEPOINT_SUPPORT, savepointSupport); } @Override public void savepoint(String name) { ConnectionPreconditions.checkState(isInTransaction(), "This connection has no transaction"); + SavepointSupport savepointSupport = getSavepointSupport(); ConnectionPreconditions.checkState( savepointSupport.isSavepointCreationAllowed(), "This connection does not allow the creation of savepoints. Current value of SavepointSupport: " @@ -1155,7 +1248,7 @@ public void rollbackToSavepoint(String name) { ConnectionPreconditions.checkState( isTransactionStarted(), "This connection has no active transaction"); getCurrentUnitOfWorkOrStartNewUnitOfWork() - .rollbackToSavepoint(checkValidIdentifier(name), savepointSupport); + .rollbackToSavepoint(checkValidIdentifier(name), getSavepointSupport()); } @Override @@ -1172,7 +1265,7 @@ public StatementResult execute(Statement statement, Set allowedResul private StatementResult internalExecute( Statement statement, @Nullable Set allowedResultTypes) { ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - ParsedStatement parsedStatement = getStatementParser().parse(statement, this.queryOptions); + ParsedStatement parsedStatement = getStatementParser().parse(statement, buildQueryOptions()); checkResultTypeAllowed(parsedStatement, allowedResultTypes); switch (parsedStatement.getType()) { case CLIENT_SIDE: @@ -1251,7 +1344,7 @@ private static ResultType getResultType(ParsedStatement parsedStatement) { public AsyncStatementResult executeAsync(Statement statement) { Preconditions.checkNotNull(statement); ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - ParsedStatement parsedStatement = getStatementParser().parse(statement, this.queryOptions); + ParsedStatement parsedStatement = getStatementParser().parse(statement, buildQueryOptions()); switch (parsedStatement.getType()) { case CLIENT_SIDE: return AsyncStatementResultImpl.of( @@ -1297,38 +1390,46 @@ public ResultSet analyzeQuery(Statement query, QueryAnalyzeMode queryMode) { @Override public void setDataBoostEnabled(boolean dataBoostEnabled) { - this.dataBoostEnabled = dataBoostEnabled; + setConnectionPropertyValue(DATA_BOOST_ENABLED, dataBoostEnabled); } @Override public boolean isDataBoostEnabled() { - return this.dataBoostEnabled; + return getConnectionPropertyValue(DATA_BOOST_ENABLED); } @Override public void setAutoPartitionMode(boolean autoPartitionMode) { - this.autoPartitionMode = autoPartitionMode; + setConnectionPropertyValue(AUTO_PARTITION_MODE, autoPartitionMode); } + /** + * autoPartitionMode will force this connection to execute all queries as partitioned queries. If + * a query cannot be executed as a partitioned query, for example if it is not partitionable, then + * the query will fail. This mode is intended for integrations with frameworks that should always + * use partitioned queries, and that do not support executing custom SQL statements. This setting + * can be used in combination with the dataBoostEnabled flag to force all queries to use data + * boost. + */ @Override public boolean isAutoPartitionMode() { - return this.autoPartitionMode; + return getConnectionPropertyValue(AUTO_PARTITION_MODE); } @Override public void setMaxPartitions(int maxPartitions) { - this.maxPartitions = maxPartitions; + setConnectionPropertyValue(MAX_PARTITIONS, maxPartitions); } @Override public int getMaxPartitions() { - return this.maxPartitions; + return getConnectionPropertyValue(MAX_PARTITIONS); } @Override public ResultSet partitionQuery( Statement query, PartitionOptions partitionOptions, QueryOption... options) { - ParsedStatement parsedStatement = getStatementParser().parse(query, this.queryOptions); + ParsedStatement parsedStatement = getStatementParser().parse(query, buildQueryOptions()); if (parsedStatement.getType() != StatementType.QUERY) { throw SpannerExceptionFactory.newSpannerException( ErrorCode.INVALID_ARGUMENT, @@ -1349,7 +1450,7 @@ public ResultSet partitionQuery( private PartitionOptions getEffectivePartitionOptions( PartitionOptions callSpecificPartitionOptions) { - if (maxPartitions == 0) { + if (getMaxPartitions() == 0) { if (callSpecificPartitionOptions == null) { return PartitionOptions.newBuilder().build(); } else { @@ -1363,11 +1464,11 @@ private PartitionOptions getEffectivePartitionOptions( if (callSpecificPartitionOptions != null && callSpecificPartitionOptions.getPartitionSizeBytes() > 0L) { return PartitionOptions.newBuilder() - .setMaxPartitions(maxPartitions) + .setMaxPartitions(getMaxPartitions()) .setPartitionSizeBytes(callSpecificPartitionOptions.getPartitionSizeBytes()) .build(); } - return PartitionOptions.newBuilder().setMaxPartitions(maxPartitions).build(); + return PartitionOptions.newBuilder().setMaxPartitions(getMaxPartitions()).build(); } @Override @@ -1382,12 +1483,12 @@ public ResultSet runPartition(String encodedPartitionId) { @Override public void setMaxPartitionedParallelism(int maxThreads) { Preconditions.checkArgument(maxThreads >= 0, "maxThreads must be >=0"); - this.maxPartitionedParallelism = maxThreads; + setConnectionPropertyValue(MAX_PARTITIONED_PARALLELISM, maxThreads); } @Override public int getMaxPartitionedParallelism() { - return this.maxPartitionedParallelism; + return getConnectionPropertyValue(MAX_PARTITIONED_PARALLELISM); } @Override @@ -1401,7 +1502,7 @@ public PartitionedQueryResultSet runPartitionedQuery( } // parallelism=0 means 'dynamically choose based on the number of available processors and the // number of partitions'. - return new MergedResultSet(this, partitionIds, maxPartitionedParallelism); + return new MergedResultSet(this, partitionIds, getMaxPartitionedParallelism()); } /** @@ -1413,7 +1514,7 @@ private ResultSet parseAndExecuteQuery( Preconditions.checkNotNull(query); Preconditions.checkNotNull(analyzeMode); ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - ParsedStatement parsedStatement = getStatementParser().parse(query, this.queryOptions); + ParsedStatement parsedStatement = getStatementParser().parse(query, buildQueryOptions()); if (parsedStatement.isQuery() || parsedStatement.isUpdate()) { switch (parsedStatement.getType()) { case CLIENT_SIDE: @@ -1452,7 +1553,7 @@ private AsyncResultSet parseAndExecuteQueryAsync( CallType callType, Statement query, AnalyzeMode analyzeMode, QueryOption... options) { Preconditions.checkNotNull(query); ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - ParsedStatement parsedStatement = getStatementParser().parse(query, this.queryOptions); + ParsedStatement parsedStatement = getStatementParser().parse(query, buildQueryOptions()); if (parsedStatement.isQuery() || parsedStatement.isUpdate()) { switch (parsedStatement.getType()) { case CLIENT_SIDE: @@ -1667,7 +1768,7 @@ private QueryOption[] concat( } private QueryOption[] mergeDataBoost(QueryOption... options) { - if (this.dataBoostEnabled) { + if (isDataBoostEnabled()) { options = appendQueryOption(options, Options.dataBoostEnabled(true)); } return options; @@ -1683,13 +1784,16 @@ private QueryOption[] mergeQueryStatementTag(QueryOption... options) { private QueryOption[] mergeQueryRequestOptions( ParsedStatement parsedStatement, QueryOption... options) { - if (this.rpcPriority != null) { - options = appendQueryOption(options, Options.priority(this.rpcPriority)); + if (getConnectionPropertyValue(RPC_PRIORITY) != null) { + options = + appendQueryOption(options, Options.priority(getConnectionPropertyValue(RPC_PRIORITY))); } - if (this.directedReadOptions != null - && currentUnitOfWork != null - && currentUnitOfWork.supportsDirectedReads(parsedStatement)) { - options = appendQueryOption(options, Options.directedRead(this.directedReadOptions)); + if (currentUnitOfWork != null + && currentUnitOfWork.supportsDirectedReads(parsedStatement) + && getConnectionPropertyValue(DIRECTED_READ) != null) { + options = + appendQueryOption( + options, Options.directedRead(getConnectionPropertyValue(DIRECTED_READ))); } return options; } @@ -1719,13 +1823,13 @@ private UpdateOption[] mergeUpdateStatementTag(UpdateOption... options) { } private UpdateOption[] mergeUpdateRequestOptions(UpdateOption... options) { - if (this.rpcPriority != null) { + if (getConnectionPropertyValue(RPC_PRIORITY) != null) { // Shortcut for the most common scenario. if (options == null || options.length == 0) { - options = new UpdateOption[] {Options.priority(this.rpcPriority)}; + options = new UpdateOption[] {Options.priority(getConnectionPropertyValue(RPC_PRIORITY))}; } else { options = Arrays.copyOf(options, options.length + 1); - options[options.length - 1] = Options.priority(this.rpcPriority); + options[options.length - 1] = Options.priority(getConnectionPropertyValue(RPC_PRIORITY)); } } return options; @@ -1744,7 +1848,7 @@ private ResultSet internalExecuteQuery( boolean isInternalMetadataQuery = isInternalMetadataQuery(options); QueryOption[] combinedOptions = concat(statement.getOptionsFromHints(), options); UnitOfWork transaction = getCurrentUnitOfWorkOrStartNewUnitOfWork(isInternalMetadataQuery); - if (autoPartitionMode + if (isAutoPartitionMode() && statement.getType() == StatementType.QUERY && !isInternalMetadataQuery) { return runPartitionedQuery( @@ -1768,7 +1872,7 @@ private AsyncResultSet internalExecuteQueryAsync( || (statement.getType() == StatementType.UPDATE && statement.hasReturningClause()), "Statement must be a query or DML with returning clause."); ConnectionPreconditions.checkState( - !(autoPartitionMode && statement.getType() == StatementType.QUERY), + !(isAutoPartitionMode() && statement.getType() == StatementType.QUERY), "Partitioned queries cannot be executed asynchronously"); boolean isInternalMetadataQuery = isInternalMetadataQuery(options); QueryOption[] combinedOptions = concat(statement.getOptionsFromHints(), options); @@ -1848,7 +1952,7 @@ UnitOfWork getCurrentUnitOfWorkOrStartNewUnitOfWork( createNewUnitOfWork( /* isInternalMetadataQuery = */ false, /* forceSingleUse = */ statementType == StatementType.DDL - && this.ddlInTransactionMode != DdlInTransactionMode.FAIL + && getDdlInTransactionMode() != DdlInTransactionMode.FAIL && !this.transactionBeginMarked, statementType); } @@ -1857,7 +1961,11 @@ UnitOfWork getCurrentUnitOfWorkOrStartNewUnitOfWork( private Span createSpanForUnitOfWork(String name) { return tracer - .spanBuilder(this.tracingPrefix + "." + name) + .spanBuilder( + // We can memoize this, as it is a STARTUP property. + Suppliers.memoize(() -> this.connectionState.getValue(TRACING_PREFIX).getValue()).get() + + "." + + name) .setAllAttributes(getOpenTelemetryAttributes()) .startSpan(); } @@ -1866,7 +1974,7 @@ void maybeAutoCommitCurrentTransaction(StatementType statementType) { if (this.currentUnitOfWork instanceof ReadWriteTransaction && this.currentUnitOfWork.isActive() && statementType == StatementType.DDL - && this.ddlInTransactionMode == DdlInTransactionMode.AUTO_COMMIT_TRANSACTION) { + && getDdlInTransactionMode() == DdlInTransactionMode.AUTO_COMMIT_TRANSACTION) { commit(); } } @@ -1888,12 +1996,12 @@ UnitOfWork createNewUnitOfWork( .setDdlClient(ddlClient) .setDatabaseClient(dbClient) .setBatchClient(batchClient) - .setReadOnly(isReadOnly()) - .setReadOnlyStaleness(readOnlyStaleness) - .setAutocommitDmlMode(autocommitDmlMode) - .setReturnCommitStats(returnCommitStats) + .setReadOnly(getConnectionPropertyValue(READONLY)) + .setReadOnlyStaleness(getConnectionPropertyValue(READ_ONLY_STALENESS)) + .setAutocommitDmlMode(getConnectionPropertyValue(AUTOCOMMIT_DML_MODE)) + .setReturnCommitStats(getConnectionPropertyValue(RETURN_COMMIT_STATS)) .setExcludeTxnFromChangeStreams(excludeTxnFromChangeStreams) - .setMaxCommitDelay(maxCommitDelay) + .setMaxCommitDelay(getConnectionPropertyValue(MAX_COMMIT_DELAY)) .setStatementTimeout(statementTimeout) .withStatementExecutor(statementExecutor) .setSpan( @@ -1912,11 +2020,11 @@ UnitOfWork createNewUnitOfWork( return ReadOnlyTransaction.newBuilder() .setDatabaseClient(dbClient) .setBatchClient(batchClient) - .setReadOnlyStaleness(readOnlyStaleness) + .setReadOnlyStaleness(getConnectionPropertyValue(READ_ONLY_STALENESS)) .setStatementTimeout(statementTimeout) .withStatementExecutor(statementExecutor) .setTransactionTag(transactionTag) - .setRpcPriority(rpcPriority) + .setRpcPriority(getConnectionPropertyValue(RPC_PRIORITY)) .setSpan(createSpanForUnitOfWork(READ_ONLY_TRANSACTION)) .build(); case READ_WRITE_TRANSACTION: @@ -1924,18 +2032,19 @@ UnitOfWork createNewUnitOfWork( .setUsesEmulator(options.usesEmulator()) .setUseAutoSavepointsForEmulator(options.useAutoSavepointsForEmulator()) .setDatabaseClient(dbClient) - .setDelayTransactionStartUntilFirstWrite(delayTransactionStartUntilFirstWrite) - .setKeepTransactionAlive(keepTransactionAlive) - .setRetryAbortsInternally(retryAbortsInternally) - .setSavepointSupport(savepointSupport) - .setReturnCommitStats(returnCommitStats) - .setMaxCommitDelay(maxCommitDelay) + .setDelayTransactionStartUntilFirstWrite( + getConnectionPropertyValue(DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE)) + .setKeepTransactionAlive(getConnectionPropertyValue(KEEP_TRANSACTION_ALIVE)) + .setRetryAbortsInternally(getConnectionPropertyValue(RETRY_ABORTS_INTERNALLY)) + .setSavepointSupport(getConnectionPropertyValue(SAVEPOINT_SUPPORT)) + .setReturnCommitStats(getConnectionPropertyValue(RETURN_COMMIT_STATS)) + .setMaxCommitDelay(getConnectionPropertyValue(MAX_COMMIT_DELAY)) .setTransactionRetryListeners(transactionRetryListeners) .setStatementTimeout(statementTimeout) .withStatementExecutor(statementExecutor) .setTransactionTag(transactionTag) .setExcludeTxnFromChangeStreams(excludeTxnFromChangeStreams) - .setRpcPriority(rpcPriority) + .setRpcPriority(getConnectionPropertyValue(RPC_PRIORITY)) .setSpan(createSpanForUnitOfWork(READ_WRITE_TRANSACTION)) .build(); case DML_BATCH: @@ -1948,7 +2057,7 @@ UnitOfWork createNewUnitOfWork( .withStatementExecutor(statementExecutor) .setStatementTag(statementTag) .setExcludeTxnFromChangeStreams(excludeTxnFromChangeStreams) - .setRpcPriority(rpcPriority) + .setRpcPriority(getConnectionPropertyValue(RPC_PRIORITY)) // Use the transaction Span for the DML batch. .setSpan(transactionStack.peek().getSpan()) .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 7710a1dee58..1795ad172e2 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 @@ -16,6 +16,41 @@ package com.google.cloud.spanner.connection; +import static com.google.cloud.spanner.connection.ConnectionProperties.AUTOCOMMIT; +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.CREDENTIALS_PROVIDER; +import static com.google.cloud.spanner.connection.ConnectionProperties.CREDENTIALS_URL; +import static com.google.cloud.spanner.connection.ConnectionProperties.DATABASE_ROLE; +import static com.google.cloud.spanner.connection.ConnectionProperties.DATA_BOOST_ENABLED; +import static com.google.cloud.spanner.connection.ConnectionProperties.DIALECT; +import static com.google.cloud.spanner.connection.ConnectionProperties.ENABLE_API_TRACING; +import static com.google.cloud.spanner.connection.ConnectionProperties.ENABLE_EXTENDED_TRACING; +import static com.google.cloud.spanner.connection.ConnectionProperties.ENCODED_CREDENTIALS; +import static com.google.cloud.spanner.connection.ConnectionProperties.ENDPOINT; +import static com.google.cloud.spanner.connection.ConnectionProperties.LENIENT; +import static com.google.cloud.spanner.connection.ConnectionProperties.MAX_COMMIT_DELAY; +import static com.google.cloud.spanner.connection.ConnectionProperties.MAX_PARTITIONED_PARALLELISM; +import static com.google.cloud.spanner.connection.ConnectionProperties.MAX_PARTITIONS; +import static com.google.cloud.spanner.connection.ConnectionProperties.MAX_SESSIONS; +import static com.google.cloud.spanner.connection.ConnectionProperties.MIN_SESSIONS; +import static com.google.cloud.spanner.connection.ConnectionProperties.NUM_CHANNELS; +import static com.google.cloud.spanner.connection.ConnectionProperties.OAUTH_TOKEN; +import static com.google.cloud.spanner.connection.ConnectionProperties.READONLY; +import static com.google.cloud.spanner.connection.ConnectionProperties.RETRY_ABORTS_INTERNALLY; +import static com.google.cloud.spanner.connection.ConnectionProperties.RETURN_COMMIT_STATS; +import static com.google.cloud.spanner.connection.ConnectionProperties.ROUTE_TO_LEADER; +import static com.google.cloud.spanner.connection.ConnectionProperties.TRACING_PREFIX; +import static com.google.cloud.spanner.connection.ConnectionProperties.TRACK_CONNECTION_LEAKS; +import static com.google.cloud.spanner.connection.ConnectionProperties.TRACK_SESSION_LEAKS; +import static com.google.cloud.spanner.connection.ConnectionProperties.USER_AGENT; +import static com.google.cloud.spanner.connection.ConnectionProperties.USE_AUTO_SAVEPOINTS_FOR_EMULATOR; +import static com.google.cloud.spanner.connection.ConnectionProperties.USE_PLAIN_TEXT; +import static com.google.cloud.spanner.connection.ConnectionProperties.USE_VIRTUAL_GRPC_TRANSPORT_THREADS; +import static com.google.cloud.spanner.connection.ConnectionProperties.USE_VIRTUAL_THREADS; +import static com.google.cloud.spanner.connection.ConnectionPropertyValue.cast; + import com.google.api.core.InternalApi; import com.google.api.gax.core.CredentialsProvider; import com.google.api.gax.rpc.TransportChannelProvider; @@ -38,19 +73,19 @@ import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.base.Suppliers; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.Sets; -import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions; import io.opentelemetry.api.OpenTelemetry; import java.io.IOException; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; import java.net.URL; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -87,7 +122,12 @@ */ @InternalApi public class ConnectionOptions { - /** Supported connection properties that can be included in the connection URI. */ + /** + * Supported connection properties that can be included in the connection URI. + * + * @deprecated Replaced by {@link com.google.cloud.spanner.connection.ConnectionProperty}. + */ + @Deprecated public static class ConnectionProperty { private static final String[] BOOLEAN_VALUES = new String[] {"true", "false"}; private final String name; @@ -167,41 +207,48 @@ public String[] getValidValues() { } } + /** + * Set this system property to true to enable transactional connection state by default for + * PostgreSQL-dialect databases. The default is currently false. + */ + public static String ENABLE_TRANSACTIONAL_CONNECTION_STATE_FOR_POSTGRESQL_PROPERTY = + "spanner.enable_transactional_connection_state_for_postgresql"; + private static final LocalConnectionChecker LOCAL_CONNECTION_CHECKER = new LocalConnectionChecker(); - private static final boolean DEFAULT_USE_PLAIN_TEXT = false; + static final boolean DEFAULT_USE_PLAIN_TEXT = false; static final boolean DEFAULT_AUTOCOMMIT = true; static final boolean DEFAULT_READONLY = false; static final boolean DEFAULT_RETRY_ABORTS_INTERNALLY = true; static final boolean DEFAULT_USE_VIRTUAL_THREADS = false; static final boolean DEFAULT_USE_VIRTUAL_GRPC_TRANSPORT_THREADS = false; - private static final String DEFAULT_CREDENTIALS = null; - private static final String DEFAULT_OAUTH_TOKEN = null; - private static final String DEFAULT_MIN_SESSIONS = null; - private static final String DEFAULT_MAX_SESSIONS = null; - private static final String DEFAULT_NUM_CHANNELS = null; + static final String DEFAULT_CREDENTIALS = null; + static final String DEFAULT_OAUTH_TOKEN = null; + static final Integer DEFAULT_MIN_SESSIONS = null; + static final Integer DEFAULT_MAX_SESSIONS = null; + static final Integer DEFAULT_NUM_CHANNELS = null; static final String DEFAULT_ENDPOINT = null; - private static final String DEFAULT_CHANNEL_PROVIDER = null; - private static final String DEFAULT_DATABASE_ROLE = null; - private static final String DEFAULT_USER_AGENT = null; - private static final String DEFAULT_OPTIMIZER_VERSION = ""; - private static final String DEFAULT_OPTIMIZER_STATISTICS_PACKAGE = ""; - private static final RpcPriority DEFAULT_RPC_PRIORITY = null; - private static final DdlInTransactionMode DEFAULT_DDL_IN_TRANSACTION_MODE = + static final String DEFAULT_CHANNEL_PROVIDER = null; + static final String DEFAULT_DATABASE_ROLE = null; + static final String DEFAULT_USER_AGENT = null; + static final String DEFAULT_OPTIMIZER_VERSION = ""; + static final String DEFAULT_OPTIMIZER_STATISTICS_PACKAGE = ""; + static final RpcPriority DEFAULT_RPC_PRIORITY = null; + static final DdlInTransactionMode DEFAULT_DDL_IN_TRANSACTION_MODE = DdlInTransactionMode.ALLOW_IN_EMPTY_TRANSACTION; - private static final boolean DEFAULT_RETURN_COMMIT_STATS = false; - private static final boolean DEFAULT_LENIENT = false; - private static final boolean DEFAULT_ROUTE_TO_LEADER = true; - private static final boolean DEFAULT_DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE = false; - private static final boolean DEFAULT_KEEP_TRANSACTION_ALIVE = false; - private static final boolean DEFAULT_TRACK_SESSION_LEAKS = true; - private static final boolean DEFAULT_TRACK_CONNECTION_LEAKS = true; - private static final boolean DEFAULT_DATA_BOOST_ENABLED = false; - private static final boolean DEFAULT_AUTO_PARTITION_MODE = false; - private static final int DEFAULT_MAX_PARTITIONS = 0; - private static final int DEFAULT_MAX_PARTITIONED_PARALLELISM = 1; - private static final Boolean DEFAULT_ENABLE_EXTENDED_TRACING = null; - private static final Boolean DEFAULT_ENABLE_API_TRACING = null; + static final boolean DEFAULT_RETURN_COMMIT_STATS = false; + static final boolean DEFAULT_LENIENT = false; + static final boolean DEFAULT_ROUTE_TO_LEADER = true; + static final boolean DEFAULT_DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE = false; + static final boolean DEFAULT_KEEP_TRANSACTION_ALIVE = false; + static final boolean DEFAULT_TRACK_SESSION_LEAKS = true; + static final boolean DEFAULT_TRACK_CONNECTION_LEAKS = true; + static final boolean DEFAULT_DATA_BOOST_ENABLED = false; + static final boolean DEFAULT_AUTO_PARTITION_MODE = false; + static final int DEFAULT_MAX_PARTITIONS = 0; + static final int DEFAULT_MAX_PARTITIONED_PARALLELISM = 1; + static final Boolean DEFAULT_ENABLE_EXTENDED_TRACING = null; + static final Boolean DEFAULT_ENABLE_API_TRACING = null; private static final String PLAIN_TEXT_PROTOCOL = "http:"; private static final String HOST_PROTOCOL = "https:"; @@ -209,7 +256,7 @@ public String[] getValidValues() { private static final String SPANNER_EMULATOR_HOST_ENV_VAR = "SPANNER_EMULATOR_HOST"; private static final String DEFAULT_EMULATOR_HOST = "http://localhost:9010"; /** Use plain text is only for local testing purposes. */ - private static final String USE_PLAIN_TEXT_PROPERTY_NAME = "usePlainText"; + static final String USE_PLAIN_TEXT_PROPERTY_NAME = "usePlainText"; /** Name of the 'autocommit' connection property. */ public static final String AUTOCOMMIT_PROPERTY_NAME = "autocommit"; /** Name of the 'readonly' connection property. */ @@ -252,12 +299,11 @@ public String[] getValidValues() { public static final String ENABLE_CHANNEL_PROVIDER_SYSTEM_PROPERTY = "ENABLE_CHANNEL_PROVIDER"; /** Custom user agent string is only for other Google libraries. */ - private static final String USER_AGENT_PROPERTY_NAME = "userAgent"; + static final String USER_AGENT_PROPERTY_NAME = "userAgent"; /** Query optimizer version to use for a connection. */ - private static final String OPTIMIZER_VERSION_PROPERTY_NAME = "optimizerVersion"; + static final String OPTIMIZER_VERSION_PROPERTY_NAME = "optimizerVersion"; /** Query optimizer statistics package to use for a connection. */ - private static final String OPTIMIZER_STATISTICS_PACKAGE_PROPERTY_NAME = - "optimizerStatisticsPackage"; + static final String OPTIMIZER_STATISTICS_PACKAGE_PROPERTY_NAME = "optimizerStatisticsPackage"; /** Name of the 'lenientMode' connection property. */ public static final String LENIENT_PROPERTY_NAME = "lenient"; /** Name of the 'rpcPriority' connection property. */ @@ -265,7 +311,7 @@ public String[] getValidValues() { public static final String DDL_IN_TRANSACTION_MODE_PROPERTY_NAME = "ddlInTransactionMode"; /** Dialect to use for a connection. */ - private static final String DIALECT_PROPERTY_NAME = "dialect"; + static final String DIALECT_PROPERTY_NAME = "dialect"; /** Name of the 'databaseRole' connection property. */ public static final String DATABASE_ROLE_PROPERTY_NAME = "databaseRole"; /** Name of the 'delay transaction start until first write' property. */ @@ -300,7 +346,17 @@ private static String generateGuardedConnectionPropertyError( systemPropertyName); } - /** All valid connection properties. */ + static boolean isEnableTransactionalConnectionStateForPostgreSQL() { + return Boolean.parseBoolean( + System.getProperty(ENABLE_TRANSACTIONAL_CONNECTION_STATE_FOR_POSTGRESQL_PROPERTY, "false")); + } + + /** + * All valid connection properties. + * + * @deprecated Replaced by {@link ConnectionProperties#CONNECTION_PROPERTIES} + */ + @Deprecated public static final Set VALID_PROPERTIES = Collections.unmodifiableSet( new HashSet<>( @@ -373,7 +429,8 @@ private static String generateGuardedConnectionPropertyError( "Sets the default query optimizer version to use for this connection."), ConnectionProperty.createStringProperty( OPTIMIZER_STATISTICS_PACKAGE_PROPERTY_NAME, ""), - ConnectionProperty.createBooleanProperty("returnCommitStats", "", false), + ConnectionProperty.createBooleanProperty( + "returnCommitStats", "", DEFAULT_RETURN_COMMIT_STATS), ConnectionProperty.createStringProperty( "maxCommitDelay", "The maximum commit delay in milliseconds that should be applied to commit requests from this connection."), @@ -534,16 +591,15 @@ public interface ExternalChannelProvider { /** Builder for {@link ConnectionOptions} instances. */ public static class Builder { + private final Map> connectionPropertyValues = + new HashMap<>(); private String uri; - private String credentialsUrl; - private String oauthToken; private Credentials credentials; private SessionPoolOptions sessionPoolOptions; private List statementExecutionInterceptors = Collections.emptyList(); private SpannerOptionsConfigurator configurator; private OpenTelemetry openTelemetry; - private String tracingPrefix; private Builder() {} @@ -626,11 +682,20 @@ public Builder setUri(String uri) { Preconditions.checkArgument( isValidUri(uri), "The specified URI is not a valid Cloud Spanner connection URI. Please specify a URI in the format \"cloudspanner:[//host[:port]]/projects/project-id[/instances/instance-id[/databases/database-name]][\\?property-name=property-value[;property-name=property-value]*]?\""); - checkValidProperties(uri); + ConnectionPropertyValue value = + cast(ConnectionProperties.parseValues(uri).get(LENIENT.getKey())); + checkValidProperties(value != null && value.getValue(), uri); this.uri = uri; return this; } + Builder setConnectionPropertyValue( + com.google.cloud.spanner.connection.ConnectionProperty property, T value) { + this.connectionPropertyValues.put( + property.getKey(), new ConnectionPropertyValue<>(property, value, value)); + return this; + } + /** Sets the {@link SessionPoolOptions} to use for the connection. */ public Builder setSessionPoolOptions(SessionPoolOptions sessionPoolOptions) { Preconditions.checkNotNull(sessionPoolOptions); @@ -655,7 +720,7 @@ public Builder setSessionPoolOptions(SessionPoolOptions sessionPoolOptions) { * @return this builder */ public Builder setCredentialsUrl(String credentialsUrl) { - this.credentialsUrl = credentialsUrl; + setConnectionPropertyValue(CREDENTIALS_URL, credentialsUrl); return this; } @@ -671,7 +736,7 @@ public Builder setCredentialsUrl(String credentialsUrl) { * @return this builder */ public Builder setOAuthToken(String oauthToken) { - this.oauthToken = oauthToken; + setConnectionPropertyValue(OAUTH_TOKEN, oauthToken); return this; } @@ -699,7 +764,7 @@ public Builder setOpenTelemetry(OpenTelemetry openTelemetry) { } public Builder setTracingPrefix(String tracingPrefix) { - this.tracingPrefix = tracingPrefix; + setConnectionPropertyValue(TRACING_PREFIX, tracingPrefix); return this; } @@ -720,55 +785,19 @@ public static Builder newBuilder() { return new Builder(); } + private final ConnectionState initialConnectionState; private final String uri; private final String warnings; - private final String credentialsUrl; - private final String encodedCredentials; - private final CredentialsProvider credentialsProvider; - private final String oauthToken; private final Credentials fixedCredentials; - private final boolean usePlainText; private final String host; private final String projectId; private final String instanceId; private final String databaseName; private final Credentials credentials; private final SessionPoolOptions sessionPoolOptions; - private final Integer numChannels; - private final String channelProvider; - private final Integer minSessions; - private final Integer maxSessions; - private final String databaseRole; - private final String userAgent; - private final QueryOptions queryOptions; - private final boolean returnCommitStats; - private final Long maxCommitDelay; - private final boolean autoConfigEmulator; - private final boolean useAutoSavepointsForEmulator; - private final Dialect dialect; - private final RpcPriority rpcPriority; - private final DdlInTransactionMode ddlInTransactionMode; - private final boolean delayTransactionStartUntilFirstWrite; - private final boolean keepTransactionAlive; - private final boolean trackSessionLeaks; - private final boolean trackConnectionLeaks; - - private final boolean dataBoostEnabled; - private final boolean autoPartitionMode; - private final int maxPartitions; - private final int maxPartitionedParallelism; - - private final boolean autocommit; - private final boolean readOnly; - private final boolean routeToLeader; - private final boolean retryAbortsInternally; - private final boolean useVirtualThreads; - private final boolean useVirtualGrpcTransportThreads; + private final OpenTelemetry openTelemetry; - private final String tracingPrefix; - private final Boolean enableExtendedTracing; - private final Boolean enableApiTracing; private final List statementExecutionInterceptors; private final SpannerOptionsConfigurator configurator; @@ -776,72 +805,79 @@ private ConnectionOptions(Builder builder) { Matcher matcher = Builder.SPANNER_URI_PATTERN.matcher(builder.uri); Preconditions.checkArgument( matcher.find(), String.format("Invalid connection URI specified: %s", builder.uri)); - this.warnings = checkValidProperties(builder.uri); + ImmutableMap> connectionPropertyValues = + ImmutableMap.>builder() + .putAll(ConnectionProperties.parseValues(builder.uri)) + .putAll(builder.connectionPropertyValues) + .buildKeepingLast(); this.uri = builder.uri; - this.credentialsUrl = - builder.credentialsUrl != null ? builder.credentialsUrl : parseCredentials(builder.uri); - this.encodedCredentials = parseEncodedCredentials(builder.uri); - this.credentialsProvider = parseCredentialsProvider(builder.uri); - this.oauthToken = - builder.oauthToken != null ? builder.oauthToken : parseOAuthToken(builder.uri); + ConnectionPropertyValue value = cast(connectionPropertyValues.get(LENIENT.getKey())); + this.warnings = checkValidProperties(value != null && value.getValue(), uri); + this.fixedCredentials = builder.credentials; + + this.openTelemetry = builder.openTelemetry; + this.statementExecutionInterceptors = + Collections.unmodifiableList(builder.statementExecutionInterceptors); + this.configurator = builder.configurator; + + // Create the initial connection state from the parsed properties in the connection URL. + this.initialConnectionState = new ConnectionState(connectionPropertyValues); + // Check that at most one of credentials location, encoded credentials, credentials provider and // OUAuth token has been specified in the connection URI. Preconditions.checkArgument( Stream.of( - this.credentialsUrl, - this.encodedCredentials, - this.credentialsProvider, - this.oauthToken) + getInitialConnectionPropertyValue(CREDENTIALS_URL), + getInitialConnectionPropertyValue(ENCODED_CREDENTIALS), + getInitialConnectionPropertyValue(CREDENTIALS_PROVIDER), + getInitialConnectionPropertyValue(OAUTH_TOKEN)) .filter(Objects::nonNull) .count() <= 1, "Specify only one of credentialsUrl, encodedCredentials, credentialsProvider and OAuth token"); - this.fixedCredentials = builder.credentials; + checkGuardedProperty( + getInitialConnectionPropertyValue(ENCODED_CREDENTIALS), + ENABLE_ENCODED_CREDENTIALS_SYSTEM_PROPERTY, + ENCODED_CREDENTIALS_PROPERTY_NAME); + checkGuardedProperty( + getInitialConnectionPropertyValue(CREDENTIALS_PROVIDER) == null + ? null + : getInitialConnectionPropertyValue(CREDENTIALS_PROVIDER).getClass().getName(), + ENABLE_CREDENTIALS_PROVIDER_SYSTEM_PROPERTY, + CREDENTIALS_PROVIDER_PROPERTY_NAME); + checkGuardedProperty( + getInitialConnectionPropertyValue(CHANNEL_PROVIDER), + ENABLE_CHANNEL_PROVIDER_SYSTEM_PROPERTY, + CHANNEL_PROVIDER_PROPERTY_NAME); - this.userAgent = parseUserAgent(this.uri); - QueryOptions.Builder queryOptionsBuilder = QueryOptions.newBuilder(); - queryOptionsBuilder.setOptimizerVersion(parseOptimizerVersion(this.uri)); - queryOptionsBuilder.setOptimizerStatisticsPackage(parseOptimizerStatisticsPackage(this.uri)); - this.queryOptions = queryOptionsBuilder.build(); - this.returnCommitStats = parseReturnCommitStats(this.uri); - this.maxCommitDelay = parseMaxCommitDelay(this.uri); - this.autoConfigEmulator = parseAutoConfigEmulator(this.uri); - this.useAutoSavepointsForEmulator = parseUseAutoSavepointsForEmulator(this.uri); - this.dialect = parseDialect(this.uri); - this.usePlainText = this.autoConfigEmulator || parseUsePlainText(this.uri); + boolean usePlainText = + getInitialConnectionPropertyValue(AUTO_CONFIG_EMULATOR) + || getInitialConnectionPropertyValue(USE_PLAIN_TEXT); this.host = determineHost( - matcher, parseEndpoint(this.uri), autoConfigEmulator, usePlainText, System.getenv()); - this.rpcPriority = parseRPCPriority(this.uri); - this.ddlInTransactionMode = parseDdlInTransactionMode(this.uri); - this.delayTransactionStartUntilFirstWrite = parseDelayTransactionStartUntilFirstWrite(this.uri); - this.keepTransactionAlive = parseKeepTransactionAlive(this.uri); - this.trackSessionLeaks = parseTrackSessionLeaks(this.uri); - this.trackConnectionLeaks = parseTrackConnectionLeaks(this.uri); - - this.dataBoostEnabled = parseDataBoostEnabled(this.uri); - this.autoPartitionMode = parseAutoPartitionMode(this.uri); - this.maxPartitions = parseMaxPartitions(this.uri); - this.maxPartitionedParallelism = parseMaxPartitionedParallelism(this.uri); - - this.instanceId = matcher.group(Builder.INSTANCE_GROUP); - this.databaseName = matcher.group(Builder.DATABASE_GROUP); + matcher, + getInitialConnectionPropertyValue(ENDPOINT), + getInitialConnectionPropertyValue(AUTO_CONFIG_EMULATOR), + usePlainText, + System.getenv()); // Using credentials on a plain text connection is not allowed, so if the user has not specified // any credentials and is using a plain text connection, we should not try to get the // credentials from the environment, but default to NoCredentials. if (this.fixedCredentials == null - && this.credentialsUrl == null - && this.encodedCredentials == null - && this.credentialsProvider == null - && this.oauthToken == null - && this.usePlainText) { + && getInitialConnectionPropertyValue(CREDENTIALS_URL) == null + && getInitialConnectionPropertyValue(ENCODED_CREDENTIALS) == null + && getInitialConnectionPropertyValue(CREDENTIALS_PROVIDER) == null + && getInitialConnectionPropertyValue(OAUTH_TOKEN) == null + && usePlainText) { this.credentials = NoCredentials.getInstance(); - } else if (this.oauthToken != null) { - this.credentials = new GoogleCredentials(new AccessToken(oauthToken, null)); - } else if (this.credentialsProvider != null) { + } else if (getInitialConnectionPropertyValue(OAUTH_TOKEN) != null) { + this.credentials = + new GoogleCredentials( + new AccessToken(getInitialConnectionPropertyValue(OAUTH_TOKEN), null)); + } else if (getInitialConnectionPropertyValue(CREDENTIALS_PROVIDER) != null) { try { - this.credentials = this.credentialsProvider.getCredentials(); + this.credentials = getInitialConnectionPropertyValue(CREDENTIALS_PROVIDER).getCredentials(); } catch (IOException exception) { throw SpannerExceptionFactory.newSpannerException( ErrorCode.INVALID_ARGUMENT, @@ -850,52 +886,31 @@ private ConnectionOptions(Builder builder) { } } else if (this.fixedCredentials != null) { this.credentials = fixedCredentials; - } else if (this.encodedCredentials != null) { - this.credentials = getCredentialsService().decodeCredentials(this.encodedCredentials); + } else if (getInitialConnectionPropertyValue(ENCODED_CREDENTIALS) != null) { + this.credentials = + getCredentialsService() + .decodeCredentials(getInitialConnectionPropertyValue(ENCODED_CREDENTIALS)); } else { - this.credentials = getCredentialsService().createCredentials(this.credentialsUrl); + this.credentials = + getCredentialsService() + .createCredentials(getInitialConnectionPropertyValue(CREDENTIALS_URL)); } - this.minSessions = - parseIntegerProperty(MIN_SESSIONS_PROPERTY_NAME, parseMinSessions(builder.uri)); - this.maxSessions = - parseIntegerProperty(MAX_SESSIONS_PROPERTY_NAME, parseMaxSessions(builder.uri)); - this.numChannels = - parseIntegerProperty(NUM_CHANNELS_PROPERTY_NAME, parseNumChannels(builder.uri)); - this.channelProvider = parseChannelProvider(builder.uri); - this.databaseRole = parseDatabaseRole(this.uri); - String projectId = matcher.group(Builder.PROJECT_GROUP); - if (Builder.DEFAULT_PROJECT_ID_PLACEHOLDER.equalsIgnoreCase(projectId)) { - projectId = getDefaultProjectId(this.credentials); - } - this.projectId = projectId; - - this.autocommit = parseAutocommit(this.uri); - this.readOnly = parseReadOnly(this.uri); - this.routeToLeader = parseRouteToLeader(this.uri); - this.retryAbortsInternally = parseRetryAbortsInternally(this.uri); - this.useVirtualThreads = parseUseVirtualThreads(this.uri); - this.useVirtualGrpcTransportThreads = parseUseVirtualGrpcTransportThreads(this.uri); - this.openTelemetry = builder.openTelemetry; - this.tracingPrefix = builder.tracingPrefix; - this.enableExtendedTracing = parseEnableExtendedTracing(this.uri); - this.enableApiTracing = parseEnableApiTracing(this.uri); - this.statementExecutionInterceptors = - Collections.unmodifiableList(builder.statementExecutionInterceptors); - this.configurator = builder.configurator; - - if (this.minSessions != null || this.maxSessions != null || !this.trackSessionLeaks) { + if (getInitialConnectionPropertyValue(MIN_SESSIONS) != null + || getInitialConnectionPropertyValue(MAX_SESSIONS) != null + || !getInitialConnectionPropertyValue(TRACK_SESSION_LEAKS)) { SessionPoolOptions.Builder sessionPoolOptionsBuilder = builder.sessionPoolOptions == null ? SessionPoolOptions.newBuilder() : builder.sessionPoolOptions.toBuilder(); - sessionPoolOptionsBuilder.setTrackStackTraceOfSessionCheckout(this.trackSessionLeaks); + sessionPoolOptionsBuilder.setTrackStackTraceOfSessionCheckout( + getInitialConnectionPropertyValue(TRACK_SESSION_LEAKS)); sessionPoolOptionsBuilder.setAutoDetectDialect(true); - if (this.minSessions != null) { - sessionPoolOptionsBuilder.setMinSessions(this.minSessions); + if (getInitialConnectionPropertyValue(MIN_SESSIONS) != null) { + sessionPoolOptionsBuilder.setMinSessions(getInitialConnectionPropertyValue(MIN_SESSIONS)); } - if (this.maxSessions != null) { - sessionPoolOptionsBuilder.setMaxSessions(this.maxSessions); + if (getInitialConnectionPropertyValue(MAX_SESSIONS) != null) { + sessionPoolOptionsBuilder.setMaxSessions(getInitialConnectionPropertyValue(MAX_SESSIONS)); } this.sessionPoolOptions = sessionPoolOptionsBuilder.build(); } else if (builder.sessionPoolOptions != null) { @@ -903,6 +918,14 @@ private ConnectionOptions(Builder builder) { } else { this.sessionPoolOptions = SessionPoolOptions.newBuilder().setAutoDetectDialect(true).build(); } + + String projectId = matcher.group(Builder.PROJECT_GROUP); + if (Builder.DEFAULT_PROJECT_ID_PLACEHOLDER.equalsIgnoreCase(projectId)) { + projectId = getDefaultProjectId(this.credentials); + } + this.projectId = projectId; + this.instanceId = matcher.group(Builder.INSTANCE_GROUP); + this.databaseName = matcher.group(Builder.DATABASE_GROUP); } @VisibleForTesting @@ -937,20 +960,6 @@ static String determineHost( return HOST_PROTOCOL + host; } - private static Integer parseIntegerProperty(String propertyName, String value) { - if (value != null) { - try { - return Integer.valueOf(value); - } catch (NumberFormatException e) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.INVALID_ARGUMENT, - String.format("Invalid %s value specified: %s", propertyName, value), - e); - } - } - return null; - } - /** * @return an instance of OpenTelemetry. If OpenTelemetry object is not set then null * will be returned. @@ -959,15 +968,6 @@ OpenTelemetry getOpenTelemetry() { return this.openTelemetry; } - /** - * @return The prefix that will be added to all traces that are started by the Connection API. - * This property is used by for example the JDBC driver to make sure all traces start with - * CloudSpannerJdbc. - */ - String getTracingPrefix() { - return this.tracingPrefix; - } - SpannerOptionsConfigurator getConfigurator() { return configurator; } @@ -977,103 +977,6 @@ CredentialsService getCredentialsService() { return CredentialsService.INSTANCE; } - @VisibleForTesting - static boolean parseUsePlainText(String uri) { - String value = parseUriProperty(uri, USE_PLAIN_TEXT_PROPERTY_NAME); - return value != null ? Boolean.parseBoolean(value) : DEFAULT_USE_PLAIN_TEXT; - } - - @VisibleForTesting - static boolean parseAutocommit(String uri) { - String value = parseUriProperty(uri, AUTOCOMMIT_PROPERTY_NAME); - return value != null ? Boolean.parseBoolean(value) : DEFAULT_AUTOCOMMIT; - } - - @VisibleForTesting - static boolean parseReadOnly(String uri) { - String value = parseUriProperty(uri, READONLY_PROPERTY_NAME); - return value != null ? Boolean.parseBoolean(value) : DEFAULT_READONLY; - } - - static boolean parseRouteToLeader(String uri) { - String value = parseUriProperty(uri, ROUTE_TO_LEADER_PROPERTY_NAME); - return value != null ? Boolean.parseBoolean(value) : DEFAULT_ROUTE_TO_LEADER; - } - - @VisibleForTesting - static boolean parseRetryAbortsInternally(String uri) { - String value = parseUriProperty(uri, RETRY_ABORTS_INTERNALLY_PROPERTY_NAME); - return value != null ? Boolean.parseBoolean(value) : DEFAULT_RETRY_ABORTS_INTERNALLY; - } - - @VisibleForTesting - static boolean parseUseVirtualThreads(String uri) { - String value = parseUriProperty(uri, USE_VIRTUAL_THREADS_PROPERTY_NAME); - return value != null ? Boolean.parseBoolean(value) : DEFAULT_USE_VIRTUAL_THREADS; - } - - @VisibleForTesting - static boolean parseUseVirtualGrpcTransportThreads(String uri) { - String value = parseUriProperty(uri, USE_VIRTUAL_GRPC_TRANSPORT_THREADS_PROPERTY_NAME); - return value != null ? Boolean.parseBoolean(value) : DEFAULT_USE_VIRTUAL_GRPC_TRANSPORT_THREADS; - } - - @VisibleForTesting - static @Nullable String parseCredentials(String uri) { - String value = parseUriProperty(uri, CREDENTIALS_PROPERTY_NAME); - return value != null ? value : DEFAULT_CREDENTIALS; - } - - @VisibleForTesting - static @Nullable String parseEncodedCredentials(String uri) { - String encodedCredentials = parseUriProperty(uri, ENCODED_CREDENTIALS_PROPERTY_NAME); - checkGuardedProperty( - encodedCredentials, - ENABLE_ENCODED_CREDENTIALS_SYSTEM_PROPERTY, - ENCODED_CREDENTIALS_PROPERTY_NAME); - return encodedCredentials; - } - - @VisibleForTesting - static @Nullable CredentialsProvider parseCredentialsProvider(String uri) { - String credentialsProviderName = parseUriProperty(uri, CREDENTIALS_PROVIDER_PROPERTY_NAME); - checkGuardedProperty( - credentialsProviderName, - ENABLE_CREDENTIALS_PROVIDER_SYSTEM_PROPERTY, - CREDENTIALS_PROVIDER_PROPERTY_NAME); - if (!Strings.isNullOrEmpty(credentialsProviderName)) { - try { - Class clazz = - (Class) Class.forName(credentialsProviderName); - Constructor constructor = clazz.getDeclaredConstructor(); - return constructor.newInstance(); - } catch (ClassNotFoundException classNotFoundException) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.INVALID_ARGUMENT, - "Unknown or invalid CredentialsProvider class name: " + credentialsProviderName, - classNotFoundException); - } catch (NoSuchMethodException noSuchMethodException) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.INVALID_ARGUMENT, - "Credentials provider " - + credentialsProviderName - + " does not have a public no-arg constructor.", - noSuchMethodException); - } catch (InvocationTargetException - | InstantiationException - | IllegalAccessException exception) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.INVALID_ARGUMENT, - "Failed to create an instance of " - + credentialsProviderName - + ": " - + exception.getMessage(), - exception); - } - } - return null; - } - private static void checkGuardedProperty( String value, String systemPropertyName, String connectionPropertyName) { if (!Strings.isNullOrEmpty(value) @@ -1084,219 +987,6 @@ private static void checkGuardedProperty( } } - @VisibleForTesting - static @Nullable String parseOAuthToken(String uri) { - String value = parseUriProperty(uri, OAUTH_TOKEN_PROPERTY_NAME); - return value != null ? value : DEFAULT_OAUTH_TOKEN; - } - - @VisibleForTesting - static String parseMinSessions(String uri) { - String value = parseUriProperty(uri, MIN_SESSIONS_PROPERTY_NAME); - return value != null ? value : DEFAULT_MIN_SESSIONS; - } - - @VisibleForTesting - static String parseMaxSessions(String uri) { - String value = parseUriProperty(uri, MAX_SESSIONS_PROPERTY_NAME); - return value != null ? value : DEFAULT_MAX_SESSIONS; - } - - @VisibleForTesting - static String parseNumChannels(String uri) { - String value = parseUriProperty(uri, NUM_CHANNELS_PROPERTY_NAME); - return value != null ? value : DEFAULT_NUM_CHANNELS; - } - - private static String parseEndpoint(String uri) { - String value = parseUriProperty(uri, ENDPOINT_PROPERTY_NAME); - return value != null ? value : DEFAULT_ENDPOINT; - } - - @VisibleForTesting - static String parseChannelProvider(String uri) { - String value = parseUriProperty(uri, CHANNEL_PROVIDER_PROPERTY_NAME); - checkGuardedProperty( - value, ENABLE_CHANNEL_PROVIDER_SYSTEM_PROPERTY, CHANNEL_PROVIDER_PROPERTY_NAME); - return value != null ? value : DEFAULT_CHANNEL_PROVIDER; - } - - @VisibleForTesting - static String parseDatabaseRole(String uri) { - String value = parseUriProperty(uri, DATABASE_ROLE_PROPERTY_NAME); - return value != null ? value : DEFAULT_DATABASE_ROLE; - } - - @VisibleForTesting - static String parseUserAgent(String uri) { - String value = parseUriProperty(uri, USER_AGENT_PROPERTY_NAME); - return value != null ? value : DEFAULT_USER_AGENT; - } - - @VisibleForTesting - static String parseOptimizerVersion(String uri) { - String value = parseUriProperty(uri, OPTIMIZER_VERSION_PROPERTY_NAME); - return value != null ? value : DEFAULT_OPTIMIZER_VERSION; - } - - @VisibleForTesting - static String parseOptimizerStatisticsPackage(String uri) { - String value = parseUriProperty(uri, OPTIMIZER_STATISTICS_PACKAGE_PROPERTY_NAME); - return value != null ? value : DEFAULT_OPTIMIZER_STATISTICS_PACKAGE; - } - - @VisibleForTesting - static boolean parseReturnCommitStats(String uri) { - String value = parseUriProperty(uri, "returnCommitStats"); - return Boolean.parseBoolean(value); - } - - @VisibleForTesting - static Long parseMaxCommitDelay(String uri) { - String value = parseUriProperty(uri, "maxCommitDelay"); - try { - Long millis = value == null ? null : Long.valueOf(value); - if (millis != null && millis < 0L) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.INVALID_ARGUMENT, "maxCommitDelay must be >=0"); - } - return millis; - } catch (NumberFormatException numberFormatException) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.INVALID_ARGUMENT, - "Invalid value for maxCommitDelay: " - + value - + "\n" - + "The value must be a positive integer indicating the number of " - + "milliseconds to use as the max delay."); - } - } - - static boolean parseAutoConfigEmulator(String uri) { - String value = parseUriProperty(uri, "autoConfigEmulator"); - return Boolean.parseBoolean(value); - } - - static boolean parseUseAutoSavepointsForEmulator(String uri) { - String value = parseUriProperty(uri, "useAutoSavepointsForEmulator"); - return Boolean.parseBoolean(value); - } - - @VisibleForTesting - static Dialect parseDialect(String uri) { - String value = parseUriProperty(uri, DIALECT_PROPERTY_NAME); - return value != null ? Dialect.valueOf(value.toUpperCase()) : Dialect.GOOGLE_STANDARD_SQL; - } - - @VisibleForTesting - static boolean parseLenient(String uri) { - String value = parseUriProperty(uri, LENIENT_PROPERTY_NAME); - return value != null ? Boolean.parseBoolean(value) : DEFAULT_LENIENT; - } - - @VisibleForTesting - static boolean parseDelayTransactionStartUntilFirstWrite(String uri) { - String value = parseUriProperty(uri, DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE_NAME); - return value != null - ? Boolean.parseBoolean(value) - : DEFAULT_DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE; - } - - @VisibleForTesting - static boolean parseKeepTransactionAlive(String uri) { - String value = parseUriProperty(uri, KEEP_TRANSACTION_ALIVE_PROPERTY_NAME); - return value != null ? Boolean.parseBoolean(value) : DEFAULT_KEEP_TRANSACTION_ALIVE; - } - - @VisibleForTesting - static boolean parseTrackSessionLeaks(String uri) { - String value = parseUriProperty(uri, TRACK_SESSION_LEAKS_PROPERTY_NAME); - return value != null ? Boolean.parseBoolean(value) : DEFAULT_TRACK_SESSION_LEAKS; - } - - @VisibleForTesting - static boolean parseTrackConnectionLeaks(String uri) { - String value = parseUriProperty(uri, TRACK_CONNECTION_LEAKS_PROPERTY_NAME); - return value != null ? Boolean.parseBoolean(value) : DEFAULT_TRACK_CONNECTION_LEAKS; - } - - @VisibleForTesting - static boolean parseDataBoostEnabled(String uri) { - String value = parseUriProperty(uri, DATA_BOOST_ENABLED_PROPERTY_NAME); - return value != null ? Boolean.parseBoolean(value) : DEFAULT_DATA_BOOST_ENABLED; - } - - @VisibleForTesting - static boolean parseAutoPartitionMode(String uri) { - String value = parseUriProperty(uri, AUTO_PARTITION_MODE_PROPERTY_NAME); - return value != null ? Boolean.parseBoolean(value) : DEFAULT_AUTO_PARTITION_MODE; - } - - @VisibleForTesting - static int parseMaxPartitions(String uri) { - String stringValue = parseUriProperty(uri, MAX_PARTITIONS_PROPERTY_NAME); - if (stringValue == null) { - return DEFAULT_MAX_PARTITIONS; - } - try { - int value = Integer.parseInt(stringValue); - if (value < 0) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.INVALID_ARGUMENT, "maxPartitions must be >=0"); - } - return value; - } catch (NumberFormatException numberFormatException) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.INVALID_ARGUMENT, "Invalid value for maxPartitions: " + stringValue); - } - } - - @VisibleForTesting - static int parseMaxPartitionedParallelism(String uri) { - String stringValue = parseUriProperty(uri, MAX_PARTITIONED_PARALLELISM_PROPERTY_NAME); - if (stringValue == null) { - return DEFAULT_MAX_PARTITIONED_PARALLELISM; - } - try { - int value = Integer.parseInt(stringValue); - if (value < 0) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.INVALID_ARGUMENT, "maxPartitionedParallelism must be >=0"); - } - return value; - } catch (NumberFormatException numberFormatException) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.INVALID_ARGUMENT, - "Invalid value for maxPartitionedParallelism: " + stringValue); - } - } - - @VisibleForTesting - static RpcPriority parseRPCPriority(String uri) { - String value = parseUriProperty(uri, RPC_PRIORITY_NAME); - return value != null ? RpcPriority.valueOf(value) : DEFAULT_RPC_PRIORITY; - } - - @VisibleForTesting - static DdlInTransactionMode parseDdlInTransactionMode(String uri) { - String value = parseUriProperty(uri, DDL_IN_TRANSACTION_MODE_PROPERTY_NAME); - return value != null - ? DdlInTransactionMode.valueOf(value.toUpperCase()) - : DEFAULT_DDL_IN_TRANSACTION_MODE; - } - - @VisibleForTesting - static Boolean parseEnableExtendedTracing(String uri) { - String value = parseUriProperty(uri, ENABLE_EXTENDED_TRACING_PROPERTY_NAME); - return value != null ? Boolean.valueOf(value) : DEFAULT_ENABLE_EXTENDED_TRACING; - } - - @VisibleForTesting - static Boolean parseEnableApiTracing(String uri) { - String value = parseUriProperty(uri, ENABLE_API_TRACING_PROPERTY_NAME); - return value != null ? Boolean.valueOf(value) : DEFAULT_ENABLE_API_TRACING; - } - @VisibleForTesting static String parseUriProperty(String uri, String property) { Pattern pattern = Pattern.compile(String.format("(?is)(?:;|\\?)%s=(.*?)(?:;|$)", property)); @@ -1309,23 +999,23 @@ static String parseUriProperty(String uri, String property) { /** Check that only valid properties have been specified. */ @VisibleForTesting - static String checkValidProperties(String uri) { - String invalidProperties = ""; + static String checkValidProperties(boolean lenient, String uri) { + StringBuilder invalidProperties = new StringBuilder(); List properties = parseProperties(uri); - boolean lenient = parseLenient(uri); for (String property : properties) { - if (!INTERNAL_VALID_PROPERTIES.contains(ConnectionProperty.createEmptyProperty(property))) { + if (!ConnectionProperties.CONNECTION_PROPERTIES.containsKey( + property.toLowerCase(Locale.ENGLISH))) { if (invalidProperties.length() > 0) { - invalidProperties = invalidProperties + ", "; + invalidProperties.append(", "); } - invalidProperties = invalidProperties + property; + invalidProperties.append(property); } } if (lenient) { return String.format("Invalid properties found in connection URI: %s", invalidProperties); } else { Preconditions.checkArgument( - invalidProperties.isEmpty(), + invalidProperties.length() == 0, String.format( "Invalid properties found in connection URI. Add lenient=true to the connection string to ignore unknown properties. Invalid properties: %s", invalidProperties)); @@ -1369,13 +1059,23 @@ public String getUri() { return uri; } + /** The connection properties that have been pre-set for this {@link ConnectionOptions}. */ + Map> getInitialConnectionPropertyValues() { + return this.initialConnectionState.getAllValues(); + } + + T getInitialConnectionPropertyValue( + com.google.cloud.spanner.connection.ConnectionProperty property) { + return this.initialConnectionState.getValue(property).getValue(); + } + /** The credentials URL of this {@link ConnectionOptions} */ public String getCredentialsUrl() { - return credentialsUrl; + return getInitialConnectionPropertyValue(CREDENTIALS_URL); } String getOAuthToken() { - return this.oauthToken; + return getInitialConnectionPropertyValue(OAUTH_TOKEN); } Credentials getFixedCredentials() { @@ -1383,7 +1083,7 @@ Credentials getFixedCredentials() { } CredentialsProvider getCredentialsProvider() { - return this.credentialsProvider; + return getInitialConnectionPropertyValue(CREDENTIALS_PROVIDER); } /** The {@link SessionPoolOptions} of this {@link ConnectionOptions}. */ @@ -1397,7 +1097,7 @@ public SessionPoolOptions getSessionPoolOptions() { * database using the same connection settings. */ public Integer getMinSessions() { - return minSessions; + return getInitialConnectionPropertyValue(MIN_SESSIONS); } /** @@ -1406,16 +1106,17 @@ public Integer getMinSessions() { * database using the same connection settings. */ public Integer getMaxSessions() { - return maxSessions; + return getInitialConnectionPropertyValue(MAX_SESSIONS); } /** The number of channels to use for the connection. */ public Integer getNumChannels() { - return numChannels; + return getInitialConnectionPropertyValue(NUM_CHANNELS); } /** Calls the getChannelProvider() method from the supplied class. */ public TransportChannelProvider getChannelProvider() { + String channelProvider = getInitialConnectionPropertyValue(CHANNEL_PROVIDER); if (channelProvider == null) { return null; } @@ -1438,7 +1139,7 @@ public TransportChannelProvider getChannelProvider() { * used to for example restrict the access of a connection to a specific set of tables. */ public String getDatabaseRole() { - return databaseRole; + return getInitialConnectionPropertyValue(DATABASE_ROLE); } /** The host and port number that this {@link ConnectionOptions} will connect to */ @@ -1479,12 +1180,12 @@ public Credentials getCredentials() { /** The initial autocommit value for connections created by this {@link ConnectionOptions} */ public boolean isAutocommit() { - return autocommit; + return getInitialConnectionPropertyValue(AUTOCOMMIT); } /** The initial readonly value for connections created by this {@link ConnectionOptions} */ public boolean isReadOnly() { - return readOnly; + return getInitialConnectionPropertyValue(READONLY); } /** @@ -1492,7 +1193,7 @@ public boolean isReadOnly() { * region. */ public boolean isRouteToLeader() { - return routeToLeader; + return getInitialConnectionPropertyValue(ROUTE_TO_LEADER); } /** @@ -1500,17 +1201,17 @@ public boolean isRouteToLeader() { * ConnectionOptions} */ public boolean isRetryAbortsInternally() { - return retryAbortsInternally; + return getInitialConnectionPropertyValue(RETRY_ABORTS_INTERNALLY); } /** Whether connections should use virtual threads for connection executors. */ public boolean isUseVirtualThreads() { - return useVirtualThreads; + return getInitialConnectionPropertyValue(USE_VIRTUAL_THREADS); } /** Whether virtual threads should be used for gRPC transport. */ public boolean isUseVirtualGrpcTransportThreads() { - return useVirtualGrpcTransportThreads; + return getInitialConnectionPropertyValue(USE_VIRTUAL_GRPC_TRANSPORT_THREADS); } /** Any warnings that were generated while creating the {@link ConnectionOptions} instance. */ @@ -1521,7 +1222,8 @@ public String getWarnings() { /** Use http instead of https. Only valid for (local) test servers. */ boolean isUsePlainText() { - return usePlainText; + return getInitialConnectionPropertyValue(AUTO_CONFIG_EMULATOR) + || getInitialConnectionPropertyValue(USE_PLAIN_TEXT); } /** @@ -1529,28 +1231,23 @@ boolean isUsePlainText() { * default JDBC user agent string will be used. */ String getUserAgent() { - return userAgent; - } - - /** The {@link QueryOptions} to use for the connection. */ - QueryOptions getQueryOptions() { - return queryOptions; + return getInitialConnectionPropertyValue(USER_AGENT); } /** Whether connections created by this {@link ConnectionOptions} return commit stats. */ public boolean isReturnCommitStats() { - return returnCommitStats; + return getInitialConnectionPropertyValue(RETURN_COMMIT_STATS); } /** The max_commit_delay that should be applied to commit operations on this connection. */ public Duration getMaxCommitDelay() { - return maxCommitDelay == null ? null : Duration.ofMillis(maxCommitDelay); + return getInitialConnectionPropertyValue(MAX_COMMIT_DELAY); } boolean usesEmulator() { return Suppliers.memoize( () -> - this.autoConfigEmulator + isAutoConfigEmulator() || !Strings.isNullOrEmpty(System.getenv("SPANNER_EMULATOR_HOST"))) .get(); } @@ -1562,7 +1259,7 @@ boolean usesEmulator() { * emulator instance. */ public boolean isAutoConfigEmulator() { - return autoConfigEmulator; + return getInitialConnectionPropertyValue(AUTO_CONFIG_EMULATOR); } /** @@ -1572,68 +1269,39 @@ public boolean isAutoConfigEmulator() { *

This is no longer needed since version 1.5.23 of the emulator. */ boolean useAutoSavepointsForEmulator() { - return useAutoSavepointsForEmulator; + return getInitialConnectionPropertyValue(USE_AUTO_SAVEPOINTS_FOR_EMULATOR); } public Dialect getDialect() { - return dialect; - } - - /** The {@link RpcPriority} to use for the connection. */ - RpcPriority getRPCPriority() { - return rpcPriority; - } - - DdlInTransactionMode getDdlInTransactionMode() { - return this.ddlInTransactionMode; - } - - /** - * Whether connections created by this {@link ConnectionOptions} should delay the actual start of - * a read/write transaction until the first write operation. - */ - boolean isDelayTransactionStartUntilFirstWrite() { - return delayTransactionStartUntilFirstWrite; - } - - /** - * Whether connections created by this {@link ConnectionOptions} should keep read/write - * transactions alive by executing a SELECT 1 once every 10 seconds if no other statements are - * executed. This option should be used with caution, as enabling it can keep transactions alive - * for a very long time, which will hold on to any locks that have been taken by the transaction. - * This option should typically only be enabled for CLI-type applications or other user-input - * applications that might wait for a longer period of time on user input. - */ - boolean isKeepTransactionAlive() { - return keepTransactionAlive; + return getInitialConnectionPropertyValue(DIALECT); } boolean isTrackConnectionLeaks() { - return this.trackConnectionLeaks; + return getInitialConnectionPropertyValue(TRACK_CONNECTION_LEAKS); } boolean isDataBoostEnabled() { - return this.dataBoostEnabled; + return getInitialConnectionPropertyValue(DATA_BOOST_ENABLED); } boolean isAutoPartitionMode() { - return this.autoPartitionMode; + return getInitialConnectionPropertyValue(AUTO_PARTITION_MODE); } int getMaxPartitions() { - return this.maxPartitions; + return getInitialConnectionPropertyValue(MAX_PARTITIONS); } int getMaxPartitionedParallelism() { - return this.maxPartitionedParallelism; + return getInitialConnectionPropertyValue(MAX_PARTITIONED_PARALLELISM); } Boolean isEnableExtendedTracing() { - return this.enableExtendedTracing; + return getInitialConnectionPropertyValue(ENABLE_EXTENDED_TRACING); } Boolean isEnableApiTracing() { - return this.enableApiTracing; + return getInitialConnectionPropertyValue(ENABLE_API_TRACING); } /** Interceptors that should be executed after each statement */ 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 new file mode 100644 index 00000000000..b18326e015d --- /dev/null +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionProperties.java @@ -0,0 +1,515 @@ +/* + * Copyright 2024 Google LLC + * + * 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.google.cloud.spanner.connection; + +import static com.google.cloud.spanner.connection.ConnectionOptions.AUTOCOMMIT_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.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; +import static com.google.cloud.spanner.connection.ConnectionOptions.DATA_BOOST_ENABLED_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.DDL_IN_TRANSACTION_MODE_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_AUTOCOMMIT; +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_CREDENTIALS; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_DATABASE_ROLE; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_DATA_BOOST_ENABLED; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_DDL_IN_TRANSACTION_MODE; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_ENABLE_API_TRACING; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_ENABLE_EXTENDED_TRACING; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_ENDPOINT; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_KEEP_TRANSACTION_ALIVE; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_LENIENT; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_MAX_PARTITIONED_PARALLELISM; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_MAX_PARTITIONS; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_MAX_SESSIONS; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_MIN_SESSIONS; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_NUM_CHANNELS; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_OAUTH_TOKEN; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_OPTIMIZER_STATISTICS_PACKAGE; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_OPTIMIZER_VERSION; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_READONLY; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_RETRY_ABORTS_INTERNALLY; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_RETURN_COMMIT_STATS; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_ROUTE_TO_LEADER; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_RPC_PRIORITY; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_TRACK_CONNECTION_LEAKS; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_TRACK_SESSION_LEAKS; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_USER_AGENT; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_USE_PLAIN_TEXT; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_USE_VIRTUAL_GRPC_TRANSPORT_THREADS; +import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_USE_VIRTUAL_THREADS; +import static com.google.cloud.spanner.connection.ConnectionOptions.DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.DIALECT_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.ENABLE_API_TRACING_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.ENABLE_EXTENDED_TRACING_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.ENCODED_CREDENTIALS_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.ENDPOINT_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.KEEP_TRANSACTION_ALIVE_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.LENIENT_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.MAX_PARTITIONED_PARALLELISM_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.MAX_PARTITIONS_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.MAX_SESSIONS_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.MIN_SESSIONS_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.NUM_CHANNELS_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.OAUTH_TOKEN_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.OPTIMIZER_STATISTICS_PACKAGE_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.OPTIMIZER_VERSION_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.READONLY_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.RETRY_ABORTS_INTERNALLY_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.ROUTE_TO_LEADER_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.RPC_PRIORITY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.TRACK_CONNECTION_LEAKS_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.TRACK_SESSION_LEAKS_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.USER_AGENT_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.USE_PLAIN_TEXT_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.USE_VIRTUAL_GRPC_TRANSPORT_THREADS_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionOptions.USE_VIRTUAL_THREADS_PROPERTY_NAME; +import static com.google.cloud.spanner.connection.ConnectionProperty.castProperty; + +import com.google.api.gax.core.CredentialsProvider; +import com.google.cloud.spanner.Dialect; +import com.google.cloud.spanner.Options.RpcPriority; +import com.google.cloud.spanner.TimestampBound; +import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.AutocommitDmlModeConverter; +import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.BooleanConverter; +import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.ConnectionStateTypeConverter; +import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.CredentialsProviderConverter; +import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.DdlInTransactionModeConverter; +import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.DialectConverter; +import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.DurationConverter; +import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.NonNegativeIntegerConverter; +import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.ReadOnlyStalenessConverter; +import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.RpcPriorityConverter; +import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.SavepointSupportConverter; +import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.StringValueConverter; +import com.google.cloud.spanner.connection.ConnectionProperty.Context; +import com.google.cloud.spanner.connection.DirectedReadOptionsUtil.DirectedReadOptionsConverter; +import com.google.common.collect.ImmutableMap; +import com.google.spanner.v1.DirectedReadOptions; +import java.time.Duration; +import java.util.Map; + +/** + * Utility class that defines all known connection properties. This class will eventually replace + * the list of {@link com.google.cloud.spanner.connection.ConnectionOptions.ConnectionProperty} in + * {@link ConnectionOptions}. + */ +class ConnectionProperties { + private static final ImmutableMap.Builder> + CONNECTION_PROPERTIES_BUILDER = ImmutableMap.builder(); + + static final ConnectionProperty CONNECTION_STATE_TYPE = + create( + "connection_state_type", + "The type of connection state to use for this connection. Can only be set at start up. " + + "If no value is set, then the database dialect default will be used, " + + "which is NON_TRANSACTIONAL for GoogleSQL and TRANSACTIONAL for PostgreSQL.", + null, + ConnectionStateTypeConverter.INSTANCE, + Context.STARTUP); + static final ConnectionProperty TRACING_PREFIX = + create( + "tracing_prefix", + "The prefix that will be prepended to all OpenTelemetry traces that are " + + "generated by a Connection.", + "CloudSpanner", + StringValueConverter.INSTANCE, + Context.STARTUP); + static final ConnectionProperty LENIENT = + create( + LENIENT_PROPERTY_NAME, + "Silently ignore unknown properties in the connection string/properties (true/false)", + DEFAULT_LENIENT, + BooleanConverter.INSTANCE, + Context.STARTUP); + static final ConnectionProperty ENDPOINT = + create( + ENDPOINT_PROPERTY_NAME, + "The endpoint that the JDBC driver should connect to. " + + "The default is the default Spanner production endpoint when autoConfigEmulator=false, " + + "and the default Spanner emulator endpoint (localhost:9010) when autoConfigEmulator=true. " + + "This property takes precedence over any host name at the start of the connection URL.", + DEFAULT_ENDPOINT, + StringValueConverter.INSTANCE, + Context.STARTUP); + static final ConnectionProperty AUTO_CONFIG_EMULATOR = + create( + "autoConfigEmulator", + "Automatically configure the connection to try to connect to the Cloud Spanner emulator (true/false). " + + "The instance and database in the connection string will automatically be created if these do not yet exist on the emulator. " + + "Add dialect=postgresql to the connection string to make sure that the database that is created uses the PostgreSQL dialect.", + false, + BooleanConverter.INSTANCE, + Context.STARTUP); + static final ConnectionProperty USE_AUTO_SAVEPOINTS_FOR_EMULATOR = + create( + "useAutoSavepointsForEmulator", + "Automatically creates savepoints for each statement in a read/write transaction when using the Emulator. " + + "This is no longer needed when using Emulator version 1.5.23 or higher.", + false, + BooleanConverter.INSTANCE, + Context.STARTUP); + static final ConnectionProperty USE_PLAIN_TEXT = + create( + 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, + BooleanConverter.INSTANCE, + Context.STARTUP); + + static final ConnectionProperty CREDENTIALS_URL = + create( + CREDENTIALS_PROPERTY_NAME, + "The location of the credentials file to use for this connection. If neither this property or encoded credentials are set, the connection will use the default Google Cloud credentials for the runtime environment.", + DEFAULT_CREDENTIALS, + StringValueConverter.INSTANCE, + Context.STARTUP); + static final ConnectionProperty ENCODED_CREDENTIALS = + create( + ENCODED_CREDENTIALS_PROPERTY_NAME, + "Base64-encoded credentials to use for this connection. If neither this property or a credentials location are set, the connection will use the default Google Cloud credentials for the runtime environment.", + null, + StringValueConverter.INSTANCE, + Context.STARTUP); + static final ConnectionProperty OAUTH_TOKEN = + create( + OAUTH_TOKEN_PROPERTY_NAME, + "A valid pre-existing OAuth token to use for authentication for this connection. Setting this property will take precedence over any value set for a credentials file.", + DEFAULT_OAUTH_TOKEN, + StringValueConverter.INSTANCE, + Context.STARTUP); + static final ConnectionProperty CREDENTIALS_PROVIDER = + create( + CREDENTIALS_PROVIDER_PROPERTY_NAME, + "The class name of the com.google.api.gax.core.CredentialsProvider implementation that should be used to obtain credentials for connections.", + null, + CredentialsProviderConverter.INSTANCE, + Context.STARTUP); + + static final ConnectionProperty USER_AGENT = + create( + 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.", + DEFAULT_USER_AGENT, + StringValueConverter.INSTANCE, + Context.STARTUP); + static final ConnectionProperty DIALECT = + create( + DIALECT_PROPERTY_NAME, + "Sets the dialect to use for new databases that are created by this connection.", + Dialect.GOOGLE_STANDARD_SQL, + DialectConverter.INSTANCE, + Context.STARTUP); + static final ConnectionProperty TRACK_SESSION_LEAKS = + create( + TRACK_SESSION_LEAKS_PROPERTY_NAME, + "Capture the call stack of the thread that checked out a session of the session pool. This will " + + "pre-create a LeakedSessionException already when a session is checked out. This can be disabled, " + + "for example if a monitoring system logs the pre-created exception. " + + "If disabled, the LeakedSessionException will only be created when an " + + "actual session leak is detected. The stack trace of the exception will " + + "in that case not contain the call stack of when the session was checked out.", + DEFAULT_TRACK_SESSION_LEAKS, + BooleanConverter.INSTANCE, + Context.STARTUP); + static final ConnectionProperty TRACK_CONNECTION_LEAKS = + create( + TRACK_CONNECTION_LEAKS_PROPERTY_NAME, + "Capture the call stack of the thread that created a connection. This will " + + "pre-create a LeakedConnectionException already when a connection is created. " + + "This can be disabled, for example if a monitoring system logs the pre-created exception. " + + "If disabled, the LeakedConnectionException will only be created when an " + + "actual connection leak is detected. The stack trace of the exception will " + + "in that case not contain the call stack of when the connection was created.", + DEFAULT_TRACK_CONNECTION_LEAKS, + BooleanConverter.INSTANCE, + Context.STARTUP); + static final ConnectionProperty ROUTE_TO_LEADER = + create( + ROUTE_TO_LEADER_PROPERTY_NAME, + "Should read/write transactions and partitioned DML be routed to leader region (true/false)", + DEFAULT_ROUTE_TO_LEADER, + BooleanConverter.INSTANCE, + Context.STARTUP); + static final ConnectionProperty USE_VIRTUAL_THREADS = + create( + USE_VIRTUAL_THREADS_PROPERTY_NAME, + "Use a virtual thread instead of a platform thread for each connection (true/false). " + + "This option only has any effect if the application is running on Java 21 or higher. In all other cases, the option is ignored.", + DEFAULT_USE_VIRTUAL_THREADS, + BooleanConverter.INSTANCE, + Context.STARTUP); + static final ConnectionProperty USE_VIRTUAL_GRPC_TRANSPORT_THREADS = + create( + USE_VIRTUAL_GRPC_TRANSPORT_THREADS_PROPERTY_NAME, + "Use a virtual thread instead of a platform thread for the gRPC executor (true/false). " + + "This option only has any effect if the application is running on Java 21 or higher. In all other cases, the option is ignored.", + DEFAULT_USE_VIRTUAL_GRPC_TRANSPORT_THREADS, + BooleanConverter.INSTANCE, + Context.STARTUP); + static final ConnectionProperty ENABLE_EXTENDED_TRACING = + create( + ENABLE_EXTENDED_TRACING_PROPERTY_NAME, + "Include the SQL string in the OpenTelemetry traces that are generated " + + "by this connection. The SQL string is added as the standard OpenTelemetry " + + "attribute 'db.statement'.", + DEFAULT_ENABLE_EXTENDED_TRACING, + BooleanConverter.INSTANCE, + Context.STARTUP); + static final ConnectionProperty ENABLE_API_TRACING = + create( + ENABLE_API_TRACING_PROPERTY_NAME, + "Add OpenTelemetry traces for each individual RPC call. Enable this " + + "to get a detailed view of each RPC that is being executed by your application, " + + "or if you want to debug potential latency problems caused by RPCs that are " + + "being retried.", + DEFAULT_ENABLE_API_TRACING, + BooleanConverter.INSTANCE, + Context.STARTUP); + static final ConnectionProperty MIN_SESSIONS = + create( + MIN_SESSIONS_PROPERTY_NAME, + "The minimum number of sessions in the backing session pool. The default is 100.", + DEFAULT_MIN_SESSIONS, + NonNegativeIntegerConverter.INSTANCE, + Context.STARTUP); + static final ConnectionProperty MAX_SESSIONS = + create( + MAX_SESSIONS_PROPERTY_NAME, + "The maximum number of sessions in the backing session pool. The default is 400.", + DEFAULT_MAX_SESSIONS, + NonNegativeIntegerConverter.INSTANCE, + Context.STARTUP); + static final ConnectionProperty NUM_CHANNELS = + create( + NUM_CHANNELS_PROPERTY_NAME, + "The number of gRPC channels to use to communicate with Cloud Spanner. The default is 4.", + DEFAULT_NUM_CHANNELS, + NonNegativeIntegerConverter.INSTANCE, + Context.STARTUP); + static final ConnectionProperty CHANNEL_PROVIDER = + create( + CHANNEL_PROVIDER_PROPERTY_NAME, + "The name of the channel provider class. The name must reference an implementation of ExternalChannelProvider. If this property is not set, the connection will use the default grpc channel provider.", + DEFAULT_CHANNEL_PROVIDER, + StringValueConverter.INSTANCE, + Context.STARTUP); + static final ConnectionProperty DATABASE_ROLE = + create( + DATABASE_ROLE_PROPERTY_NAME, + "Sets the database role to use for this connection. The default is privileges assigned to IAM role", + DEFAULT_DATABASE_ROLE, + StringValueConverter.INSTANCE, + Context.STARTUP); + + static final ConnectionProperty AUTOCOMMIT = + create( + AUTOCOMMIT_PROPERTY_NAME, + "Should the connection start in autocommit (true/false)", + DEFAULT_AUTOCOMMIT, + BooleanConverter.INSTANCE, + Context.USER); + static final ConnectionProperty READONLY = + create( + READONLY_PROPERTY_NAME, + "Should the connection start in read-only mode (true/false)", + DEFAULT_READONLY, + BooleanConverter.INSTANCE, + Context.USER); + static final ConnectionProperty AUTOCOMMIT_DML_MODE = + create( + "autocommit_dml_mode", + "Should the connection automatically retry Aborted errors (true/false)", + AutocommitDmlMode.TRANSACTIONAL, + AutocommitDmlModeConverter.INSTANCE, + Context.USER); + static final ConnectionProperty RETRY_ABORTS_INTERNALLY = + create( + // TODO: Add support for synonyms for connection properties. + // retryAbortsInternally / retry_aborts_internally is currently not consistent. + // The connection URL property is retryAbortsInternally. The SET statement assumes + // that the property name is retry_aborts_internally. We should support both to be + // backwards compatible, but the standard should be snake_case. + RETRY_ABORTS_INTERNALLY_PROPERTY_NAME, + "Should the connection automatically retry Aborted errors (true/false)", + DEFAULT_RETRY_ABORTS_INTERNALLY, + BooleanConverter.INSTANCE, + Context.USER); + static final ConnectionProperty RETURN_COMMIT_STATS = + create( + "returnCommitStats", + "Request that Spanner returns commit statistics for read/write transactions (true/false)", + DEFAULT_RETURN_COMMIT_STATS, + BooleanConverter.INSTANCE, + Context.USER); + static final ConnectionProperty DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE = + create( + DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE_NAME, + "Enabling this option will delay the actual start of a read/write transaction until the first write operation is seen in that transaction. " + + "All reads that happen before the first write in a transaction will instead be executed as if the connection was in auto-commit mode. " + + "Enabling this option will make read/write transactions lose their SERIALIZABLE isolation level. Read operations that are executed after " + + "the first write operation in a read/write transaction will be executed using the read/write transaction. Enabling this mode can reduce locking " + + "and improve performance for applications that can handle the lower transaction isolation semantics.", + DEFAULT_DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE, + BooleanConverter.INSTANCE, + Context.USER); + static final ConnectionProperty KEEP_TRANSACTION_ALIVE = + create( + KEEP_TRANSACTION_ALIVE_PROPERTY_NAME, + "Enabling this option will trigger the connection to keep read/write transactions alive by executing a SELECT 1 query once every 10 seconds " + + "if no other statements are being executed. This option should be used with caution, as it can keep transactions alive and hold on to locks " + + "longer than intended. This option should typically be used for CLI-type application that might wait for user input for a longer period of time.", + DEFAULT_KEEP_TRANSACTION_ALIVE, + BooleanConverter.INSTANCE, + Context.USER); + + static final ConnectionProperty READ_ONLY_STALENESS = + create( + "read_only_staleness", + "The read-only staleness to use for read-only transactions and single-use queries.", + TimestampBound.strong(), + ReadOnlyStalenessConverter.INSTANCE, + Context.USER); + static final ConnectionProperty AUTO_PARTITION_MODE = + create( + AUTO_PARTITION_MODE_PROPERTY_NAME, + "Execute all queries on this connection as partitioned queries. " + + "Executing a query that cannot be partitioned will fail. " + + "Executing a query in a read/write transaction will also fail.", + DEFAULT_AUTO_PARTITION_MODE, + BooleanConverter.INSTANCE, + Context.USER); + static final ConnectionProperty DATA_BOOST_ENABLED = + create( + DATA_BOOST_ENABLED_PROPERTY_NAME, + "Enable data boost for all partitioned queries that are executed by this connection. " + + "This setting is only used for partitioned queries and is ignored by all other statements.", + DEFAULT_DATA_BOOST_ENABLED, + BooleanConverter.INSTANCE, + Context.USER); + static final ConnectionProperty MAX_PARTITIONS = + create( + MAX_PARTITIONS_PROPERTY_NAME, + "The max partitions hint value to use for partitioned queries. " + + "Use 0 if you do not want to specify a hint.", + DEFAULT_MAX_PARTITIONS, + NonNegativeIntegerConverter.INSTANCE, + Context.USER); + static final ConnectionProperty MAX_PARTITIONED_PARALLELISM = + create( + MAX_PARTITIONED_PARALLELISM_PROPERTY_NAME, + "The max partitions hint value to use for partitioned queries. " + + "Use 0 if you do not want to specify a hint.", + DEFAULT_MAX_PARTITIONED_PARALLELISM, + NonNegativeIntegerConverter.INSTANCE, + Context.USER); + + static final ConnectionProperty DIRECTED_READ = + create( + "directed_read", + "The directed read options to apply to read-only transactions.", + null, + DirectedReadOptionsConverter.INSTANCE, + Context.USER); + static final ConnectionProperty OPTIMIZER_VERSION = + create( + OPTIMIZER_VERSION_PROPERTY_NAME, + "Sets the default query optimizer version to use for this connection.", + DEFAULT_OPTIMIZER_VERSION, + StringValueConverter.INSTANCE, + Context.USER); + static final ConnectionProperty OPTIMIZER_STATISTICS_PACKAGE = + create( + OPTIMIZER_STATISTICS_PACKAGE_PROPERTY_NAME, + "Sets the query optimizer statistics package to use for this connection.", + DEFAULT_OPTIMIZER_STATISTICS_PACKAGE, + StringValueConverter.INSTANCE, + Context.USER); + static final ConnectionProperty RPC_PRIORITY = + create( + RPC_PRIORITY_NAME, + "Sets the priority for all RPC invocations from this connection (HIGH/MEDIUM/LOW). The default is HIGH.", + DEFAULT_RPC_PRIORITY, + RpcPriorityConverter.INSTANCE, + Context.USER); + static final ConnectionProperty SAVEPOINT_SUPPORT = + create( + "savepoint_support", + "Determines the behavior of the connection when savepoints are used.", + SavepointSupport.FAIL_AFTER_ROLLBACK, + SavepointSupportConverter.INSTANCE, + Context.USER); + static final ConnectionProperty DDL_IN_TRANSACTION_MODE = + create( + DDL_IN_TRANSACTION_MODE_PROPERTY_NAME, + "Determines how the connection should handle DDL statements in a read/write transaction.", + DEFAULT_DDL_IN_TRANSACTION_MODE, + DdlInTransactionModeConverter.INSTANCE, + Context.USER); + static final ConnectionProperty MAX_COMMIT_DELAY = + create( + "maxCommitDelay", + "The max delay that Spanner may apply to commit requests to improve throughput.", + null, + DurationConverter.INSTANCE, + Context.USER); + + static final Map> CONNECTION_PROPERTIES = + CONNECTION_PROPERTIES_BUILDER.build(); + + /** Utility method for creating a new core {@link ConnectionProperty}. */ + private static ConnectionProperty create( + String name, + String description, + T defaultValue, + ClientSideStatementValueConverter converter, + Context context) { + ConnectionProperty property = + ConnectionProperty.create(name, description, defaultValue, converter, context); + CONNECTION_PROPERTIES_BUILDER.put(property.getKey(), property); + return property; + } + + /** Parse the connection properties that can be found in the given connection URL. */ + static ImmutableMap> parseValues(String url) { + ImmutableMap.Builder> builder = ImmutableMap.builder(); + for (ConnectionProperty property : CONNECTION_PROPERTIES.values()) { + ConnectionPropertyValue value = parseValue(castProperty(property), url); + if (value != null) { + builder.put(property.getKey(), value); + } + } + return builder.build(); + } + + /** + * Parse and convert the value of the specific connection property from a connection URL (e.g. + * readonly=true). + */ + private static ConnectionPropertyValue parseValue( + ConnectionProperty property, String url) { + String stringValue = ConnectionOptions.parseUriProperty(url, property.getKey()); + return property.convert(stringValue); + } + + /** This class should not be instantiated. */ + private ConnectionProperties() {} +} diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionProperty.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionProperty.java new file mode 100644 index 00000000000..c203d44203b --- /dev/null +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionProperty.java @@ -0,0 +1,197 @@ +/* + * Copyright 2024 Google LLC + * + * 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.google.cloud.spanner.connection; + +import com.google.cloud.spanner.ErrorCode; +import com.google.cloud.spanner.SpannerExceptionFactory; +import com.google.common.base.Strings; +import java.util.Locale; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * {@link ConnectionProperty} is a variable for a connection. The total set of connection properties + * is the state of a connection, and determine the behavior of that connection. For example, a + * connection with a {@link ConnectionProperty} READONLY=true and AUTOCOMMIT=false will use + * read-only transactions by default, while a connection with READONLY=false and AUTOCOMMIT=false + * will use read/write transactions. + * + *

Connection properties are stored in a {@link ConnectionState} instance. {@link + * ConnectionState} can be transactional. That is; changes to a connection property during a + * transaction will be undone if the transaction is rolled back. Transactional connection state is + * the default for PostgreSQL-dialect databases. For GoogleSQL-dialect databases, transactional + * connection state is an opt-in. + */ +public class ConnectionProperty { + /** + * Context indicates when a {@link ConnectionProperty} may be set. Each higher-ordinal value + * includes the preceding values, meaning that a {@link ConnectionProperty} with {@link + * Context#USER} can be set both at connection startup and during the connection's lifetime. + */ + enum Context { + /** The property can only be set at startup of the connection. */ + STARTUP, + /** + * The property can be set at startup or by a user during the lifetime of a connection. The + * value is persisted until it is changed again by the user. + */ + USER, + } + + /** Utility method for doing an unchecked cast to a typed {@link ConnectionProperty}. */ + static ConnectionProperty castProperty(ConnectionProperty property) { + //noinspection unchecked + return (ConnectionProperty) property; + } + + /** + * Utility method for creating a key for a {@link ConnectionProperty}. The key of a property is + * always lower-case and consists of '[extension.]name'. + */ + @Nonnull + static String createKey(String extension, @Nonnull String name) { + ConnectionPreconditions.checkArgument( + !Strings.isNullOrEmpty(name), "property name must be a non-empty string"); + return extension == null + ? name.toLowerCase(Locale.ENGLISH) + : extension.toLowerCase(Locale.ENGLISH) + "." + name.toLowerCase(Locale.ENGLISH); + } + + /** Utility method for creating a typed {@link ConnectionProperty}. */ + @Nonnull + static ConnectionProperty create( + @Nonnull String name, + String description, + T defaultValue, + ClientSideStatementValueConverter converter, + Context context) { + return new ConnectionProperty<>( + null, name, description, defaultValue, null, converter, context); + } + + /** + * The 'extension' of this property. This is (currently) only used for PostgreSQL-dialect + * databases. + */ + private final String extension; + + @Nonnull private final String name; + + @Nonnull private final String key; + + @Nonnull private final String description; + + private final T defaultValue; + + private final T[] validValues; + + private final ClientSideStatementValueConverter converter; + + private final Context context; + + ConnectionProperty( + String extension, + @Nonnull String name, + @Nonnull String description, + T defaultValue, + T[] validValues, + ClientSideStatementValueConverter converter, + Context context) { + ConnectionPreconditions.checkArgument( + !Strings.isNullOrEmpty(name), "property name must be a non-empty string"); + ConnectionPreconditions.checkArgument( + !Strings.isNullOrEmpty(description), "property description must be a non-empty string"); + this.extension = extension == null ? null : extension.toLowerCase(Locale.ENGLISH); + this.name = name.toLowerCase(Locale.ENGLISH); + this.description = description; + this.defaultValue = defaultValue; + this.validValues = validValues; + this.converter = converter; + this.context = context; + this.key = createKey(this.extension, this.name); + } + + @Override + public String toString() { + return this.key; + } + + @Override + public int hashCode() { + return this.key.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof ConnectionProperty)) { + return false; + } + ConnectionProperty other = (ConnectionProperty) o; + return this.key.equals(other.key); + } + + ConnectionPropertyValue createInitialValue(@Nullable ConnectionPropertyValue initialValue) { + return initialValue == null + ? new ConnectionPropertyValue<>(this, this.defaultValue, this.defaultValue) + : initialValue.copy(); + } + + @Nullable + ConnectionPropertyValue convert(@Nullable String stringValue) { + if (stringValue == null) { + return null; + } + T convertedValue = this.converter.convert(stringValue); + if (convertedValue == null) { + throw SpannerExceptionFactory.newSpannerException( + ErrorCode.INVALID_ARGUMENT, "Invalid value for property " + this + ": " + stringValue); + } + return new ConnectionPropertyValue<>(this, convertedValue, convertedValue); + } + + String getKey() { + return this.key; + } + + boolean hasExtension() { + return this.extension != null; + } + + String getExtension() { + return this.extension; + } + + String getName() { + return this.name; + } + + String getDescription() { + return this.description; + } + + T getDefaultValue() { + return this.defaultValue; + } + + T[] getValidValues() { + return this.validValues; + } + + Context getContext() { + return this.context; + } +} diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionPropertyValue.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionPropertyValue.java new file mode 100644 index 00000000000..088a28d9d8a --- /dev/null +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionPropertyValue.java @@ -0,0 +1,80 @@ +/* + * Copyright 2024 Google LLC + * + * 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.google.cloud.spanner.connection; + +import com.google.cloud.spanner.connection.ConnectionProperty.Context; +import java.util.Objects; + +class ConnectionPropertyValue { + static ConnectionPropertyValue cast(ConnectionPropertyValue value) { + //noinspection unchecked + return (ConnectionPropertyValue) value; + } + + private final ConnectionProperty property; + private final T resetValue; + + private T value; + + ConnectionPropertyValue(ConnectionProperty property, T resetValue, T value) { + this.property = property; + this.resetValue = resetValue; + this.value = value; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof ConnectionPropertyValue)) { + return false; + } + ConnectionPropertyValue other = cast((ConnectionPropertyValue) o); + return Objects.equals(this.property, other.property) + && Objects.equals(this.resetValue, other.resetValue) + && Objects.equals(this.value, other.value); + } + + @Override + public int hashCode() { + return Objects.hash(this.property, this.resetValue, this.value); + } + + ConnectionProperty getProperty() { + return property; + } + + T getResetValue() { + return resetValue; + } + + T getValue() { + return value; + } + + void setValue(T value, Context context) { + ConnectionPreconditions.checkState( + property.getContext().ordinal() >= context.ordinal(), + "Property has context " + + property.getContext() + + " and cannot be set in context " + + context); + this.value = value; + } + + ConnectionPropertyValue copy() { + return new ConnectionPropertyValue<>(this.property, this.resetValue, this.value); + } +} diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionState.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionState.java new file mode 100644 index 00000000000..b732d617c22 --- /dev/null +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionState.java @@ -0,0 +1,282 @@ +/* + * Copyright 2024 Google LLC + * + * 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.google.cloud.spanner.connection; + +import static com.google.cloud.spanner.connection.ConnectionProperties.CONNECTION_PROPERTIES; +import static com.google.cloud.spanner.connection.ConnectionProperties.CONNECTION_STATE_TYPE; +import static com.google.cloud.spanner.connection.ConnectionProperty.castProperty; +import static com.google.cloud.spanner.connection.ConnectionPropertyValue.cast; + +import com.google.cloud.spanner.ErrorCode; +import com.google.cloud.spanner.SpannerException; +import com.google.cloud.spanner.SpannerExceptionFactory; +import com.google.cloud.spanner.connection.ConnectionProperty.Context; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Suppliers; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.function.Supplier; +import javax.annotation.Nullable; + +class ConnectionState { + /** The type of connection state that is used. */ + enum Type { + /** + * Transactional connection state will roll back changes to connection properties that have been + * done during a transaction if the transaction is rolled back. + */ + TRANSACTIONAL, + /** + * Non-transactional connection state directly applies connection property changes during + * transactions to the main set of properties. Note that non-transactional connection state does + * support local properties. These are property changes that are only visible during the current + * transaction, and that are lost after committing or rolling back the current transaction. + */ + NON_TRANSACTIONAL, + } + + private final Object lock = new Object(); + + private final Supplier type; + + /** properties contain the current connection properties of a connection. */ + private final Map> properties; + + /** + * transactionProperties are the modified connection properties during a transaction. This is only + * used for {@link ConnectionState} that is marked as {@link Type#TRANSACTIONAL}. + */ + private Map> transactionProperties; + /** localProperties are the modified local properties during a transaction. */ + private Map> localProperties; + + /** Constructs a non-transactional {@link ConnectionState} with the given initial values. */ + ConnectionState(Map> initialValues) { + this(initialValues, Suppliers.ofInstance(Type.NON_TRANSACTIONAL)); + } + + /** + * Constructs a {@link ConnectionState} with the given initial values. The type will be + * transactional or non-transactional based on the value that is returned by the given supplier. + * The type is determined lazily to allow connections to determine the default based on the + * dialect, and the dialect is not known directly when a connection is created. + */ + ConnectionState( + Map> initialValues, + Supplier defaultConnectionStateTypeSupplier) { + this.properties = new HashMap<>(CONNECTION_PROPERTIES.size()); + for (Entry> entry : CONNECTION_PROPERTIES.entrySet()) { + this.properties.put( + entry.getKey(), + entry.getValue().createInitialValue(cast(initialValues.get(entry.getKey())))); + } + // Add any additional non-core values from the options. + for (Entry> entry : initialValues.entrySet()) { + if (!this.properties.containsKey(entry.getKey())) { + setValue( + castProperty(entry.getValue().getProperty()), + cast(entry.getValue()).getValue(), + Context.STARTUP, + /* inTransaction = */ false); + } + } + Type configuredType = getValue(CONNECTION_STATE_TYPE).getValue(); + if (configuredType == null) { + this.type = defaultConnectionStateTypeSupplier; + } else { + this.type = Suppliers.ofInstance(configuredType); + } + } + + @VisibleForTesting + Type getType() { + return this.type.get(); + } + + boolean hasTransactionalChanges() { + synchronized (lock) { + return this.transactionProperties != null || this.localProperties != null; + } + } + + /** + * Returns an unmodifiable map with all the property values of this {@link ConnectionState}. The + * map cannot be modified, but any changes to the current (committed) state will be reflected in + * the map that is returned by this method. + */ + Map> getAllValues() { + synchronized (lock) { + return Collections.unmodifiableMap(this.properties); + } + } + + /** Returns the current value of the specified setting. */ + ConnectionPropertyValue getValue(ConnectionProperty property) { + synchronized (lock) { + return internalGetValue(property, true); + } + } + + /** Returns the current value of the specified setting or null if undefined. */ + @Nullable + ConnectionPropertyValue tryGetValue(ConnectionProperty property) { + synchronized (lock) { + return internalGetValue(property, false); + } + } + + private ConnectionPropertyValue internalGetValue( + ConnectionProperty property, boolean throwForUnknownParam) { + if (localProperties != null && localProperties.containsKey(property.getKey())) { + return cast(localProperties.get(property.getKey())); + } + if (transactionProperties != null && transactionProperties.containsKey(property.getKey())) { + return cast(transactionProperties.get(property.getKey())); + } + if (properties.containsKey(property.getKey())) { + return cast(properties.get(property.getKey())); + } + if (throwForUnknownParam) { + throw unknownParamError(property); + } + return null; + } + + /** + * Sets the value of the specified property. The new value will be persisted if the current + * transaction is committed or directly if the connection state is non-transactional. The value + * will be lost if the transaction is rolled back and the connection state is transactional. + */ + void setValue( + ConnectionProperty property, T value, Context context, boolean inTransaction) { + ConnectionPreconditions.checkState( + property.getContext().ordinal() >= context.ordinal(), + "Property has context " + + property.getContext() + + " and cannot be set in context " + + context); + synchronized (lock) { + if (!inTransaction + || getType() == Type.NON_TRANSACTIONAL + || context.ordinal() < Context.USER.ordinal()) { + internalSetValue(property, value, properties, context); + return; + } + + if (transactionProperties == null) { + transactionProperties = new HashMap<>(); + } + internalSetValue(property, value, transactionProperties, context); + // Remove the setting from the local settings if it's there, as the new transaction setting is + // the one that should be used. + if (localProperties != null) { + localProperties.remove(property.getKey()); + } + } + } + + /** + * Sets the value of the specified setting for the current transaction. This value is lost when + * the transaction is committed or rolled back. This can be used to temporarily set a value only + * during a transaction, for example if a user wants to disable internal transaction retries only + * for a single transaction. + */ + void setLocalValue(ConnectionProperty property, T value) { + ConnectionPreconditions.checkState( + property.getContext().ordinal() >= Context.USER.ordinal(), + "setLocalValue is only supported for properties with context USER or higher."); + synchronized (lock) { + if (localProperties == null) { + localProperties = new HashMap<>(); + } + // Note that setting a local setting does not remove it from the transaction settings. This + // means that a commit will persist the setting in transactionSettings. + internalSetValue(property, value, localProperties, Context.USER); + } + } + + /** + * Resets the value of the specified property. The new value will be persisted if the current + * transaction is committed or directly if the connection state is non-transactional. The value + * will be lost if the transaction is rolled back and the connection state is transactional. + */ + void resetValue(ConnectionProperty property, Context context, boolean inTransaction) { + synchronized (lock) { + ConnectionPropertyValue currentValue = getValue(property); + if (currentValue == null) { + setValue(property, null, context, inTransaction); + } else { + setValue(property, currentValue.getResetValue(), context, inTransaction); + } + } + } + + /** Persists the new value for a property to the given map of properties. */ + private void internalSetValue( + ConnectionProperty property, + T value, + Map> currentProperties, + Context context) { + ConnectionPropertyValue newValue = cast(currentProperties.get(property.getKey())); + if (newValue == null) { + ConnectionPropertyValue existingValue = cast(properties.get(property.getKey())); + if (existingValue == null) { + if (!property.hasExtension()) { + throw unknownParamError(property); + } + newValue = new ConnectionPropertyValue(property, null, null); + } else { + newValue = existingValue.copy(); + } + } + newValue.setValue(value, context); + currentProperties.put(property.getKey(), newValue); + } + + /** Creates an exception for an unknown connection property. */ + static SpannerException unknownParamError(ConnectionProperty property) { + return SpannerExceptionFactory.newSpannerException( + ErrorCode.INVALID_ARGUMENT, + String.format("unrecognized configuration property \"%s\"", property)); + } + + /** + * Commits the current transaction and persists any changes to the settings (except local + * changes). + */ + void commit() { + synchronized (lock) { + if (transactionProperties != null) { + for (ConnectionPropertyValue value : transactionProperties.values()) { + properties.put(value.getProperty().getKey(), value); + } + } + this.localProperties = null; + this.transactionProperties = null; + } + } + + /** Rolls back the current transaction and abandons any pending changes to the settings. */ + void rollback() { + synchronized (lock) { + this.localProperties = null; + this.transactionProperties = null; + } + } +} diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DdlBatch.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DdlBatch.java index 4c8a0542696..6ae28822473 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DdlBatch.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DdlBatch.java @@ -45,6 +45,7 @@ import java.util.Arrays; import java.util.List; import java.util.concurrent.Callable; +import javax.annotation.Nonnull; /** * {@link UnitOfWork} that is used when a DDL batch is started. These batches only accept DDL @@ -298,13 +299,15 @@ public void abortBatch() { } @Override - public ApiFuture commitAsync(CallType callType) { + public ApiFuture commitAsync( + @Nonnull CallType callType, @Nonnull EndTransactionCallback callback) { throw SpannerExceptionFactory.newSpannerException( ErrorCode.FAILED_PRECONDITION, "Commit is not allowed for DDL batches."); } @Override - public ApiFuture rollbackAsync(CallType callType) { + public ApiFuture rollbackAsync( + @Nonnull CallType callType, @Nonnull EndTransactionCallback callback) { throw SpannerExceptionFactory.newSpannerException( ErrorCode.FAILED_PRECONDITION, "Rollback is not allowed for DDL batches."); } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DirectedReadOptionsUtil.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DirectedReadOptionsUtil.java index 8b1f8a90199..8b346a08f3d 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DirectedReadOptionsUtil.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DirectedReadOptionsUtil.java @@ -23,6 +23,25 @@ import com.google.spanner.v1.DirectedReadOptions; public class DirectedReadOptionsUtil { + static class DirectedReadOptionsConverter + implements ClientSideStatementValueConverter { + static DirectedReadOptionsConverter INSTANCE = new DirectedReadOptionsConverter(); + + @Override + public Class getParameterClass() { + return DirectedReadOptions.class; + } + + @Override + public DirectedReadOptions convert(String value) { + try { + return parse(value); + } catch (Throwable ignore) { + // ClientSideStatementValueConverters should return null if the value cannot be converted. + return null; + } + } + } /** * Generates a valid JSON string for the given {@link DirectedReadOptions} that can be used with diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DmlBatch.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DmlBatch.java index fde3c609cb3..ea7732a7c0a 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DmlBatch.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DmlBatch.java @@ -36,6 +36,7 @@ import io.opentelemetry.context.Scope; import java.util.ArrayList; import java.util.List; +import javax.annotation.Nonnull; /** * {@link UnitOfWork} that is used when a DML batch is started. These batches only accept DML @@ -257,13 +258,15 @@ public void abortBatch() { } @Override - public ApiFuture commitAsync(CallType callType) { + public ApiFuture commitAsync( + @Nonnull CallType callType, @Nonnull EndTransactionCallback callback) { throw SpannerExceptionFactory.newSpannerException( ErrorCode.FAILED_PRECONDITION, "Commit is not allowed for DML batches."); } @Override - public ApiFuture rollbackAsync(CallType callType) { + public ApiFuture rollbackAsync( + @Nonnull CallType callType, @Nonnull EndTransactionCallback callback) { throw SpannerExceptionFactory.newSpannerException( ErrorCode.FAILED_PRECONDITION, "Rollback is not allowed for DML batches."); } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReadOnlyTransaction.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReadOnlyTransaction.java index ea4a11a4cd1..357503cb17f 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReadOnlyTransaction.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReadOnlyTransaction.java @@ -39,6 +39,7 @@ import com.google.spanner.v1.SpannerGrpc; import io.opentelemetry.context.Scope; import java.util.concurrent.Callable; +import javax.annotation.Nonnull; /** * Transaction that is used when a {@link Connection} is in read-only mode or when the transaction @@ -241,20 +242,30 @@ public ApiFuture writeAsync(CallType callType, Iterable mutation } @Override - public ApiFuture commitAsync(CallType callType) { + public ApiFuture commitAsync( + @Nonnull CallType callType, @Nonnull EndTransactionCallback callback) { try (Scope ignore = span.makeCurrent()) { ApiFuture result = closeTransactions(); + callback.onSuccess(); this.state = UnitOfWorkState.COMMITTED; return result; + } catch (Throwable throwable) { + callback.onFailure(); + throw throwable; } } @Override - public ApiFuture rollbackAsync(CallType callType) { + public ApiFuture rollbackAsync( + @Nonnull CallType callType, @Nonnull EndTransactionCallback callback) { try (Scope ignore = span.makeCurrent()) { ApiFuture result = closeTransactions(); + callback.onSuccess(); this.state = UnitOfWorkState.ROLLED_BACK; return result; + } catch (Throwable throwable) { + callback.onFailure(); + throw throwable; } } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReadWriteTransaction.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReadWriteTransaction.java index d047857d6ad..6a83142b175 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReadWriteTransaction.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReadWriteTransaction.java @@ -78,6 +78,7 @@ import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; +import javax.annotation.Nonnull; /** * Transaction that is used when a {@link Connection} is normal read/write mode (i.e. not autocommit @@ -861,7 +862,8 @@ public Void call() { }; @Override - public ApiFuture commitAsync(CallType callType) { + public ApiFuture commitAsync( + @Nonnull CallType callType, @Nonnull EndTransactionCallback callback) { try (Scope ignore = span.makeCurrent()) { checkOrCreateValidTransaction(COMMIT_STATEMENT, callType); cancelScheduledKeepAlivePing(); @@ -871,11 +873,11 @@ public ApiFuture commitAsync(CallType callType) { // Check if this transaction actually needs to commit anything. if (txContextFuture == null) { // No actual transaction was started by this read/write transaction, which also means that - // we - // don't have to commit anything. + // we don't have to commit anything. commitResponseFuture.set( new CommitResponse( Timestamp.fromProto(com.google.protobuf.Timestamp.getDefaultInstance()))); + callback.onSuccess(); state = UnitOfWorkState.COMMITTED; res = SettableApiFuture.create(); ((SettableApiFuture) res).set(null); @@ -887,17 +889,21 @@ public ApiFuture commitAsync(CallType callType) { () -> { checkTimedOut(); try { - return runWithRetry( - () -> { - getStatementExecutor() - .invokeInterceptors( - COMMIT_STATEMENT, - StatementExecutionStep.EXECUTE_STATEMENT, - ReadWriteTransaction.this); - return commitCallable.call(); - }); + Void result = + runWithRetry( + () -> { + getStatementExecutor() + .invokeInterceptors( + COMMIT_STATEMENT, + StatementExecutionStep.EXECUTE_STATEMENT, + ReadWriteTransaction.this); + return commitCallable.call(); + }); + callback.onSuccess(); + return result; } catch (Throwable t) { commitResponseFuture.setException(t); + callback.onFailure(); state = UnitOfWorkState.COMMIT_FAILED; try { txManager.close(); @@ -917,9 +923,12 @@ public ApiFuture commitAsync(CallType callType) { () -> { checkTimedOut(); try { - return commitCallable.call(); + Void result = commitCallable.call(); + callback.onSuccess(); + return result; } catch (Throwable t) { commitResponseFuture.setException(t); + callback.onFailure(); state = UnitOfWorkState.COMMIT_FAILED; try { txManager.close(); @@ -1220,9 +1229,14 @@ public Void call() { }; @Override - public ApiFuture rollbackAsync(CallType callType) { + public ApiFuture rollbackAsync( + @Nonnull CallType callType, @Nonnull EndTransactionCallback callback) { try (Scope ignore = span.makeCurrent()) { + callback.onSuccess(); return rollbackAsync(callType, true); + } catch (Throwable throwable) { + callback.onFailure(); + throw throwable; } } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SingleUseTransaction.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SingleUseTransaction.java index 44f20ccdbd8..53a1bb03b10 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SingleUseTransaction.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SingleUseTransaction.java @@ -57,6 +57,7 @@ import java.time.Duration; import java.util.Arrays; import java.util.concurrent.Callable; +import javax.annotation.Nonnull; /** * Transaction that is used when a {@link Connection} is in autocommit mode. Each method on this @@ -682,13 +683,15 @@ public ApiFuture writeAsync(CallType callType, final Iterable mu } @Override - public ApiFuture commitAsync(CallType callType) { + public ApiFuture commitAsync( + @Nonnull CallType callType, @Nonnull EndTransactionCallback callback) { throw SpannerExceptionFactory.newSpannerException( ErrorCode.FAILED_PRECONDITION, "Commit is not supported for single-use transactions"); } @Override - public ApiFuture rollbackAsync(CallType callType) { + public ApiFuture rollbackAsync( + @Nonnull CallType callType, @Nonnull EndTransactionCallback callback) { throw SpannerExceptionFactory.newSpannerException( ErrorCode.FAILED_PRECONDITION, "Rollback is not supported for single-use transactions"); } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/UnitOfWork.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/UnitOfWork.java index 9a5e9197245..ffa93d486e1 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/UnitOfWork.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/UnitOfWork.java @@ -66,6 +66,24 @@ public boolean isActive() { } } + /** + * Callback for end-of-transaction methods. This is used to commit or rollback connection state + * after an async commit/rollback of a database transaction. + */ + interface EndTransactionCallback { + /** + * This method will be called if the end-of-transaction method (commit or rollback) finished + * successfully, but before the {@link ApiFuture} that is returned by the method is done. + */ + void onSuccess(); + + /** + * This method will be called if the end-of-transaction method (commit or rollback) failed, but + * before the {@link ApiFuture} that is returned by the method is done. + */ + void onFailure(); + } + /** Cancel the currently running statement (if any and the statement may be cancelled). */ void cancel(); @@ -90,9 +108,10 @@ public boolean isActive() { * a {@link Type#BATCH}. * * @param callType Indicates whether the top-level call is a sync or async call. + * @param callback Callback that should be called when the commit succeeded or failed. * @return An {@link ApiFuture} that is done when the commit has finished. */ - ApiFuture commitAsync(CallType callType); + ApiFuture commitAsync(@Nonnull CallType callType, @Nonnull EndTransactionCallback callback); /** * Rollbacks any changes in this unit of work. For read-only transactions, this only closes the @@ -100,9 +119,11 @@ public boolean isActive() { * Type#BATCH}. * * @param callType Indicates whether the top-level call is a sync or async call. + * @param callback Callback that should be called when the rollback succeeded or failed. * @return An {@link ApiFuture} that is done when the rollback has finished. */ - ApiFuture rollbackAsync(CallType callType); + ApiFuture rollbackAsync( + @Nonnull CallType callType, @Nonnull EndTransactionCallback callback); /** @see Connection#savepoint(String) */ void savepoint(@Nonnull String name, @Nonnull Dialect dialect); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionImplTest.java index 299205bafc6..f118a77edbc 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionImplTest.java @@ -78,6 +78,7 @@ import com.google.cloud.spanner.connection.StatementResult.ResultType; import com.google.cloud.spanner.connection.UnitOfWork.CallType; import com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.io.ByteStreams; import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata; @@ -209,6 +210,7 @@ private static ResultSet createSelect1MockResultSet() { when(mockResultSet.next()).thenReturn(true, false); when(mockResultSet.getLong(0)).thenReturn(1L); when(mockResultSet.getLong("TEST")).thenReturn(1L); + when(mockResultSet.getType()).thenReturn(Type.struct()); when(mockResultSet.getColumnType(0)).thenReturn(Type.int64()); when(mockResultSet.getColumnType("TEST")).thenReturn(Type.int64()); return mockResultSet; @@ -1524,6 +1526,7 @@ public void testAddRemoveTransactionRetryListener() { @Test public void testMergeQueryOptions() { ConnectionOptions connectionOptions = mock(ConnectionOptions.class); + when(connectionOptions.getInitialConnectionPropertyValues()).thenReturn(ImmutableMap.of()); SpannerPool spannerPool = mock(SpannerPool.class); DdlClient ddlClient = mock(DdlClient.class); DatabaseClient dbClient = mock(DatabaseClient.class); @@ -1633,6 +1636,7 @@ UnitOfWork getCurrentUnitOfWorkOrStartNewUnitOfWork(boolean isInternalMetadataQu public void testStatementTagAlwaysAllowed() { ConnectionOptions connectionOptions = mock(ConnectionOptions.class); when(connectionOptions.isAutocommit()).thenReturn(true); + when(connectionOptions.getInitialConnectionPropertyValues()).thenReturn(ImmutableMap.of()); SpannerPool spannerPool = mock(SpannerPool.class); DdlClient ddlClient = mock(DdlClient.class); DatabaseClient dbClient = mock(DatabaseClient.class); @@ -1677,6 +1681,7 @@ UnitOfWork getCurrentUnitOfWorkOrStartNewUnitOfWork(boolean isInternalMetadataQu public void testTransactionTagAllowedInTransaction() { ConnectionOptions connectionOptions = mock(ConnectionOptions.class); when(connectionOptions.isAutocommit()).thenReturn(false); + when(connectionOptions.getInitialConnectionPropertyValues()).thenReturn(ImmutableMap.of()); SpannerPool spannerPool = mock(SpannerPool.class); DdlClient ddlClient = mock(DdlClient.class); DatabaseClient dbClient = mock(DatabaseClient.class); @@ -1719,6 +1724,7 @@ connectionOptions, spannerPool, ddlClient, dbClient, mock(BatchClient.class))) { public void testTransactionTagNotAllowedWithoutTransaction() { ConnectionOptions connectionOptions = mock(ConnectionOptions.class); when(connectionOptions.isAutocommit()).thenReturn(true); + when(connectionOptions.getInitialConnectionPropertyValues()).thenReturn(ImmutableMap.of()); SpannerPool spannerPool = mock(SpannerPool.class); DdlClient ddlClient = mock(DdlClient.class); DatabaseClient dbClient = mock(DatabaseClient.class); @@ -1741,6 +1747,7 @@ connectionOptions, spannerPool, ddlClient, dbClient, mock(BatchClient.class))) { public void testTransactionTagNotAllowedAfterTransactionStarted() { ConnectionOptions connectionOptions = mock(ConnectionOptions.class); when(connectionOptions.isAutocommit()).thenReturn(false); + when(connectionOptions.getInitialConnectionPropertyValues()).thenReturn(ImmutableMap.of()); SpannerPool spannerPool = mock(SpannerPool.class); DdlClient ddlClient = mock(DdlClient.class); DatabaseClient dbClient = mock(DatabaseClient.class); @@ -1751,7 +1758,7 @@ public void testTransactionTagNotAllowedAfterTransactionStarted() { when(unitOfWork.executeQueryAsync( any(), any(ParsedStatement.class), any(AnalyzeMode.class), Mockito.any())) .thenReturn(ApiFutures.immediateFuture(mock(ResultSet.class))); - when(unitOfWork.rollbackAsync(any())).thenReturn(ApiFutures.immediateFuture(null)); + when(unitOfWork.rollbackAsync(any(), any())).thenReturn(ApiFutures.immediateFuture(null)); try (ConnectionImpl connection = new ConnectionImpl( connectionOptions, spannerPool, ddlClient, dbClient, mock(BatchClient.class)) { @@ -1881,37 +1888,37 @@ public void testSetRetryAbortsInternally() { .build())) { assertFalse("Read-only should be disabled by default", connection.isReadOnly()); assertTrue("Autocommit should be enabled by default", connection.isAutocommit()); - assertFalse( - "Retry aborts internally should be disabled by default on test connections", + assertTrue( + "Retry aborts internally should be enabled by default on test connections", connection.isRetryAbortsInternally()); // It should be possible to change this value also when in auto-commit mode. - connection.setRetryAbortsInternally(true); - assertTrue(connection.isRetryAbortsInternally()); + connection.setRetryAbortsInternally(false); + assertFalse(connection.isRetryAbortsInternally()); // It should be possible to change this value also when in transactional mode, as long as // there is no active transaction. connection.setAutocommit(false); - connection.setRetryAbortsInternally(false); - assertFalse(connection.isRetryAbortsInternally()); + connection.setRetryAbortsInternally(true); + assertTrue(connection.isRetryAbortsInternally()); // It should be possible to change the value when in read-only mode. connection.setReadOnly(true); - connection.setRetryAbortsInternally(true); - assertTrue(connection.isRetryAbortsInternally()); + connection.setRetryAbortsInternally(false); + assertFalse(connection.isRetryAbortsInternally()); // It should not be possible to change the value when there is an active transaction. connection.setReadOnly(false); connection.setAutocommit(false); connection.execute(Statement.of(SELECT)); - assertThrows(SpannerException.class, () -> connection.setRetryAbortsInternally(false)); + assertThrows(SpannerException.class, () -> connection.setRetryAbortsInternally(true)); // Verify that the value did not change. - assertTrue(connection.isRetryAbortsInternally()); + assertFalse(connection.isRetryAbortsInternally()); // Rolling back the connection should allow us to set the property again. connection.rollback(); - connection.setRetryAbortsInternally(false); - assertFalse(connection.isRetryAbortsInternally()); + connection.setRetryAbortsInternally(true); + assertTrue(connection.isRetryAbortsInternally()); } } @@ -1934,6 +1941,7 @@ private void assertThrowResultNotAllowed( public void testProtoDescriptorsAlwaysAllowed() { ConnectionOptions connectionOptions = mock(ConnectionOptions.class); when(connectionOptions.isAutocommit()).thenReturn(true); + when(connectionOptions.getInitialConnectionPropertyValues()).thenReturn(ImmutableMap.of()); SpannerPool spannerPool = mock(SpannerPool.class); DdlClient ddlClient = mock(DdlClient.class); DatabaseClient dbClient = mock(DatabaseClient.class); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionPropertyTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionPropertyTest.java new file mode 100644 index 00000000000..0888f61cf90 --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionPropertyTest.java @@ -0,0 +1,301 @@ +/* + * Copyright 2024 Google LLC + * + * 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.google.cloud.spanner.connection; + +import static com.google.cloud.spanner.connection.ConnectionProperty.create; +import static com.google.cloud.spanner.connection.ConnectionProperty.createKey; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; + +import com.google.cloud.spanner.SpannerException; +import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.NonNegativeIntegerConverter; +import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.StringValueConverter; +import com.google.cloud.spanner.connection.ConnectionProperty.Context; +import java.util.Objects; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class ConnectionPropertyTest { + + @Test + public void testCreateKey() { + assertEquals("my_property", createKey(/* extension = */ null, "my_property")); + assertEquals("my_property", createKey(/* extension = */ null, "My_Property")); + assertEquals("my_property", createKey(/* extension = */ null, "MY_PROPERTY")); + assertEquals("my_extension.my_property", createKey("my_extension", "my_property")); + assertEquals("my_extension.my_property", createKey("My_Extension", "My_Property")); + assertEquals("my_extension.my_property", createKey("MY_EXTENSION", "MY_PROPERTY")); + + //noinspection DataFlowIssue + assertThrows(SpannerException.class, () -> createKey("my_extension", /* name = */ null)); + assertThrows(SpannerException.class, () -> createKey("my_extension", "")); + } + + @Test + public void testCreate() { + ConnectionProperty property = + create( + "my_property", + "Description of my_property", + "default_value", + StringValueConverter.INSTANCE, + Context.USER); + assertEquals("my_property", property.getName()); + assertEquals("Description of my_property", property.getDescription()); + assertEquals("default_value", property.getDefaultValue()); + assertEquals("my_value", Objects.requireNonNull(property.convert("my_value")).getValue()); + assertEquals(property.getContext(), Context.USER); + assertEquals("my_property", property.getKey()); + + ConnectionProperty startupProperty = + create( + "STARTUP_PROPERTY", + "Description of STARTUP_PROPERTY", + 1, + NonNegativeIntegerConverter.INSTANCE, + Context.STARTUP); + // The name is folded to lower-case. + assertEquals("startup_property", startupProperty.getName()); + assertEquals("Description of STARTUP_PROPERTY", startupProperty.getDescription()); + assertEquals(Integer.valueOf(1), startupProperty.getDefaultValue()); + assertEquals( + Integer.valueOf(2), Objects.requireNonNull(startupProperty.convert("2")).getValue()); + assertEquals(startupProperty.getContext(), Context.STARTUP); + assertEquals("startup_property", startupProperty.getKey()); + } + + @Test + public void testEquals() { + ConnectionProperty property1 = + new ConnectionProperty<>( + /* extension = */ null, + "my_property", + "Description of property1", + "default_value_1", + null, + StringValueConverter.INSTANCE, + Context.STARTUP); + ConnectionProperty property2 = + new ConnectionProperty<>( + /* extension = */ null, + "my_property", + "Description of property2", + "default_value_2", + null, + StringValueConverter.INSTANCE, + Context.USER); + ConnectionProperty property3 = + new ConnectionProperty<>( + "my_extension", + "my_property", + "Description of property3", + "default_value_3", + null, + StringValueConverter.INSTANCE, + Context.STARTUP); + ConnectionProperty property4 = + new ConnectionProperty<>( + "my_extension", + "my_property", + "Description of property4", + "default_value_4", + null, + StringValueConverter.INSTANCE, + Context.USER); + ConnectionProperty property5 = + new ConnectionProperty<>( + /* extension = */ null, + "my_other_property", + "Description of property5", + "default_value_5", + null, + StringValueConverter.INSTANCE, + Context.STARTUP); + ConnectionProperty property6 = + new ConnectionProperty<>( + "my_extension", + "my_other_property", + "Description of property6", + "default_value_6", + null, + StringValueConverter.INSTANCE, + Context.STARTUP); + ConnectionProperty property7 = + new ConnectionProperty<>( + /* extension = */ null, + "MY_PROPERTY", + "Description of property7", + "default_value_7", + null, + StringValueConverter.INSTANCE, + Context.STARTUP); + ConnectionProperty property8 = + new ConnectionProperty<>( + "MY_EXTENSION", + "my_property", + "Description of property8", + "default_value_8", + null, + StringValueConverter.INSTANCE, + Context.STARTUP); + ConnectionProperty property9 = + new ConnectionProperty<>( + "my_extension", + "MY_PROPERTY", + "Description of property9", + "default_value_9", + null, + StringValueConverter.INSTANCE, + Context.STARTUP); + + // Equality is based only on the key. + // The key is the lower case combination of extension and name. + // If extension is null, then only the name is the key. + + // property1 = my_property + assertEquals(property1, property2); + assertNotEquals(property1, property3); + assertNotEquals(property1, property4); + assertNotEquals(property1, property5); + assertNotEquals(property1, property6); + assertEquals(property1, property7); + assertNotEquals(property1, property8); + assertNotEquals(property1, property9); + + // property2 = my_property + assertEquals(property2, property1); + assertNotEquals(property2, property3); + assertNotEquals(property2, property4); + assertNotEquals(property2, property5); + assertNotEquals(property2, property6); + assertEquals(property2, property7); + assertNotEquals(property2, property8); + assertNotEquals(property2, property9); + + // property3 = my_extension.my_property + assertNotEquals(property3, property1); + assertNotEquals(property3, property2); + assertEquals(property3, property4); + assertNotEquals(property3, property5); + assertNotEquals(property3, property6); + assertNotEquals(property3, property7); + assertEquals(property3, property8); + assertEquals(property3, property9); + + // property4 = my_extension.my_property + assertNotEquals(property4, property1); + assertNotEquals(property4, property2); + assertEquals(property4, property3); + assertNotEquals(property4, property5); + assertNotEquals(property4, property6); + assertNotEquals(property4, property7); + assertEquals(property4, property8); + assertEquals(property4, property9); + + // property5 = my_other_property + assertNotEquals(property5, property1); + assertNotEquals(property5, property2); + assertNotEquals(property5, property3); + assertNotEquals(property5, property4); + assertNotEquals(property5, property6); + assertNotEquals(property5, property7); + assertNotEquals(property5, property8); + assertNotEquals(property5, property9); + + // property6 = my_extension.my_other_property + assertNotEquals(property6, property1); + assertNotEquals(property6, property2); + assertNotEquals(property6, property3); + assertNotEquals(property6, property4); + assertNotEquals(property6, property5); + assertNotEquals(property6, property7); + assertNotEquals(property6, property8); + assertNotEquals(property6, property9); + + // property7 = MY_PROPERTY (same as property1 and property2) + assertEquals(property7, property1); + assertEquals(property7, property2); + assertNotEquals(property7, property3); + assertNotEquals(property7, property4); + assertNotEquals(property7, property5); + assertNotEquals(property7, property6); + assertNotEquals(property7, property8); + assertNotEquals(property7, property9); + + // property8 = MY_EXTENSION.my_property (same as property4) + assertNotEquals(property8, property1); + assertNotEquals(property8, property2); + assertEquals(property8, property3); + assertEquals(property8, property4); + assertNotEquals(property8, property5); + assertNotEquals(property8, property6); + assertNotEquals(property8, property7); + assertEquals(property8, property9); + + // property9 = my_extension.MY_PROPERTY (same as property4 and property8) + assertNotEquals(property9, property1); + assertNotEquals(property9, property2); + assertEquals(property9, property3); + assertEquals(property9, property4); + assertNotEquals(property9, property5); + assertNotEquals(property9, property6); + assertNotEquals(property9, property7); + assertEquals(property9, property8); + } + + @Test + public void testConvert() { + ConnectionProperty property = + create( + "my_property", + "Description of my_property", + 1, + NonNegativeIntegerConverter.INSTANCE, + Context.STARTUP); + assertEquals(Integer.valueOf(100), Objects.requireNonNull(property.convert("100")).getValue()); + assertThrows(SpannerException.class, () -> property.convert("foo")); + assertThrows(SpannerException.class, () -> property.convert("-100")); + } + + @Test + public void testCreateInitialValue() { + ConnectionProperty property = + create( + "my_property", + "Description of my_property", + "default_value", + StringValueConverter.INSTANCE, + Context.USER); + + ConnectionPropertyValue initialValue = property.createInitialValue(null); + assertEquals(property.getDefaultValue(), initialValue.getValue()); + assertEquals(property.getDefaultValue(), initialValue.getResetValue()); + assertSame(initialValue.getProperty(), property); + + ConnectionPropertyValue startupValue = + new ConnectionPropertyValue<>(property, "other_value", "other_value"); + ConnectionPropertyValue initialValueWithStartupValue = + property.createInitialValue(startupValue); + assertEquals("other_value", initialValueWithStartupValue.getValue()); + assertEquals("other_value", initialValueWithStartupValue.getResetValue()); + assertSame(initialValueWithStartupValue.getProperty(), property); + } +} diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionPropertyValueTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionPropertyValueTest.java new file mode 100644 index 00000000000..d4f795185e4 --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionPropertyValueTest.java @@ -0,0 +1,100 @@ +/* + * Copyright 2024 Google LLC + * + * 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.google.cloud.spanner.connection; + +import static com.google.cloud.spanner.connection.ConnectionProperties.AUTOCOMMIT_DML_MODE; +import static com.google.cloud.spanner.connection.ConnectionProperties.CONNECTION_STATE_TYPE; +import static com.google.cloud.spanner.connection.ConnectionProperties.READONLY; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertThrows; + +import com.google.cloud.spanner.SpannerException; +import com.google.cloud.spanner.connection.ConnectionProperty.Context; +import com.google.cloud.spanner.connection.ConnectionState.Type; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class ConnectionPropertyValueTest { + + @Test + public void testSetValue() { + // This value can be set at any time. + ConnectionPropertyValue value = READONLY.createInitialValue(null); + assertEquals(READONLY.getDefaultValue(), value.getValue()); + + value.setValue(Boolean.FALSE, Context.STARTUP); + assertEquals(Boolean.FALSE, value.getValue()); + + value.setValue(Boolean.TRUE, Context.USER); + assertEquals(Boolean.TRUE, value.getValue()); + + value.setValue(Boolean.FALSE, Context.USER); + assertEquals(Boolean.FALSE, value.getValue()); + + // This value may only be set outside transactions. + ConnectionPropertyValue outsideTransactionOnlyValue = + AUTOCOMMIT_DML_MODE.createInitialValue(null); + assertEquals(AUTOCOMMIT_DML_MODE.getDefaultValue(), outsideTransactionOnlyValue.getValue()); + + outsideTransactionOnlyValue.setValue(AutocommitDmlMode.PARTITIONED_NON_ATOMIC, Context.STARTUP); + assertEquals(AutocommitDmlMode.PARTITIONED_NON_ATOMIC, outsideTransactionOnlyValue.getValue()); + + outsideTransactionOnlyValue.setValue(AutocommitDmlMode.TRANSACTIONAL, Context.USER); + assertEquals(AutocommitDmlMode.TRANSACTIONAL, outsideTransactionOnlyValue.getValue()); + + // This value may only be set at startup. + ConnectionPropertyValue startupOnlyValue = + CONNECTION_STATE_TYPE.createInitialValue(null); + assertEquals(CONNECTION_STATE_TYPE.getDefaultValue(), startupOnlyValue.getValue()); + + startupOnlyValue.setValue(Type.TRANSACTIONAL, Context.STARTUP); + assertEquals(Type.TRANSACTIONAL, startupOnlyValue.getValue()); + + // This property may not be set after startup.. + assertThrows( + SpannerException.class, + () -> startupOnlyValue.setValue(Type.NON_TRANSACTIONAL, Context.USER)); + // The value should not have changed. + assertEquals(Type.TRANSACTIONAL, startupOnlyValue.getValue()); + + // This property may not be set in a transaction. + assertThrows( + SpannerException.class, + () -> startupOnlyValue.setValue(Type.NON_TRANSACTIONAL, Context.USER)); + // The value should not have changed. + assertEquals(Type.TRANSACTIONAL, startupOnlyValue.getValue()); + } + + @Test + public void testCopy() { + ConnectionPropertyValue value = + new ConnectionPropertyValue<>( + /* property = */ AUTOCOMMIT_DML_MODE, + /* resetValue = */ AutocommitDmlMode.PARTITIONED_NON_ATOMIC, + /* value = */ AutocommitDmlMode.TRANSACTIONAL); + ConnectionPropertyValue copy = value.copy(); + + assertEquals(value, copy); + assertNotSame(value, copy); + assertEquals(value.getProperty(), copy.getProperty()); + assertEquals(value.getValue(), copy.getValue()); + assertEquals(value.getResetValue(), copy.getResetValue()); + } +} diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionStateMockServerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionStateMockServerTest.java new file mode 100644 index 00000000000..ea79a7132bf --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionStateMockServerTest.java @@ -0,0 +1,231 @@ +/* + * Copyright 2023 Google LLC + * + * 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.google.cloud.spanner.connection; + +import static com.google.cloud.spanner.connection.ConnectionProperties.CONNECTION_STATE_TYPE; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.google.cloud.spanner.Dialect; +import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult; +import com.google.cloud.spanner.connection.ConnectionState.Type; +import com.google.cloud.spanner.connection.ITAbstractSpannerTest.ITConnection; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class ConnectionStateMockServerTest extends AbstractMockServerTest { + + @Parameters(name = "dialect = {0})") + public static Object[] data() { + return Dialect.values(); + } + + @Parameter public Dialect dialect; + + private Dialect currentDialect; + + @BeforeClass + public static void enableTransactionalConnectionStateForPostgreSQL() { + System.setProperty( + ConnectionOptions.ENABLE_TRANSACTIONAL_CONNECTION_STATE_FOR_POSTGRESQL_PROPERTY, "true"); + } + + @AfterClass + public static void disableTransactionalConnectionStateForPostgreSQL() { + System.clearProperty( + ConnectionOptions.ENABLE_TRANSACTIONAL_CONNECTION_STATE_FOR_POSTGRESQL_PROPERTY); + } + + @Before + public void setupDialect() { + if (currentDialect != dialect) { + // Reset the dialect result. + SpannerPool.closeSpannerPool(); + mockSpanner.putStatementResult(StatementResult.detectDialectResult(dialect)); + currentDialect = dialect; + } + } + + @After + public void clearRequests() { + mockSpanner.clearRequests(); + } + + protected String getBaseUrl() { + return String.format( + "cloudspanner://localhost:%d/projects/proj/instances/inst/databases/db?usePlainText=true", + getPort()); + } + + ITConnection createConnection(ConnectionState.Type type) { + return createConnection(";" + CONNECTION_STATE_TYPE.getKey() + "=" + type.name()); + } + + @Test + public void testConnectionStateType() { + try (Connection connection = createConnection()) { + // The default ConnectionState.Type should depend on the dialect. + assertEquals( + dialect == Dialect.POSTGRESQL ? Type.TRANSACTIONAL : Type.NON_TRANSACTIONAL, + ((ConnectionImpl) connection).getConnectionStateType()); + } + // It should be possible to override the default ConnectionState.Type, irrespective of the + // database dialect. + try (Connection connection = createConnection(Type.TRANSACTIONAL)) { + assertEquals(Type.TRANSACTIONAL, ((ConnectionImpl) connection).getConnectionStateType()); + } + try (Connection connection = createConnection(Type.NON_TRANSACTIONAL)) { + assertEquals(Type.NON_TRANSACTIONAL, ((ConnectionImpl) connection).getConnectionStateType()); + } + } + + @Test + public void testAutocommitPersistsConnectionState() { + try (Connection connection = createConnection(";autocommit=true")) { + assertTrue(connection.isAutocommit()); + + assertEquals(AutocommitDmlMode.TRANSACTIONAL, connection.getAutocommitDmlMode()); + connection.setAutocommitDmlMode(AutocommitDmlMode.PARTITIONED_NON_ATOMIC); + assertEquals(AutocommitDmlMode.PARTITIONED_NON_ATOMIC, connection.getAutocommitDmlMode()); + } + } + + @Test + public void testNonTransactionalState_commitsAutomatically() { + try (Connection connection = + createConnection(";connection_state_type=non_transactional;autocommit=false")) { + assertEquals(((ConnectionImpl) connection).getConnectionStateType(), Type.NON_TRANSACTIONAL); + assertFalse(connection.isAutocommit()); + + // Verify the initial default value. + assertFalse(connection.isReturnCommitStats()); + + // Change the value and read it back in the same transaction. + connection.setReturnCommitStats(true); + assertTrue(connection.isReturnCommitStats()); + + // Rolling back should not have any impact on the connection state, as the connection state is + // non-transactional. + connection.rollback(); + assertTrue(connection.isReturnCommitStats()); + + // Verify that the behavior is the same with autocommit=true and a temporary transaction. + assertTrue(connection.isReturnCommitStats()); + connection.setAutocommit(true); + connection.beginTransaction(); + connection.setReturnCommitStats(false); + assertFalse(connection.isReturnCommitStats()); + connection.rollback(); + assertFalse(connection.isReturnCommitStats()); + } + } + + @Test + public void testTransactionalState_rollBacksConnectionState() { + try (Connection connection = + createConnection(";connection_state_type=transactional;autocommit=false")) { + assertEquals(((ConnectionImpl) connection).getConnectionStateType(), Type.TRANSACTIONAL); + assertFalse(connection.isAutocommit()); + + // Verify the initial default value. + assertFalse(connection.isReturnCommitStats()); + + // Change the value and read it back in the same transaction. + connection.setReturnCommitStats(true); + assertTrue(connection.isReturnCommitStats()); + + // Rolling back will undo the connection state change. + connection.rollback(); + assertFalse(connection.isReturnCommitStats()); + + // Verify that the behavior is the same with autocommit=true and a temporary transaction. + assertFalse(connection.isReturnCommitStats()); + connection.setAutocommit(true); + connection.beginTransaction(); + connection.setReturnCommitStats(true); + assertTrue(connection.isReturnCommitStats()); + connection.rollback(); + assertFalse(connection.isReturnCommitStats()); + } + } + + @Test + public void testTransactionalState_commitsConnectionState() { + try (Connection connection = + createConnection(";connection_state_type=transactional;autocommit=false")) { + assertEquals(((ConnectionImpl) connection).getConnectionStateType(), Type.TRANSACTIONAL); + assertFalse(connection.isAutocommit()); + + // Verify the initial default value. + assertFalse(connection.isReturnCommitStats()); + + // Change the value and read it back in the same transaction. + connection.setReturnCommitStats(true); + assertTrue(connection.isReturnCommitStats()); + + // Committing will persist the connection state change. + connection.commit(); + assertTrue(connection.isReturnCommitStats()); + + // Verify that the behavior is the same with autocommit=true and a temporary transaction. + assertTrue(connection.isReturnCommitStats()); + connection.setAutocommit(true); + connection.beginTransaction(); + connection.setReturnCommitStats(false); + assertFalse(connection.isReturnCommitStats()); + connection.commit(); + assertFalse(connection.isReturnCommitStats()); + } + } + + @Test + public void testLocalChangeIsLostAfterTransaction() { + // SET LOCAL ... has the same effect regardless of connection state type. + for (ConnectionState.Type type : Type.values()) { + try (ConnectionImpl connection = (ConnectionImpl) createConnection()) { + assertTrue(connection.isAutocommit()); + + for (boolean commit : new boolean[] {true, false}) { + // Verify the initial default value. + assertFalse(connection.isReturnCommitStats()); + + connection.beginTransaction(); + // Change the value and read it back in the same transaction. + connection.setReturnCommitStats(true, /* local = */ true); + assertTrue(connection.isReturnCommitStats()); + // Both rolling back and committing will undo the connection state change. + if (commit) { + connection.commit(); + } else { + connection.rollback(); + } + // The local change should now be undone. + assertFalse(connection.isReturnCommitStats()); + } + } + } + } +} diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionStateTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionStateTest.java new file mode 100644 index 00000000000..7d613a3eef9 --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionStateTest.java @@ -0,0 +1,267 @@ +/* + * Copyright 2024 Google LLC + * + * 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.google.cloud.spanner.connection; + +import static com.google.cloud.spanner.connection.ConnectionProperties.AUTOCOMMIT_DML_MODE; +import static com.google.cloud.spanner.connection.ConnectionProperties.CONNECTION_STATE_TYPE; +import static com.google.cloud.spanner.connection.ConnectionProperties.READONLY; +import static com.google.cloud.spanner.connection.ConnectionProperties.RETRY_ABORTS_INTERNALLY; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; + +import com.google.cloud.NoCredentials; +import com.google.cloud.spanner.ErrorCode; +import com.google.cloud.spanner.SpannerException; +import com.google.cloud.spanner.connection.ConnectionProperty.Context; +import com.google.cloud.spanner.connection.ConnectionState.Type; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class ConnectionStateTest { + + @Parameters(name = "connectionStateType = {0}") + public static Object[] data() { + return ConnectionState.Type.values(); + } + + @SuppressWarnings("ClassEscapesDefinedScope") + @Parameter + public ConnectionState.Type connectionStateType; + + ConnectionState getNonTransactionalState() { + return new ConnectionState( + createConnectionOptionsBuilder().build().getInitialConnectionPropertyValues()); + } + + ConnectionState getTransactionalState() { + return new ConnectionState( + createConnectionOptionsBuilder() + .setConnectionPropertyValue(CONNECTION_STATE_TYPE, Type.TRANSACTIONAL) + .build() + .getInitialConnectionPropertyValues()); + } + + ConnectionOptions.Builder createConnectionOptionsBuilder() { + return ConnectionOptions.newBuilder() + .setUri("cloudspanner:/projects/p/instances/i/databases/d") + .setCredentials(NoCredentials.getInstance()); + } + + ConnectionState getConnectionState() { + return connectionStateType == Type.TRANSACTIONAL + ? getTransactionalState() + : getNonTransactionalState(); + } + + @Test + public void testSetOutsideTransaction() { + ConnectionState state = getConnectionState(); + assertEquals(connectionStateType, state.getType()); + + assertEquals(false, state.getValue(READONLY).getValue()); + state.setValue(READONLY, true, Context.USER, /* inTransaction = */ false); + assertEquals(true, state.getValue(READONLY).getValue()); + } + + @Test + public void testSetToNullOutsideTransaction() { + ConnectionState state = getConnectionState(); + assertEquals(AutocommitDmlMode.TRANSACTIONAL, state.getValue(AUTOCOMMIT_DML_MODE).getValue()); + state.setValue(AUTOCOMMIT_DML_MODE, null, Context.USER, /* inTransaction = */ false); + assertNull(state.getValue(AUTOCOMMIT_DML_MODE).getValue()); + } + + @Test + public void testSetInTransactionCommit() { + ConnectionState state = getConnectionState(); + assertEquals(true, state.getValue(RETRY_ABORTS_INTERNALLY).getValue()); + state.setValue(RETRY_ABORTS_INTERNALLY, false, Context.USER, /* inTransaction = */ true); + assertEquals(false, state.getValue(RETRY_ABORTS_INTERNALLY).getValue()); + + // Verify that the change is persisted if the transaction is committed. + state.commit(); + assertEquals(false, state.getValue(RETRY_ABORTS_INTERNALLY).getValue()); + } + + @Test + public void testSetInTransactionRollback() { + ConnectionState state = getConnectionState(); + assertEquals(true, state.getValue(RETRY_ABORTS_INTERNALLY).getValue()); + state.setValue(RETRY_ABORTS_INTERNALLY, false, Context.USER, /* inTransaction = */ true); + assertEquals(false, state.getValue(RETRY_ABORTS_INTERNALLY).getValue()); + + // Verify that the change is rolled back if the transaction is rolled back and the connection + // state is transactional. + state.rollback(); + // The value should rolled back to true if the state is transactional. + // The value should (still) be false if the state is non-transactional. + boolean expectedValue = connectionStateType == Type.TRANSACTIONAL; + assertEquals(expectedValue, state.getValue(RETRY_ABORTS_INTERNALLY).getValue()); + } + + @Test + public void testResetInTransactionCommit() { + ConnectionState state = getConnectionState(); + assertEquals(true, state.getValue(RETRY_ABORTS_INTERNALLY).getValue()); + state.setValue(RETRY_ABORTS_INTERNALLY, false, Context.USER, /* inTransaction = */ true); + assertEquals(false, state.getValue(RETRY_ABORTS_INTERNALLY).getValue()); + state.commit(); + + // Reset the value to the default (true). + state.resetValue(RETRY_ABORTS_INTERNALLY, Context.USER, /* inTransaction = */ true); + assertEquals(true, state.getValue(RETRY_ABORTS_INTERNALLY).getValue()); + + // Verify that the change is persisted if the transaction is committed. + state.commit(); + assertEquals(true, state.getValue(RETRY_ABORTS_INTERNALLY).getValue()); + } + + @Test + public void testResetInTransactionRollback() { + ConnectionState state = getConnectionState(); + assertEquals(true, state.getValue(RETRY_ABORTS_INTERNALLY).getValue()); + state.setValue(RETRY_ABORTS_INTERNALLY, false, Context.USER, /* inTransaction = */ true); + assertEquals(false, state.getValue(RETRY_ABORTS_INTERNALLY).getValue()); + state.commit(); + + // Reset the value to the default (true). + state.resetValue(RETRY_ABORTS_INTERNALLY, Context.USER, /* inTransaction = */ true); + assertEquals(true, state.getValue(RETRY_ABORTS_INTERNALLY).getValue()); + + // Verify that the change is rolled back if the transaction is rolled back and the connection + // state is transactional. + state.rollback(); + // The value should rolled back to false if the state is transactional. + // The value should (still) be true if the state is non-transactional. + boolean expectedValue = connectionStateType != Type.TRANSACTIONAL; + assertEquals(expectedValue, state.getValue(RETRY_ABORTS_INTERNALLY).getValue()); + } + + @Test + public void testSetLocal() { + ConnectionState state = getConnectionState(); + assertEquals(true, state.getValue(RETRY_ABORTS_INTERNALLY).getValue()); + state.setLocalValue(RETRY_ABORTS_INTERNALLY, false); + assertEquals(false, state.getValue(RETRY_ABORTS_INTERNALLY).getValue()); + + // Verify that the change is no longer visible once the transaction has ended, even if the + // transaction was committed. + state.commit(); + assertEquals(true, state.getValue(RETRY_ABORTS_INTERNALLY).getValue()); + } + + @Test + public void testSetLocalForStartupProperty() { + ConnectionState state = getConnectionState(); + SpannerException exception = + assertThrows( + SpannerException.class, + () -> state.setLocalValue(CONNECTION_STATE_TYPE, Type.TRANSACTIONAL)); + assertEquals(ErrorCode.FAILED_PRECONDITION, exception.getErrorCode()); + } + + @Test + public void testSetInTransactionForStartupProperty() { + ConnectionState state = getConnectionState(); + SpannerException exception = + assertThrows( + SpannerException.class, + () -> + state.setValue( + CONNECTION_STATE_TYPE, + Type.TRANSACTIONAL, + Context.USER, + /* inTransaction = */ true)); + assertEquals(ErrorCode.FAILED_PRECONDITION, exception.getErrorCode()); + } + + @Test + public void testSetStartupOnlyProperty() { + ConnectionState state = getConnectionState(); + SpannerException exception = + assertThrows( + SpannerException.class, + () -> + state.setValue( + CONNECTION_STATE_TYPE, + Type.TRANSACTIONAL, + Context.USER, + /* inTransaction = */ false)); + assertEquals(ErrorCode.FAILED_PRECONDITION, exception.getErrorCode()); + } + + @Test + public void testReset() { + ConnectionState state = getConnectionState(); + // The default should be true. + assertEquals(true, state.getValue(RETRY_ABORTS_INTERNALLY).getValue()); + state.setValue(RETRY_ABORTS_INTERNALLY, false, Context.USER, /* inTransaction = */ false); + assertEquals(false, state.getValue(RETRY_ABORTS_INTERNALLY).getValue()); + + // Resetting the property should reset it to the default value. + state.resetValue(RETRY_ABORTS_INTERNALLY, Context.USER, /* inTransaction = */ false); + assertEquals(true, state.getValue(RETRY_ABORTS_INTERNALLY).getValue()); + } + + @Test + public void testResetInTransaction() { + ConnectionState state = getConnectionState(); + // The default should be true. + assertEquals(true, state.getValue(RETRY_ABORTS_INTERNALLY).getValue()); + state.setValue(RETRY_ABORTS_INTERNALLY, false, Context.USER, /* inTransaction = */ true); + assertEquals(false, state.getValue(RETRY_ABORTS_INTERNALLY).getValue()); + state.commit(); + + // Resetting the property should reset it to the default value. + state.resetValue(RETRY_ABORTS_INTERNALLY, Context.USER, /* inTransaction = */ true); + assertEquals(true, state.getValue(RETRY_ABORTS_INTERNALLY).getValue()); + } + + @Test + public void testResetStartupOnlyProperty() { + ConnectionState state = getConnectionState(); + SpannerException exception = + assertThrows( + SpannerException.class, + () -> + state.resetValue(CONNECTION_STATE_TYPE, Context.USER, /* inTransaction = */ false)); + assertEquals(ErrorCode.FAILED_PRECONDITION, exception.getErrorCode()); + } + + @Test + public void testInitialValueInConnectionUrl() { + ConnectionOptions options = + ConnectionOptions.newBuilder() + .setUri("cloudspanner:/projects/p/instances/i/databases/d?retryAbortsInternally=false") + .setCredentials(NoCredentials.getInstance()) + .build(); + ConnectionState state = new ConnectionState(options.getInitialConnectionPropertyValues()); + + assertEquals(false, state.getValue(RETRY_ABORTS_INTERNALLY).getValue()); + state.setValue(RETRY_ABORTS_INTERNALLY, true, Context.USER, /* inTransaction = */ false); + assertEquals(true, state.getValue(RETRY_ABORTS_INTERNALLY).getValue()); + + // Resetting the property should reset it to the value that was set in the connection URL. + state.resetValue(RETRY_ABORTS_INTERNALLY, Context.USER, /* inTransaction = */ false); + assertEquals(false, state.getValue(RETRY_ABORTS_INTERNALLY).getValue()); + } +} diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/CredentialsProviderTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/CredentialsProviderTest.java index 944e4adeb2d..9e2979e1aaf 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/CredentialsProviderTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/CredentialsProviderTest.java @@ -21,9 +21,11 @@ import com.google.api.gax.core.CredentialsProvider; import com.google.auth.Credentials; +import com.google.auth.oauth2.AccessToken; import com.google.auth.oauth2.OAuth2Credentials; import io.grpc.ManagedChannelBuilder; import java.io.ObjectStreamException; +import java.util.Date; import java.util.concurrent.atomic.AtomicInteger; import org.junit.BeforeClass; import org.junit.Test; @@ -50,6 +52,14 @@ private Object readResolve() throws ObjectStreamException { return this; } + @Override + public AccessToken refreshAccessToken() { + return AccessToken.newBuilder() + .setTokenValue("foo") + .setExpirationTime(new Date(Long.MAX_VALUE)) + .build(); + } + public boolean equals(Object obj) { if (!(obj instanceof TestCredentials)) { return false; diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DdlBatchTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DdlBatchTest.java index b6a12567101..93ae60891fb 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DdlBatchTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DdlBatchTest.java @@ -610,7 +610,7 @@ public void testCancel() { public void testCommit() { DdlBatch batch = createSubject(); try { - batch.commitAsync(CallType.SYNC); + batch.commitAsync(CallType.SYNC, NoopEndTransactionCallback.INSTANCE); fail("expected FAILED_PRECONDITION"); } catch (SpannerException e) { assertEquals(ErrorCode.FAILED_PRECONDITION, e.getErrorCode()); @@ -621,7 +621,7 @@ public void testCommit() { public void testRollback() { DdlBatch batch = createSubject(); try { - batch.rollbackAsync(CallType.SYNC); + batch.rollbackAsync(CallType.SYNC, NoopEndTransactionCallback.INSTANCE); fail("expected FAILED_PRECONDITION"); } catch (SpannerException e) { assertEquals(ErrorCode.FAILED_PRECONDITION, e.getErrorCode()); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DmlBatchTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DmlBatchTest.java index d71376df40f..629ae41daf4 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DmlBatchTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DmlBatchTest.java @@ -195,7 +195,7 @@ public void testGetStateAndIsActive() { public void testCommit() { DmlBatch batch = createSubject(); try { - batch.commitAsync(CallType.SYNC); + batch.commitAsync(CallType.SYNC, NoopEndTransactionCallback.INSTANCE); fail("Expected exception"); } catch (SpannerException e) { assertEquals(ErrorCode.FAILED_PRECONDITION, e.getErrorCode()); @@ -206,7 +206,7 @@ public void testCommit() { public void testRollback() { DmlBatch batch = createSubject(); try { - batch.rollbackAsync(CallType.SYNC); + batch.rollbackAsync(CallType.SYNC, NoopEndTransactionCallback.INSTANCE); fail("Expected exception"); } catch (SpannerException e) { assertEquals(ErrorCode.FAILED_PRECONDITION, e.getErrorCode()); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/NoopEndTransactionCallback.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/NoopEndTransactionCallback.java new file mode 100644 index 00000000000..6145d9770b5 --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/NoopEndTransactionCallback.java @@ -0,0 +1,31 @@ +/* + * Copyright 2023 Google LLC + * + * 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.google.cloud.spanner.connection; + +import com.google.cloud.spanner.connection.UnitOfWork.EndTransactionCallback; + +class NoopEndTransactionCallback implements EndTransactionCallback { + static final NoopEndTransactionCallback INSTANCE = new NoopEndTransactionCallback(); + + private NoopEndTransactionCallback() {} + + @Override + public void onSuccess() {} + + @Override + public void onFailure() {} +} diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/QueryOptionsTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/QueryOptionsTest.java index 627e8c7acc9..6d5d4106638 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/QueryOptionsTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/QueryOptionsTest.java @@ -30,7 +30,7 @@ public class QueryOptionsTest extends AbstractMockServerTest { @Test public void testUseOptimizerVersionFromConnectionUrl() { - try (Connection connection = createConnection("?optimizerVersion=10")) { + try (Connection connection = createConnection(";optimizerVersion=10")) { Repeat.twice( () -> { executeSelect1AndConsumeResults(connection); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReadOnlyTransactionTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReadOnlyTransactionTest.java index 752b467b484..e243fbd620a 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReadOnlyTransactionTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReadOnlyTransactionTest.java @@ -245,7 +245,7 @@ public void testAbortBatch() { @Test public void testGetCommitTimestamp() { ReadOnlyTransaction transaction = createSubject(); - get(transaction.commitAsync(CallType.SYNC)); + get(transaction.commitAsync(CallType.SYNC, NoopEndTransactionCallback.INSTANCE)); assertThat(transaction.getState(), is(UnitOfWorkState.COMMITTED)); try { transaction.getCommitTimestamp(); @@ -258,7 +258,7 @@ public void testGetCommitTimestamp() { @Test public void testGetCommitResponse() { ReadOnlyTransaction transaction = createSubject(); - get(transaction.commitAsync(CallType.SYNC)); + get(transaction.commitAsync(CallType.SYNC, NoopEndTransactionCallback.INSTANCE)); try { transaction.getCommitResponse(); fail("expected FAILED_PRECONDITION"); @@ -270,7 +270,7 @@ public void testGetCommitResponse() { @Test public void testGetCommitResponseOrNull() { ReadOnlyTransaction transaction = createSubject(); - get(transaction.commitAsync(CallType.SYNC)); + get(transaction.commitAsync(CallType.SYNC, NoopEndTransactionCallback.INSTANCE)); assertNull(transaction.getCommitResponseOrNull()); } @@ -430,7 +430,7 @@ public void testState() { transaction.getState(), is(equalTo(com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState.STARTED))); assertThat(transaction.isActive(), is(true)); - get(transaction.commitAsync(CallType.SYNC)); + get(transaction.commitAsync(CallType.SYNC, NoopEndTransactionCallback.INSTANCE)); assertThat( transaction.getState(), is(equalTo(com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState.COMMITTED))); @@ -449,7 +449,7 @@ public void testState() { is(equalTo(com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState.STARTED))); assertThat(transaction.isActive(), is(true)); - get(transaction.commitAsync(CallType.SYNC)); + get(transaction.commitAsync(CallType.SYNC, NoopEndTransactionCallback.INSTANCE)); assertThat( transaction.getState(), is(equalTo(com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState.COMMITTED))); @@ -461,7 +461,7 @@ public void testState() { transaction.getState(), is(equalTo(com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState.STARTED))); assertThat(transaction.isActive(), is(true)); - get(transaction.rollbackAsync(CallType.SYNC)); + get(transaction.rollbackAsync(CallType.SYNC, NoopEndTransactionCallback.INSTANCE)); assertThat( transaction.getState(), is(equalTo(com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState.ROLLED_BACK))); @@ -479,7 +479,7 @@ public void testState() { transaction.getState(), is(equalTo(com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState.STARTED))); assertThat(transaction.isActive(), is(true)); - get(transaction.rollbackAsync(CallType.SYNC)); + get(transaction.rollbackAsync(CallType.SYNC, NoopEndTransactionCallback.INSTANCE)); assertThat( transaction.getState(), is(equalTo(com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState.ROLLED_BACK))); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReadWriteTransactionTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReadWriteTransactionTest.java index 136680b8a06..9fbb5b5bf16 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReadWriteTransactionTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReadWriteTransactionTest.java @@ -308,7 +308,7 @@ public void testGetCommitTimestampAfterCommit() { ReadWriteTransaction transaction = createSubject(); assertThat(get(transaction.executeUpdateAsync(CallType.SYNC, parsedStatement)), is(1L)); - get(transaction.commitAsync(CallType.SYNC)); + get(transaction.commitAsync(CallType.SYNC, NoopEndTransactionCallback.INSTANCE)); assertThat(transaction.getCommitTimestamp(), is(notNullValue())); } @@ -354,7 +354,7 @@ public void testState() { is(equalTo(com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState.STARTED))); assertThat(transaction.isActive(), is(true)); - get(transaction.commitAsync(CallType.SYNC)); + get(transaction.commitAsync(CallType.SYNC, NoopEndTransactionCallback.INSTANCE)); assertThat( transaction.getState(), is(equalTo(com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState.COMMITTED))); @@ -366,7 +366,7 @@ public void testState() { transaction.getState(), is(equalTo(com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState.STARTED))); assertThat(transaction.isActive(), is(true)); - get(transaction.rollbackAsync(CallType.SYNC)); + get(transaction.rollbackAsync(CallType.SYNC, NoopEndTransactionCallback.INSTANCE)); assertThat( transaction.getState(), is(equalTo(com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState.ROLLED_BACK))); @@ -379,7 +379,7 @@ public void testState() { is(equalTo(com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState.STARTED))); assertThat(transaction.isActive(), is(true)); try { - get(transaction.commitAsync(CallType.SYNC)); + get(transaction.commitAsync(CallType.SYNC, NoopEndTransactionCallback.INSTANCE)); } catch (SpannerException e) { // ignore } @@ -395,7 +395,7 @@ public void testState() { is(equalTo(com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState.STARTED))); assertThat(transaction.isActive(), is(true)); try { - get(transaction.commitAsync(CallType.SYNC)); + get(transaction.commitAsync(CallType.SYNC, NoopEndTransactionCallback.INSTANCE)); } catch (AbortedException e) { // ignore } @@ -411,7 +411,7 @@ public void testState() { transaction.getState(), is(equalTo(com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState.STARTED))); assertThat(transaction.isActive(), is(true)); - get(transaction.commitAsync(CallType.SYNC)); + get(transaction.commitAsync(CallType.SYNC, NoopEndTransactionCallback.INSTANCE)); assertThat( transaction.getState(), is(equalTo(com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState.COMMITTED))); @@ -483,7 +483,7 @@ public void testRetry() { subject.executeUpdateAsync(CallType.SYNC, update2); boolean expectedException = false; try { - get(subject.commitAsync(CallType.SYNC)); + get(subject.commitAsync(CallType.SYNC, NoopEndTransactionCallback.INSTANCE)); } catch (SpannerException e) { if (results == RetryResults.DIFFERENT && e.getErrorCode() == ErrorCode.ABORTED) { // expected @@ -826,7 +826,7 @@ public void testGetCommitResponseAfterCommit() { ReadWriteTransaction transaction = createSubject(); get(transaction.executeUpdateAsync(CallType.SYNC, parsedStatement)); - get(transaction.commitAsync(CallType.SYNC)); + get(transaction.commitAsync(CallType.SYNC, NoopEndTransactionCallback.INSTANCE)); assertNotNull(transaction.getCommitResponse()); assertNotNull(transaction.getCommitResponseOrNull()); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/SingleUseTransactionTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/SingleUseTransactionTest.java index 1fd6d6bd7e2..6edf46b5623 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/SingleUseTransactionTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/SingleUseTransactionTest.java @@ -516,7 +516,7 @@ private List getTestTimestampBounds() { public void testCommit() { SingleUseTransaction subject = createSubject(); try { - subject.commitAsync(CallType.SYNC); + subject.commitAsync(CallType.SYNC, NoopEndTransactionCallback.INSTANCE); fail("missing expected exception"); } catch (SpannerException e) { assertThat(e.getErrorCode()).isEqualTo(ErrorCode.FAILED_PRECONDITION); @@ -527,7 +527,7 @@ public void testCommit() { public void testRollback() { SingleUseTransaction subject = createSubject(); try { - subject.rollbackAsync(CallType.SYNC); + subject.rollbackAsync(CallType.SYNC, NoopEndTransactionCallback.INSTANCE); fail("missing expected exception"); } catch (SpannerException e) { assertThat(e.getErrorCode()).isEqualTo(ErrorCode.FAILED_PRECONDITION);