Skip to content

Commit 27c064a

Browse files
rajatbhattaarpan14
authored andcommitted
feat: add support for BatchWriteAtLeastOnce (googleapis#2520)
* feat: add support for BatchWriteAtleastOnce * test: add batchwrite() support to MockSpannerServiceImpl * test: add commit timestamp to proto * test: add commit timestamp to proto * test: add commit timestamp to proto * consume the stream in tests * refactor tests * refactor tests * test if mutations are correctly applied * null check * skip for emulator * add method documentation * add method documentation * add method documentation * remove autogenerated code * remove autogenerated tests * batchWriteAtleastOnce -> batchWriteAtLeastOnce * batchWriteAtleastOnceWithOptions -> batchWriteAtLeastOnceWithOptions * changes based on updated batch write API * add copyright and doc * address review comments * address review comments * add more documentation --------- Co-authored-by: Arpan Mishra <[email protected]>
1 parent a1767b4 commit 27c064a

File tree

12 files changed

+589
-58
lines changed

12 files changed

+589
-58
lines changed

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

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@
1616

1717
package com.google.cloud.spanner;
1818

19+
import com.google.api.gax.rpc.ServerStream;
1920
import com.google.cloud.Timestamp;
2021
import com.google.cloud.spanner.Options.RpcPriority;
2122
import com.google.cloud.spanner.Options.TransactionOption;
2223
import com.google.cloud.spanner.Options.UpdateOption;
24+
import com.google.spanner.v1.BatchWriteResponse;
2325

2426
/**
2527
* Interface for all the APIs that are used to read/write data into a Cloud Spanner database. An
@@ -191,6 +193,56 @@ CommitResponse writeWithOptions(Iterable<Mutation> mutations, TransactionOption.
191193
CommitResponse writeAtLeastOnceWithOptions(
192194
Iterable<Mutation> mutations, TransactionOption... options) throws SpannerException;
193195

196+
/**
197+
* Applies batch of mutation groups in a collection of efficient transactions. The mutation groups
198+
* are applied non-atomically in an unspecified order and thus, they must be independent of each
199+
* other. Partial failure is possible, i.e., some mutation groups may have been applied
200+
* successfully, while some may have failed. The results of individual batches are streamed into
201+
* the response as and when the batches are applied.
202+
*
203+
* <p>One BatchWriteResponse can contain the results for multiple MutationGroups. Inspect the
204+
* indexes field to determine the MutationGroups that the BatchWriteResponse is for.
205+
*
206+
* <p>The mutation groups may be applied more than once. This can lead to failures if the mutation
207+
* groups are non-idempotent. For example, an insert that is replayed can return an {@link
208+
* ErrorCode#ALREADY_EXISTS} error. For this reason, users of the library may prefer to use {@link
209+
* #write(Iterable)} instead. However, {@code batchWriteAtLeastOnce()} method may be appropriate
210+
* for non-atomically committing multiple mutation groups in a single RPC with low latency.
211+
*
212+
* <p>Example of BatchWriteAtLeastOnce
213+
*
214+
* <pre>{@code
215+
* Iterable<MutationGroup> mutationGroups =
216+
* ImmutableList.of(
217+
* MutationGroup.of(
218+
* Mutation.newInsertBuilder("FOO1").set("ID").to(1L).set("NAME").to("Bar1").build(),
219+
* Mutation.newInsertBuilder("FOO2").set("ID").to(2L).set("NAME").to("Bar2").build()),
220+
* MutationGroup.of(
221+
* Mutation.newInsertBuilder("FOO3").set("ID").to(3L).set("NAME").to("Bar3").build(),
222+
* Mutation.newInsertBuilder("FOO4").set("ID").to(4L).set("NAME").to("Bar4").build()),
223+
* MutationGroup.of(
224+
* Mutation.newInsertBuilder("FOO4").set("ID").to(4L).set("NAME").to("Bar4").build(),
225+
* Mutation.newInsertBuilder("FOO5").set("ID").to(5L).set("NAME").to("Bar5").build()),
226+
* MutationGroup.of(
227+
* Mutation.newInsertBuilder("FOO6").set("ID").to(6L).set("NAME").to("Bar6").build()));
228+
* ServerStream<BatchWriteResponse> responses =
229+
* dbClient.batchWriteAtLeastOnce(mutationGroups, Options.tag("batch-write-tag"));
230+
* for (BatchWriteResponse response : responses) {
231+
* // Do something when a response is received.
232+
* }
233+
* }</pre>
234+
*
235+
* Options for a transaction can include:
236+
*
237+
* <ul>
238+
* <li>{@link Options#priority(com.google.cloud.spanner.Options.RpcPriority)}: The {@link
239+
* RpcPriority} to use for the batch write request.
240+
* <li>{@link Options#tag(String)}: The transaction tag to use for the batch write request.
241+
* </ul>
242+
*/
243+
ServerStream<BatchWriteResponse> batchWriteAtLeastOnce(
244+
Iterable<MutationGroup> mutationGroups, TransactionOption... options) throws SpannerException;
245+
194246
/**
195247
* Returns a context in which a single read can be performed using {@link TimestampBound#strong()}
196248
* concurrency. This method will return a {@link ReadContext} that will not return the read

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package com.google.cloud.spanner;
1818

19+
import com.google.api.gax.rpc.ServerStream;
1920
import com.google.cloud.Timestamp;
2021
import com.google.cloud.spanner.Options.TransactionOption;
2122
import com.google.cloud.spanner.Options.UpdateOption;
@@ -27,6 +28,10 @@
2728
import com.google.common.annotations.VisibleForTesting;
2829
import com.google.common.base.Function;
2930
import com.google.common.util.concurrent.ListenableFuture;
31+
import com.google.spanner.v1.BatchWriteResponse;
32+
import io.opencensus.common.Scope;
33+
import io.opencensus.trace.Span;
34+
import io.opencensus.trace.Tracer;
3035
import io.opencensus.trace.Tracing;
3136
import javax.annotation.Nullable;
3237

@@ -105,6 +110,21 @@ public CommitResponse writeAtLeastOnceWithOptions(
105110
}
106111
}
107112

113+
@Override
114+
public ServerStream<BatchWriteResponse> batchWriteAtLeastOnce(
115+
final Iterable<MutationGroup> mutationGroups, final TransactionOption... options)
116+
throws SpannerException {
117+
ISpan span = tracer.spanBuilder(READ_WRITE_TRANSACTION);
118+
try (IScope s = tracer.withSpan(span)) {
119+
return runWithSessionRetry(session -> session.batchWriteAtLeastOnce(mutationGroups, options));
120+
} catch (RuntimeException e) {
121+
span.setStatus(e);
122+
throw e;
123+
} finally {
124+
span.end();
125+
}
126+
}
127+
108128
@Override
109129
public ReadContext singleUse() {
110130
ISpan span = tracer.spanBuilder(READ_ONLY_TRANSACTION);
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright 2023 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+
import com.google.common.collect.ImmutableList;
21+
import com.google.spanner.v1.BatchWriteRequest;
22+
import java.util.ArrayList;
23+
import java.util.List;
24+
25+
/** Represents a group of Cloud Spanner mutations to be committed together. */
26+
public class MutationGroup {
27+
private final ImmutableList<Mutation> mutations;
28+
29+
private MutationGroup(ImmutableList<Mutation> mutations) {
30+
this.mutations = mutations;
31+
}
32+
33+
/** Creates a {@code MutationGroup} given a vararg of mutations. */
34+
public static MutationGroup of(Mutation... mutations) {
35+
Preconditions.checkArgument(mutations.length > 0, "Should pass in at least one mutation.");
36+
return new MutationGroup(ImmutableList.copyOf(mutations));
37+
}
38+
39+
/** Creates a {@code MutationGroup} given an iterable of mutations. */
40+
public static MutationGroup of(Iterable<Mutation> mutations) {
41+
return new MutationGroup(ImmutableList.copyOf(mutations));
42+
}
43+
44+
/** Returns corresponding mutations for this MutationGroup. */
45+
public ImmutableList<Mutation> getMutations() {
46+
return mutations;
47+
}
48+
49+
static BatchWriteRequest.MutationGroup toProto(final MutationGroup mutationGroup) {
50+
List<com.google.spanner.v1.Mutation> mutationsProto = new ArrayList<>();
51+
Mutation.toProto(mutationGroup.getMutations(), mutationsProto);
52+
return BatchWriteRequest.MutationGroup.newBuilder().addAllMutations(mutationsProto).build();
53+
}
54+
55+
static List<BatchWriteRequest.MutationGroup> toListProto(
56+
final Iterable<MutationGroup> mutationGroups) {
57+
List<BatchWriteRequest.MutationGroup> mutationGroupsProto = new ArrayList<>();
58+
for (MutationGroup group : mutationGroups) {
59+
mutationGroupsProto.add(toProto(group));
60+
}
61+
return mutationGroupsProto;
62+
}
63+
}

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

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import com.google.api.core.ApiFuture;
2323
import com.google.api.core.SettableApiFuture;
24+
import com.google.api.gax.rpc.ServerStream;
2425
import com.google.cloud.Timestamp;
2526
import com.google.cloud.spanner.AbstractReadContext.MultiUseReadOnlyTransaction;
2627
import com.google.cloud.spanner.AbstractReadContext.SingleReadContext;
@@ -38,6 +39,8 @@
3839
import com.google.common.util.concurrent.MoreExecutors;
3940
import com.google.protobuf.ByteString;
4041
import com.google.protobuf.Empty;
42+
import com.google.spanner.v1.BatchWriteRequest;
43+
import com.google.spanner.v1.BatchWriteResponse;
4144
import com.google.spanner.v1.BeginTransactionRequest;
4245
import com.google.spanner.v1.CommitRequest;
4346
import com.google.spanner.v1.RequestOptions;
@@ -163,7 +166,6 @@ public CommitResponse writeAtLeastOnceWithOptions(
163166
Iterable<Mutation> mutations, TransactionOption... transactionOptions)
164167
throws SpannerException {
165168
setActive(null);
166-
Options commitRequestOptions = Options.fromTransactionOptions(transactionOptions);
167169
List<com.google.spanner.v1.Mutation> mutationsProto = new ArrayList<>();
168170
Mutation.toProto(mutations, mutationsProto);
169171
final CommitRequest.Builder requestBuilder =
@@ -175,15 +177,9 @@ public CommitResponse writeAtLeastOnceWithOptions(
175177
.setSingleUseTransaction(
176178
TransactionOptions.newBuilder()
177179
.setReadWrite(TransactionOptions.ReadWrite.getDefaultInstance()));
178-
if (commitRequestOptions.hasPriority() || commitRequestOptions.hasTag()) {
179-
RequestOptions.Builder requestOptionsBuilder = RequestOptions.newBuilder();
180-
if (commitRequestOptions.hasPriority()) {
181-
requestOptionsBuilder.setPriority(commitRequestOptions.priority());
182-
}
183-
if (commitRequestOptions.hasTag()) {
184-
requestOptionsBuilder.setTransactionTag(commitRequestOptions.tag());
185-
}
186-
requestBuilder.setRequestOptions(requestOptionsBuilder.build());
180+
RequestOptions commitRequestOptions = getRequestOptions(transactionOptions);
181+
if (commitRequestOptions != null) {
182+
requestBuilder.setRequestOptions(commitRequestOptions);
187183
}
188184
CommitRequest request = requestBuilder.build();
189185
ISpan span = tracer.spanBuilder(SpannerImpl.COMMIT);
@@ -198,6 +194,45 @@ public CommitResponse writeAtLeastOnceWithOptions(
198194
}
199195
}
200196

197+
private RequestOptions getRequestOptions(TransactionOption... transactionOptions) {
198+
Options requestOptions = Options.fromTransactionOptions(transactionOptions);
199+
if (requestOptions.hasPriority() || requestOptions.hasTag()) {
200+
RequestOptions.Builder requestOptionsBuilder = RequestOptions.newBuilder();
201+
if (requestOptions.hasPriority()) {
202+
requestOptionsBuilder.setPriority(requestOptions.priority());
203+
}
204+
if (requestOptions.hasTag()) {
205+
requestOptionsBuilder.setTransactionTag(requestOptions.tag());
206+
}
207+
return requestOptionsBuilder.build();
208+
}
209+
return null;
210+
}
211+
212+
@Override
213+
public ServerStream<BatchWriteResponse> batchWriteAtLeastOnce(
214+
Iterable<MutationGroup> mutationGroups, TransactionOption... transactionOptions)
215+
throws SpannerException {
216+
setActive(null);
217+
List<BatchWriteRequest.MutationGroup> mutationGroupsProto =
218+
MutationGroup.toListProto(mutationGroups);
219+
final BatchWriteRequest.Builder requestBuilder =
220+
BatchWriteRequest.newBuilder().setSession(name).addAllMutationGroups(mutationGroupsProto);
221+
RequestOptions batchWriteRequestOptions = getRequestOptions(transactionOptions);
222+
if (batchWriteRequestOptions != null) {
223+
requestBuilder.setRequestOptions(batchWriteRequestOptions);
224+
}
225+
Span span = tracer.spanBuilder(SpannerImpl.BATCH_WRITE).startSpan();
226+
try (Scope s = tracer.withSpan(span)) {
227+
return spanner.getRpc().batchWriteAtLeastOnce(requestBuilder.build(), this.options);
228+
} catch (Throwable e) {
229+
TraceUtil.setWithFailure(span, e);
230+
throw SpannerExceptionFactory.newSpannerException(e);
231+
} finally {
232+
span.end(TraceUtil.END_SPAN_OPTIONS);
233+
}
234+
}
235+
201236
@Override
202237
public ReadContext singleUse() {
203238
return singleUse(TimestampBound.strong());

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
import com.google.api.core.ApiFutures;
5353
import com.google.api.core.SettableApiFuture;
5454
import com.google.api.gax.core.ExecutorProvider;
55+
import com.google.api.gax.rpc.ServerStream;
5556
import com.google.cloud.Timestamp;
5657
import com.google.cloud.grpc.GrpcTransportOptions;
5758
import com.google.cloud.grpc.GrpcTransportOptions.ExecutorFactory;
@@ -82,6 +83,7 @@
8283
import com.google.common.util.concurrent.MoreExecutors;
8384
import com.google.common.util.concurrent.SettableFuture;
8485
import com.google.protobuf.Empty;
86+
import com.google.spanner.v1.BatchWriteResponse;
8587
import com.google.spanner.v1.ResultSetStats;
8688
import io.opencensus.metrics.DerivedLongCumulative;
8789
import io.opencensus.metrics.DerivedLongGauge;
@@ -1185,6 +1187,17 @@ public CommitResponse writeAtLeastOnceWithOptions(
11851187
}
11861188
}
11871189

1190+
@Override
1191+
public ServerStream<BatchWriteResponse> batchWriteAtLeastOnce(
1192+
Iterable<MutationGroup> mutationGroups, TransactionOption... options)
1193+
throws SpannerException {
1194+
try {
1195+
return get().batchWriteAtLeastOnce(mutationGroups, options);
1196+
} finally {
1197+
close();
1198+
}
1199+
}
1200+
11881201
@Override
11891202
public ReadContext singleUse() {
11901203
try {
@@ -1478,6 +1491,18 @@ public CommitResponse writeAtLeastOnceWithOptions(
14781491
}
14791492
}
14801493

1494+
@Override
1495+
public ServerStream<BatchWriteResponse> batchWriteAtLeastOnce(
1496+
Iterable<MutationGroup> mutationGroups, TransactionOption... options)
1497+
throws SpannerException {
1498+
try {
1499+
markUsed();
1500+
return delegate.batchWriteAtLeastOnce(mutationGroups, options);
1501+
} catch (SpannerException e) {
1502+
throw lastException = e;
1503+
}
1504+
}
1505+
14811506
@Override
14821507
public long executePartitionedUpdate(Statement stmt, UpdateOption... options)
14831508
throws SpannerException {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ class SpannerImpl extends BaseService<SpannerOptions> implements Spanner {
6868
static final String COMMIT = "CloudSpannerOperation.Commit";
6969
static final String QUERY = "CloudSpannerOperation.ExecuteStreamingQuery";
7070
static final String READ = "CloudSpannerOperation.ExecuteStreamingRead";
71+
static final String BATCH_WRITE = "CloudSpannerOperation.BatchWrite";
7172

7273
private static final Object CLIENT_ID_LOCK = new Object();
7374

google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,8 @@
157157
import com.google.spanner.admin.instance.v1.UpdateInstanceMetadata;
158158
import com.google.spanner.admin.instance.v1.UpdateInstanceRequest;
159159
import com.google.spanner.v1.BatchCreateSessionsRequest;
160+
import com.google.spanner.v1.BatchWriteRequest;
161+
import com.google.spanner.v1.BatchWriteResponse;
160162
import com.google.spanner.v1.BeginTransactionRequest;
161163
import com.google.spanner.v1.CommitRequest;
162164
import com.google.spanner.v1.CommitResponse;
@@ -1684,6 +1686,14 @@ public ServerStream<PartialResultSet> executeStreamingPartitionedDml(
16841686
return partitionedDmlStub.executeStreamingSqlCallable().call(request, context);
16851687
}
16861688

1689+
@Override
1690+
public ServerStream<BatchWriteResponse> batchWriteAtLeastOnce(
1691+
BatchWriteRequest request, @Nullable Map<Option, ?> options) {
1692+
GrpcCallContext context =
1693+
newCallContext(options, request.getSession(), request, SpannerGrpc.getBatchWriteMethod());
1694+
return spannerStub.batchWriteCallable().call(request, context);
1695+
}
1696+
16871697
@Override
16881698
public StreamingCall executeQuery(
16891699
ExecuteSqlRequest request,

google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerRpc.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,9 @@ ApiFuture<ResultSet> executeQueryAsync(
403403
ServerStream<PartialResultSet> executeStreamingPartitionedDml(
404404
ExecuteSqlRequest request, @Nullable Map<Option, ?> options, Duration timeout);
405405

406+
ServerStream<BatchWriteResponse> batchWriteAtLeastOnce(
407+
BatchWriteRequest request, @Nullable Map<Option, ?> options);
408+
406409
/**
407410
* Executes a query with streaming result.
408411
*

0 commit comments

Comments
 (0)