Skip to content

Commit 5722372

Browse files
authored
feat: add support for tagging to Connection API (#623)
* feat: add support for tagging in Connection API * fix: disallow statement tags for commit/rollback/run * chore: cleanup after rebase * test: add generated tests + cleanup * build: add new methods to clirr * fix: add default implementations for new methods
1 parent f257671 commit 5722372

26 files changed

+10184
-6496
lines changed

google-cloud-spanner/clirr-ignored-differences.xml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,28 @@
592592
<className>com/google/cloud/spanner/AsyncTransactionManager$CommitTimestampFuture</className>
593593
<method>java.lang.Object get()</method>
594594
</difference>
595+
<!-- Support for tagging in Connection API -->
596+
<!-- These are not breaking changes, since we provide default interface implementation -->
597+
<difference>
598+
<differenceType>7012</differenceType>
599+
<className>com/google/cloud/spanner/connection/Connection</className>
600+
<method>java.lang.String getStatementTag()</method>
601+
</difference>
602+
<difference>
603+
<differenceType>7012</differenceType>
604+
<className>com/google/cloud/spanner/connection/Connection</className>
605+
<method>void setStatementTag(java.lang.String)</method>
606+
</difference>
607+
<difference>
608+
<differenceType>7012</differenceType>
609+
<className>com/google/cloud/spanner/connection/Connection</className>
610+
<method>java.lang.String getTransactionTag()</method>
611+
</difference>
612+
<difference>
613+
<differenceType>7012</differenceType>
614+
<className>com/google/cloud/spanner/connection/Connection</className>
615+
<method>void setTransactionTag(java.lang.String)</method>
616+
</difference>
595617

596618
<!-- Adds getValue to ResultSet -->
597619
<!-- These are not breaking changes, since we provide default interface implementation -->

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractBaseUnitOfWork.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
abstract class AbstractBaseUnitOfWork implements UnitOfWork {
5252
private final StatementExecutor statementExecutor;
5353
private final StatementTimeout statementTimeout;
54+
protected final String transactionTag;
5455

5556
/** Class for keeping track of the stacktrace of the caller of an async statement. */
5657
static final class SpannerAsyncExecutionException extends RuntimeException {
@@ -82,6 +83,7 @@ enum InterceptorsUsage {
8283
abstract static class Builder<B extends Builder<?, T>, T extends AbstractBaseUnitOfWork> {
8384
private StatementExecutor statementExecutor;
8485
private StatementTimeout statementTimeout = new StatementTimeout();
86+
private String transactionTag;
8587

8688
Builder() {}
8789

@@ -102,13 +104,19 @@ B setStatementTimeout(StatementTimeout timeout) {
102104
return self();
103105
}
104106

107+
B setTransactionTag(@Nullable String tag) {
108+
this.transactionTag = tag;
109+
return self();
110+
}
111+
105112
abstract T build();
106113
}
107114

108115
AbstractBaseUnitOfWork(Builder<?, ?> builder) {
109116
Preconditions.checkState(builder.statementExecutor != null, "No statement executor specified");
110117
this.statementExecutor = builder.statementExecutor;
111118
this.statementTimeout = builder.statementTimeout;
119+
this.transactionTag = builder.transactionTag;
112120
}
113121

114122
StatementExecutor getStatementExecutor() {

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/Connection.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import com.google.cloud.spanner.Statement;
3434
import com.google.cloud.spanner.TimestampBound;
3535
import com.google.cloud.spanner.connection.StatementResult.ResultType;
36+
import com.google.spanner.v1.ExecuteBatchDmlRequest;
3637
import java.util.Iterator;
3738
import java.util.concurrent.ExecutionException;
3839
import java.util.concurrent.TimeUnit;
@@ -330,6 +331,52 @@ public interface Connection extends AutoCloseable {
330331
*/
331332
TransactionMode getTransactionMode();
332333

334+
/**
335+
* Sets the transaction tag to use for the current transaction. This method may only be called
336+
* when in a transaction and before any statements have been executed in the transaction.
337+
*
338+
* <p>The tag will be set as the transaction tag of all statements during the transaction, and as
339+
* the transaction tag of the commit.
340+
*
341+
* <p>The transaction tag will automatically be cleared after the transaction has ended.
342+
*
343+
* @param tag The tag to use.
344+
*/
345+
default void setTransactionTag(String tag) {
346+
throw new UnsupportedOperationException();
347+
}
348+
349+
/** @return The transaction tag of the current transaction. */
350+
default String getTransactionTag() {
351+
throw new UnsupportedOperationException();
352+
}
353+
354+
/**
355+
* Sets the statement tag to use for the next statement that is executed. The tag is automatically
356+
* cleared after the statement is executed. Statement tags can be used both with autocommit=true
357+
* and autocommit=false, and can be used for partitioned DML.
358+
*
359+
* <p>Statement tags are not allowed before COMMIT and ROLLBACK statements.
360+
*
361+
* <p>Statement tags are allowed before START BATCH DML statements and will be included in the
362+
* {@link ExecuteBatchDmlRequest} that is sent to Spanner. Statement tags are not allowed inside a
363+
* batch.
364+
*
365+
* @param tag The statement tag to use with the next statement that will be executed on this
366+
* connection.
367+
*/
368+
default void setStatementTag(String tag) {
369+
throw new UnsupportedOperationException();
370+
}
371+
372+
/**
373+
* @return The statement tag that will be used with the next statement that is executed on this
374+
* connection.
375+
*/
376+
default String getStatementTag() {
377+
throw new UnsupportedOperationException();
378+
}
379+
333380
/**
334381
* @return <code>true</code> if this connection will automatically retry read/write transactions
335382
* that abort. This method may only be called when the connection is in read/write

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java

Lines changed: 92 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@
2626
import com.google.cloud.spanner.DatabaseClient;
2727
import com.google.cloud.spanner.ErrorCode;
2828
import com.google.cloud.spanner.Mutation;
29+
import com.google.cloud.spanner.Options;
2930
import com.google.cloud.spanner.Options.QueryOption;
31+
import com.google.cloud.spanner.Options.UpdateOption;
3032
import com.google.cloud.spanner.ReadContext.QueryAnalyzeMode;
3133
import com.google.cloud.spanner.ResultSet;
3234
import com.google.cloud.spanner.ResultSets;
@@ -45,6 +47,7 @@
4547
import com.google.common.util.concurrent.MoreExecutors;
4648
import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions;
4749
import java.util.ArrayList;
50+
import java.util.Arrays;
4851
import java.util.Collections;
4952
import java.util.Iterator;
5053
import java.util.LinkedList;
@@ -206,6 +209,9 @@ static UnitOfWorkType of(TransactionMode transactionMode) {
206209
private TimestampBound readOnlyStaleness = TimestampBound.strong();
207210
private QueryOptions queryOptions = QueryOptions.getDefaultInstance();
208211

212+
private String transactionTag;
213+
private String statementTag;
214+
209215
/** Create a connection and register it in the SpannerPool. */
210216
ConnectionImpl(ConnectionOptions options) {
211217
Preconditions.checkNotNull(options);
@@ -512,6 +518,47 @@ public void setTransactionMode(TransactionMode transactionMode) {
512518
this.unitOfWorkType = UnitOfWorkType.of(transactionMode);
513519
}
514520

521+
@Override
522+
public String getTransactionTag() {
523+
ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG);
524+
ConnectionPreconditions.checkState(!isDdlBatchActive(), "This connection is in a DDL batch");
525+
return transactionTag;
526+
}
527+
528+
@Override
529+
public void setTransactionTag(String tag) {
530+
ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG);
531+
ConnectionPreconditions.checkState(
532+
!isBatchActive(), "Cannot set transaction tag while in a batch");
533+
ConnectionPreconditions.checkState(isInTransaction(), "This connection has no transaction");
534+
ConnectionPreconditions.checkState(
535+
!isTransactionStarted(),
536+
"The transaction tag cannot be set after the transaction has started");
537+
ConnectionPreconditions.checkState(
538+
getTransactionMode() == TransactionMode.READ_WRITE_TRANSACTION,
539+
"Transaction tag can only be set for a read/write transaction");
540+
541+
this.transactionBeginMarked = true;
542+
this.transactionTag = tag;
543+
}
544+
545+
@Override
546+
public String getStatementTag() {
547+
ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG);
548+
ConnectionPreconditions.checkState(
549+
!isBatchActive(), "Statement tags are not allowed inside a batch");
550+
return statementTag;
551+
}
552+
553+
@Override
554+
public void setStatementTag(String tag) {
555+
ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG);
556+
ConnectionPreconditions.checkState(
557+
!isBatchActive(), "Statement tags are not allowed inside a batch");
558+
559+
this.statementTag = tag;
560+
}
561+
515562
/**
516563
* Throws an {@link SpannerException} with code {@link ErrorCode#FAILED_PRECONDITION} if the
517564
* current state of this connection does not allow changing the setting for retryAbortsInternally.
@@ -643,6 +690,7 @@ private void setDefaultTransactionOptions() {
643690
? UnitOfWorkType.READ_ONLY_TRANSACTION
644691
: UnitOfWorkType.READ_WRITE_TRANSACTION;
645692
batchMode = BatchMode.NONE;
693+
transactionTag = null;
646694
} else {
647695
popUnitOfWorkFromTransactionStack();
648696
}
@@ -717,6 +765,8 @@ public ApiFuture<Void> rollbackAsync() {
717765
private ApiFuture<Void> endCurrentTransactionAsync(EndTransactionMethod endTransactionMethod) {
718766
ConnectionPreconditions.checkState(!isBatchActive(), "This connection has an active batch");
719767
ConnectionPreconditions.checkState(isInTransaction(), "This connection has no transaction");
768+
ConnectionPreconditions.checkState(
769+
statementTag == null, "Statement tags are not supported for COMMIT or ROLLBACK");
720770
ApiFuture<Void> res;
721771
try {
722772
if (isTransactionStarted()) {
@@ -954,14 +1004,43 @@ public ApiFuture<long[]> executeBatchUpdateAsync(Iterable<Statement> updates) {
9541004
return internalExecuteBatchUpdateAsync(parsedStatements);
9551005
}
9561006

1007+
private QueryOption[] mergeQueryStatementTag(QueryOption... options) {
1008+
if (this.statementTag != null) {
1009+
// Shortcut for the most common scenario.
1010+
if (options == null || options.length == 0) {
1011+
options = new QueryOption[] {Options.tag(statementTag)};
1012+
} else {
1013+
options = Arrays.copyOf(options, options.length + 1);
1014+
options[options.length - 1] = Options.tag(statementTag);
1015+
}
1016+
this.statementTag = null;
1017+
}
1018+
return options;
1019+
}
1020+
1021+
private UpdateOption[] mergeUpdateStatementTag(UpdateOption... options) {
1022+
if (this.statementTag != null) {
1023+
// Shortcut for the most common scenario.
1024+
if (options == null || options.length == 0) {
1025+
options = new UpdateOption[] {Options.tag(statementTag)};
1026+
} else {
1027+
options = Arrays.copyOf(options, options.length + 1);
1028+
options[options.length - 1] = Options.tag(statementTag);
1029+
}
1030+
this.statementTag = null;
1031+
}
1032+
return options;
1033+
}
1034+
9571035
private ResultSet internalExecuteQuery(
9581036
final ParsedStatement statement,
9591037
final AnalyzeMode analyzeMode,
9601038
final QueryOption... options) {
9611039
Preconditions.checkArgument(
9621040
statement.getType() == StatementType.QUERY, "Statement must be a query");
9631041
UnitOfWork transaction = getCurrentUnitOfWorkOrStartNewUnitOfWork();
964-
return get(transaction.executeQueryAsync(statement, analyzeMode, options));
1042+
return get(
1043+
transaction.executeQueryAsync(statement, analyzeMode, mergeQueryStatementTag(options)));
9651044
}
9661045

9671046
private AsyncResultSet internalExecuteQueryAsync(
@@ -972,21 +1051,23 @@ private AsyncResultSet internalExecuteQueryAsync(
9721051
statement.getType() == StatementType.QUERY, "Statement must be a query");
9731052
UnitOfWork transaction = getCurrentUnitOfWorkOrStartNewUnitOfWork();
9741053
return ResultSets.toAsyncResultSet(
975-
transaction.executeQueryAsync(statement, analyzeMode, options),
1054+
transaction.executeQueryAsync(statement, analyzeMode, mergeQueryStatementTag(options)),
9761055
spanner.getAsyncExecutorProvider(),
9771056
options);
9781057
}
9791058

980-
private ApiFuture<Long> internalExecuteUpdateAsync(final ParsedStatement update) {
1059+
private ApiFuture<Long> internalExecuteUpdateAsync(
1060+
final ParsedStatement update, UpdateOption... options) {
9811061
Preconditions.checkArgument(
9821062
update.getType() == StatementType.UPDATE, "Statement must be an update");
9831063
UnitOfWork transaction = getCurrentUnitOfWorkOrStartNewUnitOfWork();
984-
return transaction.executeUpdateAsync(update);
1064+
return transaction.executeUpdateAsync(update, mergeUpdateStatementTag(options));
9851065
}
9861066

987-
private ApiFuture<long[]> internalExecuteBatchUpdateAsync(List<ParsedStatement> updates) {
1067+
private ApiFuture<long[]> internalExecuteBatchUpdateAsync(
1068+
List<ParsedStatement> updates, UpdateOption... options) {
9881069
UnitOfWork transaction = getCurrentUnitOfWorkOrStartNewUnitOfWork();
989-
return transaction.executeBatchUpdateAsync(updates);
1070+
return transaction.executeBatchUpdateAsync(updates, mergeUpdateStatementTag(options));
9901071
}
9911072

9921073
/**
@@ -1001,7 +1082,8 @@ UnitOfWork getCurrentUnitOfWorkOrStartNewUnitOfWork() {
10011082
return this.currentUnitOfWork;
10021083
}
10031084

1004-
private UnitOfWork createNewUnitOfWork() {
1085+
@VisibleForTesting
1086+
UnitOfWork createNewUnitOfWork() {
10051087
if (isAutocommit() && !isInTransaction() && !isInBatch()) {
10061088
return SingleUseTransaction.newBuilder()
10071089
.setDdlClient(ddlClient)
@@ -1021,6 +1103,7 @@ private UnitOfWork createNewUnitOfWork() {
10211103
.setReadOnlyStaleness(readOnlyStaleness)
10221104
.setStatementTimeout(statementTimeout)
10231105
.withStatementExecutor(statementExecutor)
1106+
.setTransactionTag(transactionTag)
10241107
.build();
10251108
case READ_WRITE_TRANSACTION:
10261109
return ReadWriteTransaction.newBuilder()
@@ -1030,6 +1113,7 @@ private UnitOfWork createNewUnitOfWork() {
10301113
.setTransactionRetryListeners(transactionRetryListeners)
10311114
.setStatementTimeout(statementTimeout)
10321115
.withStatementExecutor(statementExecutor)
1116+
.setTransactionTag(transactionTag)
10331117
.build();
10341118
case DML_BATCH:
10351119
// A DML batch can run inside the current transaction. It should therefore only
@@ -1039,6 +1123,7 @@ private UnitOfWork createNewUnitOfWork() {
10391123
.setTransaction(currentUnitOfWork)
10401124
.setStatementTimeout(statementTimeout)
10411125
.withStatementExecutor(statementExecutor)
1126+
.setStatementTag(statementTag)
10421127
.build();
10431128
case DDL_BATCH:
10441129
return DdlBatch.newBuilder()

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionStatementExecutor.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,14 @@ interface ConnectionStatementExecutor {
7474

7575
StatementResult statementShowReturnCommitStats();
7676

77+
StatementResult statementSetStatementTag(String tag);
78+
79+
StatementResult statementShowStatementTag();
80+
81+
StatementResult statementSetTransactionTag(String tag);
82+
83+
StatementResult statementShowTransactionTag();
84+
7785
StatementResult statementBeginTransaction();
7886

7987
StatementResult statementCommit();

0 commit comments

Comments
 (0)