Skip to content

Commit b2b1191

Browse files
authored
feat!: add CommitStats to Connection API (#608)
* feat!: add support for CommitStats Adds support for returning CommitStats from read/write transactions.
1 parent 6a41028 commit b2b1191

27 files changed

+7507
-3766
lines changed

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

+17
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,23 @@
476476
<method>com.google.cloud.spanner.CommitResponse getCommitResponse()</method>
477477
</difference>
478478

479+
<!-- Support for Commit Stats in Connection API -->
480+
<difference>
481+
<differenceType>7012</differenceType>
482+
<className>com/google/cloud/spanner/connection/Connection</className>
483+
<method>com.google.cloud.spanner.CommitResponse getCommitResponse()</method>
484+
</difference>
485+
<difference>
486+
<differenceType>7012</differenceType>
487+
<className>com/google/cloud/spanner/connection/Connection</className>
488+
<method>boolean isReturnCommitStats()</method>
489+
</difference>
490+
<difference>
491+
<differenceType>7012</differenceType>
492+
<className>com/google/cloud/spanner/connection/Connection</className>
493+
<method>void setReturnCommitStats(boolean)</method>
494+
</difference>
495+
479496
<!-- PITR -->
480497
<difference>
481498
<differenceType>7013</differenceType>

google-cloud-spanner/src/main/java/com/google/cloud/spanner/CommitResponse.java

+5
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ public Timestamp getCommitTimestamp() {
4141
return Timestamp.fromProto(proto.getCommitTimestamp());
4242
}
4343

44+
/** @return true if the {@link CommitResponse} includes {@link CommitStats} */
45+
public boolean hasCommitStats() {
46+
return proto.hasCommitStats();
47+
}
48+
4449
/**
4550
* Commit statistics are returned by a read/write transaction if specifically requested by passing
4651
* in {@link Options#commitStats()} to the transaction.

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

+27-6
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import com.google.cloud.spanner.AbortedDueToConcurrentModificationException;
2323
import com.google.cloud.spanner.AbortedException;
2424
import com.google.cloud.spanner.AsyncResultSet;
25+
import com.google.cloud.spanner.CommitResponse;
2526
import com.google.cloud.spanner.ErrorCode;
2627
import com.google.cloud.spanner.Mutation;
2728
import com.google.cloud.spanner.Options.QueryOption;
@@ -438,6 +439,15 @@ public interface Connection extends AutoCloseable {
438439
*/
439440
String getOptimizerVersion();
440441

442+
/**
443+
* Sets whether this connection should request commit statistics from Cloud Spanner for read/write
444+
* transactions and DML statements in autocommit mode.
445+
*/
446+
void setReturnCommitStats(boolean returnCommitStats);
447+
448+
/** @return true if this connection requests commit statistics from Cloud Spanner */
449+
boolean isReturnCommitStats();
450+
441451
/**
442452
* Commits the current transaction of this connection. All mutations that have been buffered
443453
* during the current transaction will be written to the database.
@@ -623,15 +633,26 @@ public interface Connection extends AutoCloseable {
623633

624634
/**
625635
* @return the commit timestamp of the last {@link TransactionMode#READ_WRITE_TRANSACTION}
626-
* transaction. This method will throw a {@link SpannerException} if there is no last {@link
627-
* TransactionMode#READ_WRITE_TRANSACTION} transaction (i.e. the last transaction was a {@link
628-
* TransactionMode#READ_ONLY_TRANSACTION}), or if the last {@link
629-
* TransactionMode#READ_WRITE_TRANSACTION} transaction rolled back. It will also throw a
630-
* {@link SpannerException} if the last {@link TransactionMode#READ_WRITE_TRANSACTION}
631-
* transaction was empty when committed.
636+
* transaction. This method throws a {@link SpannerException} if there is no last {@link
637+
* TransactionMode#READ_WRITE_TRANSACTION} transaction. That is, if the last transaction was a
638+
* {@link TransactionMode#READ_ONLY_TRANSACTION}), or if the last {@link
639+
* TransactionMode#READ_WRITE_TRANSACTION} transaction rolled back. It also throws a {@link
640+
* SpannerException} if the last {@link TransactionMode#READ_WRITE_TRANSACTION} transaction
641+
* was empty when committed.
632642
*/
633643
Timestamp getCommitTimestamp();
634644

645+
/**
646+
* @return the {@link CommitResponse} of the last {@link TransactionMode#READ_WRITE_TRANSACTION}
647+
* transaction. This method throws a {@link SpannerException} if there is no last {@link
648+
* TransactionMode#READ_WRITE_TRANSACTION} transaction. That is, if the last transaction was a
649+
* {@link TransactionMode#READ_ONLY_TRANSACTION}), or if the last {@link
650+
* TransactionMode#READ_WRITE_TRANSACTION} transaction rolled back. It also throws a {@link
651+
* SpannerException} if the last {@link TransactionMode#READ_WRITE_TRANSACTION} transaction
652+
* was empty when committed.
653+
*/
654+
CommitResponse getCommitResponse();
655+
635656
/**
636657
* Starts a new DDL batch on this connection. A DDL batch allows several DDL statements to be
637658
* grouped into a batch that can be executed as a group. DDL statements that are issued during the

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

+31
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import com.google.api.core.ApiFutures;
2323
import com.google.cloud.Timestamp;
2424
import com.google.cloud.spanner.AsyncResultSet;
25+
import com.google.cloud.spanner.CommitResponse;
2526
import com.google.cloud.spanner.DatabaseClient;
2627
import com.google.cloud.spanner.ErrorCode;
2728
import com.google.cloud.spanner.Mutation;
@@ -179,6 +180,7 @@ static UnitOfWorkType of(TransactionMode transactionMode) {
179180
private DatabaseClient dbClient;
180181
private boolean autocommit;
181182
private boolean readOnly;
183+
private boolean returnCommitStats;
182184

183185
private UnitOfWork currentUnitOfWork = null;
184186
/**
@@ -213,6 +215,7 @@ static UnitOfWorkType of(TransactionMode transactionMode) {
213215
this.readOnly = options.isReadOnly();
214216
this.autocommit = options.isAutocommit();
215217
this.queryOptions = this.queryOptions.toBuilder().mergeFrom(options.getQueryOptions()).build();
218+
this.returnCommitStats = options.isReturnCommitStats();
216219
this.ddlClient = createDdlClient();
217220
setDefaultTransactionOptions();
218221
}
@@ -237,6 +240,7 @@ static UnitOfWorkType of(TransactionMode transactionMode) {
237240
this.dbClient = dbClient;
238241
setReadOnly(options.isReadOnly());
239242
setAutocommit(options.isAutocommit());
243+
setReturnCommitStats(options.isReturnCommitStats());
240244
setDefaultTransactionOptions();
241245
}
242246

@@ -580,6 +584,31 @@ Timestamp getCommitTimestampOrNull() {
580584
: this.currentUnitOfWork.getCommitTimestampOrNull();
581585
}
582586

587+
@Override
588+
public CommitResponse getCommitResponse() {
589+
ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG);
590+
ConnectionPreconditions.checkState(
591+
this.currentUnitOfWork != null, "There is no transaction on this connection");
592+
return this.currentUnitOfWork.getCommitResponse();
593+
}
594+
595+
CommitResponse getCommitResponseOrNull() {
596+
ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG);
597+
return this.currentUnitOfWork == null ? null : this.currentUnitOfWork.getCommitResponseOrNull();
598+
}
599+
600+
@Override
601+
public void setReturnCommitStats(boolean returnCommitStats) {
602+
ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG);
603+
this.returnCommitStats = returnCommitStats;
604+
}
605+
606+
@Override
607+
public boolean isReturnCommitStats() {
608+
ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG);
609+
return this.returnCommitStats;
610+
}
611+
583612
/** Resets this connection to its default transaction options. */
584613
private void setDefaultTransactionOptions() {
585614
if (transactionStack.isEmpty()) {
@@ -954,6 +983,7 @@ private UnitOfWork createNewUnitOfWork() {
954983
.setReadOnly(isReadOnly())
955984
.setReadOnlyStaleness(readOnlyStaleness)
956985
.setAutocommitDmlMode(autocommitDmlMode)
986+
.setReturnCommitStats(returnCommitStats)
957987
.setStatementTimeout(statementTimeout)
958988
.withStatementExecutor(statementExecutor)
959989
.build();
@@ -970,6 +1000,7 @@ private UnitOfWork createNewUnitOfWork() {
9701000
return ReadWriteTransaction.newBuilder()
9711001
.setDatabaseClient(dbClient)
9721002
.setRetryAbortsInternally(retryAbortsInternally)
1003+
.setReturnCommitStats(returnCommitStats)
9731004
.setTransactionRetryListeners(transactionRetryListeners)
9741005
.setStatementTimeout(statementTimeout)
9751006
.withStatementExecutor(statementExecutor)

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

+15
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ public String[] getValidValues() {
155155
private static final String DEFAULT_NUM_CHANNELS = null;
156156
private static final String DEFAULT_USER_AGENT = null;
157157
private static final String DEFAULT_OPTIMIZER_VERSION = "";
158+
private static final boolean DEFAULT_RETURN_COMMIT_STATS = false;
158159
private static final boolean DEFAULT_LENIENT = false;
159160

160161
private static final String PLAIN_TEXT_PROTOCOL = "http:";
@@ -229,6 +230,7 @@ public String[] getValidValues() {
229230
ConnectionProperty.createStringProperty(
230231
OPTIMIZER_VERSION_PROPERTY_NAME,
231232
"Sets the default query optimizer version to use for this connection."),
233+
ConnectionProperty.createBooleanProperty("returnCommitStats", "", false),
232234
ConnectionProperty.createBooleanProperty(
233235
LENIENT_PROPERTY_NAME,
234236
"Silently ignore unknown properties in the connection string/properties (true/false)",
@@ -456,6 +458,7 @@ public static Builder newBuilder() {
456458
private final Integer maxSessions;
457459
private final String userAgent;
458460
private final QueryOptions queryOptions;
461+
private final boolean returnCommitStats;
459462

460463
private final boolean autocommit;
461464
private final boolean readOnly;
@@ -485,6 +488,7 @@ private ConnectionOptions(Builder builder) {
485488
QueryOptions.Builder queryOptionsBuilder = QueryOptions.newBuilder();
486489
queryOptionsBuilder.setOptimizerVersion(parseOptimizerVersion(this.uri));
487490
this.queryOptions = queryOptionsBuilder.build();
491+
this.returnCommitStats = parseReturnCommitStats(this.uri);
488492

489493
this.host =
490494
matcher.group(Builder.HOST_GROUP) == null
@@ -634,6 +638,12 @@ static String parseOptimizerVersion(String uri) {
634638
return value != null ? value : DEFAULT_OPTIMIZER_VERSION;
635639
}
636640

641+
@VisibleForTesting
642+
static boolean parseReturnCommitStats(String uri) {
643+
String value = parseUriProperty(uri, "returnCommitStats");
644+
return value != null ? Boolean.valueOf(value) : false;
645+
}
646+
637647
@VisibleForTesting
638648
static boolean parseLenient(String uri) {
639649
String value = parseUriProperty(uri, LENIENT_PROPERTY_NAME);
@@ -823,6 +833,11 @@ QueryOptions getQueryOptions() {
823833
return queryOptions;
824834
}
825835

836+
/** Whether connections created by this {@link ConnectionOptions} return commit stats. */
837+
public boolean isReturnCommitStats() {
838+
return returnCommitStats;
839+
}
840+
826841
/** Interceptors that should be executed after each statement */
827842
List<StatementExecutionInterceptor> getStatementExecutionInterceptors() {
828843
return statementExecutionInterceptors;

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

+6
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ interface ConnectionStatementExecutor {
5656

5757
StatementResult statementShowCommitTimestamp();
5858

59+
StatementResult statementShowCommitResponse();
60+
5961
StatementResult statementSetReadOnlyStaleness(TimestampBound staleness);
6062

6163
StatementResult statementShowReadOnlyStaleness();
@@ -64,6 +66,10 @@ interface ConnectionStatementExecutor {
6466

6567
StatementResult statementShowOptimizerVersion();
6668

69+
StatementResult statementSetReturnCommitStats(Boolean returnCommitStats);
70+
71+
StatementResult statementShowReturnCommitStats();
72+
6773
StatementResult statementBeginTransaction();
6874

6975
StatementResult statementCommit();

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

+45
Original file line numberDiff line numberDiff line change
@@ -27,26 +27,37 @@
2727
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_READONLY;
2828
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_READ_ONLY_STALENESS;
2929
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_RETRY_ABORTS_INTERNALLY;
30+
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_RETURN_COMMIT_STATS;
3031
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_STATEMENT_TIMEOUT;
3132
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_TRANSACTION_MODE;
3233
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_AUTOCOMMIT;
3334
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_AUTOCOMMIT_DML_MODE;
35+
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_COMMIT_RESPONSE;
3436
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_COMMIT_TIMESTAMP;
3537
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_OPTIMIZER_VERSION;
3638
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_READONLY;
3739
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_READ_ONLY_STALENESS;
3840
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_READ_TIMESTAMP;
3941
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_RETRY_ABORTS_INTERNALLY;
42+
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_RETURN_COMMIT_STATS;
4043
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_STATEMENT_TIMEOUT;
4144
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.START_BATCH_DDL;
4245
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.START_BATCH_DML;
4346
import static com.google.cloud.spanner.connection.StatementResultImpl.noResult;
4447
import static com.google.cloud.spanner.connection.StatementResultImpl.resultSet;
4548

49+
import com.google.cloud.spanner.CommitResponse;
50+
import com.google.cloud.spanner.CommitStats;
51+
import com.google.cloud.spanner.ResultSet;
52+
import com.google.cloud.spanner.ResultSets;
53+
import com.google.cloud.spanner.Struct;
4654
import com.google.cloud.spanner.TimestampBound;
55+
import com.google.cloud.spanner.Type;
56+
import com.google.cloud.spanner.Type.StructField;
4757
import com.google.cloud.spanner.connection.ReadOnlyStalenessUtil.DurationValueGetter;
4858
import com.google.common.base.Preconditions;
4959
import com.google.protobuf.Duration;
60+
import java.util.Arrays;
5061
import java.util.concurrent.TimeUnit;
5162

5263
/**
@@ -170,6 +181,28 @@ public StatementResult statementShowCommitTimestamp() {
170181
"COMMIT_TIMESTAMP", getConnection().getCommitTimestampOrNull(), SHOW_COMMIT_TIMESTAMP);
171182
}
172183

184+
@Override
185+
public StatementResult statementShowCommitResponse() {
186+
CommitResponse response = getConnection().getCommitResponseOrNull();
187+
CommitStats stats = null;
188+
if (response != null && response.hasCommitStats()) {
189+
stats = response.getCommitStats();
190+
}
191+
ResultSet resultSet =
192+
ResultSets.forRows(
193+
Type.struct(
194+
StructField.of("COMMIT_TIMESTAMP", Type.timestamp()),
195+
StructField.of("MUTATION_COUNT", Type.int64())),
196+
Arrays.asList(
197+
Struct.newBuilder()
198+
.set("COMMIT_TIMESTAMP")
199+
.to(response == null ? null : response.getCommitTimestamp())
200+
.set("MUTATION_COUNT")
201+
.to(stats == null ? null : stats.getMutationCount())
202+
.build()));
203+
return StatementResultImpl.of(resultSet, SHOW_COMMIT_RESPONSE);
204+
}
205+
173206
@Override
174207
public StatementResult statementSetReadOnlyStaleness(TimestampBound staleness) {
175208
getConnection().setReadOnlyStaleness(staleness);
@@ -197,6 +230,18 @@ public StatementResult statementShowOptimizerVersion() {
197230
"OPTIMIZER_VERSION", getConnection().getOptimizerVersion(), SHOW_OPTIMIZER_VERSION);
198231
}
199232

233+
@Override
234+
public StatementResult statementSetReturnCommitStats(Boolean returnCommitStats) {
235+
getConnection().setReturnCommitStats(returnCommitStats);
236+
return noResult(SET_RETURN_COMMIT_STATS);
237+
}
238+
239+
@Override
240+
public StatementResult statementShowReturnCommitStats() {
241+
return resultSet(
242+
"RETURN_COMMIT_STATS", getConnection().isReturnCommitStats(), SHOW_RETURN_COMMIT_STATS);
243+
}
244+
200245
@Override
201246
public StatementResult statementBeginTransaction() {
202247
getConnection().beginTransaction();

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

+12
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.google.api.core.ApiFutures;
2121
import com.google.api.gax.longrunning.OperationFuture;
2222
import com.google.cloud.Timestamp;
23+
import com.google.cloud.spanner.CommitResponse;
2324
import com.google.cloud.spanner.DatabaseClient;
2425
import com.google.cloud.spanner.ErrorCode;
2526
import com.google.cloud.spanner.Mutation;
@@ -168,6 +169,17 @@ public Timestamp getCommitTimestampOrNull() {
168169
return null;
169170
}
170171

172+
@Override
173+
public CommitResponse getCommitResponse() {
174+
throw SpannerExceptionFactory.newSpannerException(
175+
ErrorCode.FAILED_PRECONDITION, "There is no commit response available for DDL batches.");
176+
}
177+
178+
@Override
179+
public CommitResponse getCommitResponseOrNull() {
180+
return null;
181+
}
182+
171183
@Override
172184
public ApiFuture<Void> executeDdlAsync(ParsedStatement ddl) {
173185
ConnectionPreconditions.checkState(

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

+12
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import com.google.api.core.ApiFutures;
2222
import com.google.api.core.SettableApiFuture;
2323
import com.google.cloud.Timestamp;
24+
import com.google.cloud.spanner.CommitResponse;
2425
import com.google.cloud.spanner.ErrorCode;
2526
import com.google.cloud.spanner.Mutation;
2627
import com.google.cloud.spanner.Options.QueryOption;
@@ -119,6 +120,17 @@ public Timestamp getCommitTimestampOrNull() {
119120
return null;
120121
}
121122

123+
@Override
124+
public CommitResponse getCommitResponse() {
125+
throw SpannerExceptionFactory.newSpannerException(
126+
ErrorCode.FAILED_PRECONDITION, "There is no commit response available for DML batches.");
127+
}
128+
129+
@Override
130+
public CommitResponse getCommitResponseOrNull() {
131+
return null;
132+
}
133+
122134
@Override
123135
public ApiFuture<Void> executeDdlAsync(ParsedStatement ddl) {
124136
throw SpannerExceptionFactory.newSpannerException(

0 commit comments

Comments
 (0)