Skip to content

Commit 44aa384

Browse files
olavloiteskuruppu
andauthored
feat!: add support for CommitStats (#544)
* feat!: add support for CommitStats Adds support for returning CommitStats from read/write transactions. * fix: add clirr ignored differences * fix: error message should start with getCommitResponse Co-authored-by: skuruppu <skuruppu@google.com> * fix: remove overload delay * chore: cleanup after merge * fix: update copyright years of new files * test: fix flaky test * test: skip commit stats tests on emulator * test: missed one commit stats tests against emulator * test: skip another emulator test * test: add missing test cases * fix: address review comments * chore: use junit assertion instead of truth * chore: replace truth asserts with junit asserts * chore: replace truth assertions with junit * chore: cleanup test and variable names * fix: rename test method and variables * fix: address review comments Co-authored-by: skuruppu <skuruppu@google.com>
1 parent 5e07e4e commit 44aa384

37 files changed

+1111
-153
lines changed

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

+23
Original file line numberDiff line numberDiff line change
@@ -453,4 +453,27 @@
453453
<className>com/google/cloud/spanner/TransactionContext</className>
454454
<method>com.google.api.core.ApiFuture executeUpdateAsync(com.google.cloud.spanner.Statement)</method>
455455
</difference>
456+
457+
<!-- Support for CommitStats added -->
458+
<difference>
459+
<differenceType>7012</differenceType>
460+
<className>com/google/cloud/spanner/AsyncTransactionManager</className>
461+
<method>com.google.api.core.ApiFuture getCommitResponse()</method>
462+
</difference>
463+
<difference>
464+
<differenceType>7012</differenceType>
465+
<className>com/google/cloud/spanner/AsyncRunner</className>
466+
<method>com.google.api.core.ApiFuture getCommitResponse()</method>
467+
</difference>
468+
<difference>
469+
<differenceType>7012</differenceType>
470+
<className>com/google/cloud/spanner/TransactionManager</className>
471+
<method>com.google.cloud.spanner.CommitResponse getCommitResponse()</method>
472+
</difference>
473+
<difference>
474+
<differenceType>7012</differenceType>
475+
<className>com/google/cloud/spanner/TransactionRunner</className>
476+
<method>com.google.cloud.spanner.CommitResponse getCommitResponse()</method>
477+
</difference>
478+
456479
</differences>

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

+6
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,10 @@ interface AsyncWork<R> {
5656
* {@link ExecutionException} if the transaction did not commit.
5757
*/
5858
ApiFuture<Timestamp> getCommitTimestamp();
59+
60+
/**
61+
* Returns the {@link CommitResponse} of this transaction. {@link ApiFuture#get()} throws an
62+
* {@link ExecutionException} if the transaction did not commit.
63+
*/
64+
ApiFuture<CommitResponse> getCommitResponse();
5965
}

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

+29-7
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,31 @@
1616

1717
package com.google.cloud.spanner;
1818

19+
import static com.google.common.base.Preconditions.checkState;
20+
21+
import com.google.api.core.ApiFunction;
1922
import com.google.api.core.ApiFuture;
23+
import com.google.api.core.ApiFutures;
2024
import com.google.api.core.SettableApiFuture;
2125
import com.google.cloud.Timestamp;
2226
import com.google.cloud.spanner.TransactionRunner.TransactionCallable;
27+
import com.google.common.base.Preconditions;
28+
import com.google.common.util.concurrent.MoreExecutors;
2329
import java.util.concurrent.ExecutionException;
2430
import java.util.concurrent.Executor;
2531

2632
class AsyncRunnerImpl implements AsyncRunner {
2733
private final TransactionRunnerImpl delegate;
28-
private final SettableApiFuture<Timestamp> commitTimestamp = SettableApiFuture.create();
34+
private SettableApiFuture<CommitResponse> commitResponse;
2935

3036
AsyncRunnerImpl(TransactionRunnerImpl delegate) {
31-
this.delegate = delegate;
37+
this.delegate = Preconditions.checkNotNull(delegate);
3238
}
3339

3440
@Override
3541
public <R> ApiFuture<R> runAsync(final AsyncWork<R> work, Executor executor) {
42+
Preconditions.checkState(commitResponse == null, "runAsync() can only be called once");
43+
commitResponse = SettableApiFuture.create();
3644
final SettableApiFuture<R> res = SettableApiFuture.create();
3745
executor.execute(
3846
new Runnable() {
@@ -43,7 +51,7 @@ public void run() {
4351
} catch (Throwable t) {
4452
res.setException(t);
4553
} finally {
46-
setCommitTimestamp();
54+
setCommitResponse();
4755
}
4856
}
4957
});
@@ -66,16 +74,30 @@ public R run(TransactionContext transaction) throws Exception {
6674
});
6775
}
6876

69-
private void setCommitTimestamp() {
77+
private void setCommitResponse() {
7078
try {
71-
commitTimestamp.set(delegate.getCommitTimestamp());
79+
commitResponse.set(delegate.getCommitResponse());
7280
} catch (Throwable t) {
73-
commitTimestamp.setException(t);
81+
commitResponse.setException(t);
7482
}
7583
}
7684

7785
@Override
7886
public ApiFuture<Timestamp> getCommitTimestamp() {
79-
return commitTimestamp;
87+
checkState(commitResponse != null, "runAsync() has not yet been called");
88+
return ApiFutures.transform(
89+
commitResponse,
90+
new ApiFunction<CommitResponse, Timestamp>() {
91+
@Override
92+
public Timestamp apply(CommitResponse input) {
93+
return input.getCommitTimestamp();
94+
}
95+
},
96+
MoreExecutors.directExecutor());
97+
}
98+
99+
public ApiFuture<CommitResponse> getCommitResponse() {
100+
checkState(commitResponse != null, "runAsync() has not yet been called");
101+
return commitResponse;
80102
}
81103
}

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

+3
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,9 @@ public interface AsyncTransactionFunction<I, O> {
191191
/** Returns the state of the transaction. */
192192
TransactionState getState();
193193

194+
/** Returns the {@link CommitResponse} of this transaction. */
195+
ApiFuture<CommitResponse> getCommitResponse();
196+
194197
/**
195198
* Closes the manager. If there is an active transaction, it will be rolled back. Underlying
196199
* session will be released back to the session pool.

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

+22-8
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.google.cloud.spanner;
1818

1919
import com.google.api.core.ApiAsyncFunction;
20+
import com.google.api.core.ApiFunction;
2021
import com.google.api.core.ApiFuture;
2122
import com.google.api.core.ApiFutureCallback;
2223
import com.google.api.core.ApiFutures;
@@ -45,7 +46,7 @@ final class AsyncTransactionManagerImpl
4546

4647
private TransactionRunnerImpl.TransactionContextImpl txn;
4748
private TransactionState txnState;
48-
private final SettableApiFuture<Timestamp> commitTimestamp = SettableApiFuture.create();
49+
private final SettableApiFuture<CommitResponse> commitResponse = SettableApiFuture.create();
4950

5051
AsyncTransactionManagerImpl(SessionImpl session, Span span, TransactionOption... options) {
5152
this.session = session;
@@ -132,29 +133,37 @@ public ApiFuture<Timestamp> commitAsync() {
132133
SpannerExceptionFactory.newSpannerException(
133134
ErrorCode.ABORTED, "Transaction already aborted"));
134135
}
135-
ApiFuture<Timestamp> res = txn.commitAsync();
136+
ApiFuture<CommitResponse> commitResponseFuture = txn.commitAsync();
136137
txnState = TransactionState.COMMITTED;
137138

138139
ApiFutures.addCallback(
139-
res,
140-
new ApiFutureCallback<Timestamp>() {
140+
commitResponseFuture,
141+
new ApiFutureCallback<CommitResponse>() {
141142
@Override
142143
public void onFailure(Throwable t) {
143144
if (t instanceof AbortedException) {
144145
txnState = TransactionState.ABORTED;
145146
} else {
146147
txnState = TransactionState.COMMIT_FAILED;
147-
commitTimestamp.setException(t);
148+
commitResponse.setException(t);
148149
}
149150
}
150151

151152
@Override
152-
public void onSuccess(Timestamp result) {
153-
commitTimestamp.set(result);
153+
public void onSuccess(CommitResponse result) {
154+
commitResponse.set(result);
155+
}
156+
},
157+
MoreExecutors.directExecutor());
158+
return ApiFutures.transform(
159+
commitResponseFuture,
160+
new ApiFunction<CommitResponse, Timestamp>() {
161+
@Override
162+
public Timestamp apply(CommitResponse input) {
163+
return input.getCommitTimestamp();
154164
}
155165
},
156166
MoreExecutors.directExecutor());
157-
return res;
158167
}
159168

160169
@Override
@@ -187,6 +196,11 @@ public TransactionState getState() {
187196
return txnState;
188197
}
189198

199+
@Override
200+
public ApiFuture<CommitResponse> getCommitResponse() {
201+
return commitResponse;
202+
}
203+
190204
@Override
191205
public void invalidate() {
192206
if (txnState == TransactionState.STARTED || txnState == null) {

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

+24-6
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,38 @@
1717
package com.google.cloud.spanner;
1818

1919
import com.google.cloud.Timestamp;
20+
import com.google.common.base.Preconditions;
2021
import java.util.Objects;
2122

2223
/** Represents a response from a commit operation. */
2324
public class CommitResponse {
2425

25-
private final Timestamp commitTimestamp;
26+
private final com.google.spanner.v1.CommitResponse proto;
2627

2728
public CommitResponse(Timestamp commitTimestamp) {
28-
this.commitTimestamp = commitTimestamp;
29+
this.proto =
30+
com.google.spanner.v1.CommitResponse.newBuilder()
31+
.setCommitTimestamp(commitTimestamp.toProto())
32+
.build();
2933
}
3034

31-
/** Returns a {@link Timestamp} representing the commit time of the write operation. */
35+
CommitResponse(com.google.spanner.v1.CommitResponse proto) {
36+
this.proto = Preconditions.checkNotNull(proto);
37+
}
38+
39+
/** Returns a {@link Timestamp} representing the commit time of the transaction. */
3240
public Timestamp getCommitTimestamp() {
33-
return commitTimestamp;
41+
return Timestamp.fromProto(proto.getCommitTimestamp());
42+
}
43+
44+
/**
45+
* Commit statistics are returned by a read/write transaction if specifically requested by passing
46+
* in {@link Options#commitStats()} to the transaction.
47+
*/
48+
public CommitStats getCommitStats() {
49+
Preconditions.checkState(
50+
proto.hasCommitStats(), "The CommitResponse does not contain any commit statistics.");
51+
return CommitStats.fromProto(proto.getCommitStats());
3452
}
3553

3654
@Override
@@ -42,11 +60,11 @@ public boolean equals(Object o) {
4260
return false;
4361
}
4462
CommitResponse that = (CommitResponse) o;
45-
return Objects.equals(commitTimestamp, that.commitTimestamp);
63+
return Objects.equals(proto, that.proto);
4664
}
4765

4866
@Override
4967
public int hashCode() {
50-
return Objects.hash(commitTimestamp);
68+
return Objects.hash(proto);
5169
}
5270
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright 2021 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.spanner;
18+
19+
import com.google.common.base.Preconditions;
20+
21+
/**
22+
* Commit statistics are returned by a read/write transaction if specifically requested by passing
23+
* in {@link Options#commitStats()} to the transaction.
24+
*/
25+
public class CommitStats {
26+
private final long mutationCount;
27+
28+
private CommitStats(long mutationCount) {
29+
this.mutationCount = mutationCount;
30+
}
31+
32+
static CommitStats fromProto(com.google.spanner.v1.CommitResponse.CommitStats proto) {
33+
Preconditions.checkNotNull(proto);
34+
return new CommitStats(proto.getMutationCount());
35+
}
36+
37+
/**
38+
* The number of mutations that were executed by the transaction. Insert and update operations
39+
* count with the multiplicity of the number of columns they affect. For example, inserting a new
40+
* record may count as five mutations, if values are inserted into five columns. Delete and delete
41+
* range operations count as one mutation regardless of the number of columns affected. Deleting a
42+
* row from a parent table that has the ON DELETE CASCADE annotation is also counted as one
43+
* mutation regardless of the number of interleaved child rows present. The exception to this is
44+
* if there are secondary indexes defined on rows being deleted, then the changes to the secondary
45+
* indexes are counted individually. For example, if a table has 2 secondary indexes, deleting a
46+
* range of rows in the table counts as 1 mutation for the table, plus 2 mutations for each row
47+
* that is deleted because the rows in the secondary index might be scattered over the key-space,
48+
* making it impossible for Cloud Spanner to call a single delete range operation on the secondary
49+
* indexes. Secondary indexes include the foreign keys backing indexes.
50+
*/
51+
public long getMutationCount() {
52+
return mutationCount;
53+
}
54+
}

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

+28-1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ public interface UpdateOption {}
4646
/** Marker interface to mark options applicable to list operations in admin API. */
4747
public interface ListOption {}
4848

49+
/** Specifying this instructs the transaction to request {@link CommitStats} from the backend. */
50+
public static TransactionOption commitStats() {
51+
return COMMIT_STATS_OPTION;
52+
}
53+
4954
/**
5055
* Specifying this will cause the read to yield at most this many rows. This should be greater
5156
* than 0.
@@ -116,6 +121,16 @@ public static ListOption filter(String filter) {
116121
return new FilterOption(filter);
117122
}
118123

124+
/** Option to request {@link CommitStats} for read/write transactions. */
125+
static final class CommitStatsOption extends InternalOption implements TransactionOption {
126+
@Override
127+
void appendToOptions(Options options) {
128+
options.withCommitStats = true;
129+
}
130+
}
131+
132+
static final CommitStatsOption COMMIT_STATS_OPTION = new CommitStatsOption();
133+
119134
/** Option pertaining to flow control. */
120135
static final class FlowControlOption extends InternalOption implements ReadAndQueryOption {
121136
final int prefetchChunks;
@@ -143,6 +158,7 @@ void appendToOptions(Options options) {
143158
}
144159
}
145160

161+
private boolean withCommitStats;
146162
private Long limit;
147163
private Integer prefetchChunks;
148164
private Integer bufferRows;
@@ -153,6 +169,10 @@ void appendToOptions(Options options) {
153169
// Construction is via factory methods below.
154170
private Options() {}
155171

172+
boolean withCommitStats() {
173+
return withCommitStats;
174+
}
175+
156176
boolean hasLimit() {
157177
return limit != null;
158178
}
@@ -204,6 +224,9 @@ String filter() {
204224
@Override
205225
public String toString() {
206226
StringBuilder b = new StringBuilder();
227+
if (withCommitStats) {
228+
b.append("withCommitStats: ").append(withCommitStats).append(' ');
229+
}
207230
if (limit != null) {
208231
b.append("limit: ").append(limit).append(' ');
209232
}
@@ -234,7 +257,8 @@ public boolean equals(Object o) {
234257
}
235258

236259
Options that = (Options) o;
237-
return (!hasLimit() && !that.hasLimit()
260+
return Objects.equals(withCommitStats, that.withCommitStats)
261+
&& (!hasLimit() && !that.hasLimit()
238262
|| hasLimit() && that.hasLimit() && Objects.equals(limit(), that.limit()))
239263
&& (!hasPrefetchChunks() && !that.hasPrefetchChunks()
240264
|| hasPrefetchChunks()
@@ -253,6 +277,9 @@ public boolean equals(Object o) {
253277
@Override
254278
public int hashCode() {
255279
int result = 31;
280+
if (withCommitStats) {
281+
result = 31 * result + 1231;
282+
}
256283
if (limit != null) {
257284
result = 31 * result + limit.hashCode();
258285
}

0 commit comments

Comments
 (0)