From 659719deb5a18a87859bc174f5bde1e1147834d8 Mon Sep 17 00:00:00 2001 From: Thiago Nunes Date: Fri, 23 Oct 2020 10:17:47 +1100 Subject: [PATCH] feat: adds options to the write operations (#531) * feat: adds options to the write operations Adds the possibility of adding options to the write operations and encapsulates the write response into it's own class so that we can augment the response with more fields than the commit timestamp. --- .../clirr-ignored-differences.xml | 12 ++++ .../google/cloud/spanner/CommitResponse.java | 52 ++++++++++++++++ .../google/cloud/spanner/DatabaseClient.java | 62 +++++++++++++++++++ .../cloud/spanner/DatabaseClientImpl.java | 15 +++++ .../com/google/cloud/spanner/Options.java | 3 + .../com/google/cloud/spanner/SessionImpl.java | 18 +++++- .../com/google/cloud/spanner/SessionPool.java | 30 ++++++++- 7 files changed, 189 insertions(+), 3 deletions(-) create mode 100644 google-cloud-spanner/src/main/java/com/google/cloud/spanner/CommitResponse.java diff --git a/google-cloud-spanner/clirr-ignored-differences.xml b/google-cloud-spanner/clirr-ignored-differences.xml index b68f50d78e..3968118b1c 100644 --- a/google-cloud-spanner/clirr-ignored-differences.xml +++ b/google-cloud-spanner/clirr-ignored-differences.xml @@ -378,6 +378,18 @@ com.google.api.core.ApiFuture closeAsync() + + + 7012 + com/google/cloud/spanner/DatabaseClient + com.google.cloud.spanner.CommitResponse writeWithOptions(java.lang.Iterable, com.google.cloud.spanner.Options$TransactionOption[]) + + + 7012 + com/google/cloud/spanner/DatabaseClient + com.google.cloud.spanner.CommitResponse writeAtLeastOnceWithOptions(java.lang.Iterable, com.google.cloud.spanner.Options$TransactionOption[]) + + 7009 diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/CommitResponse.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/CommitResponse.java new file mode 100644 index 0000000000..dd5534d7c3 --- /dev/null +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/CommitResponse.java @@ -0,0 +1,52 @@ +/* + * Copyright 2020 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; + +import com.google.cloud.Timestamp; +import java.util.Objects; + +/** Represents a response from a commit operation. */ +public class CommitResponse { + + private final Timestamp commitTimestamp; + + public CommitResponse(Timestamp commitTimestamp) { + this.commitTimestamp = commitTimestamp; + } + + /** Returns a {@link Timestamp} representing the commit time of the write operation. */ + public Timestamp getCommitTimestamp() { + return commitTimestamp; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CommitResponse that = (CommitResponse) o; + return Objects.equals(commitTimestamp, that.commitTimestamp); + } + + @Override + public int hashCode() { + return Objects.hash(commitTimestamp); + } +} diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java index d52d1d892e..dc1f2a80c7 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java @@ -17,6 +17,7 @@ package com.google.cloud.spanner; import com.google.cloud.Timestamp; +import com.google.cloud.spanner.Options.TransactionOption; /** * Interface for all the APIs that are used to read/write data into a Cloud Spanner database. An @@ -52,6 +53,35 @@ public interface DatabaseClient { */ Timestamp write(Iterable mutations) throws SpannerException; + /** + * Writes the given mutations atomically to the database with the given options. + * + *

This method uses retries and replay protection internally, which means that the mutations + * are applied exactly once on success, or not at all if an error is returned, regardless of any + * failures in the underlying network. Note that if the call is cancelled or reaches deadline, it + * is not possible to know whether the mutations were applied without performing a subsequent + * database operation, but the mutations will have been applied at most once. + * + *

Example of blind write. + * + *

{@code
+   * long singerId = my_singer_id;
+   * Mutation mutation = Mutation.newInsertBuilder("Singer")
+   *         .set("SingerId")
+   *         .to(singerId)
+   *         .set("FirstName")
+   *         .to("Billy")
+   *         .set("LastName")
+   *         .to("Joel")
+   *         .build();
+   * dbClient.writeWithOptions(Collections.singletonList(mutation));
+   * }
+ * + * @return a response with the timestamp at which the write was committed + */ + CommitResponse writeWithOptions(Iterable mutations, TransactionOption... options) + throws SpannerException; + /** * Writes the given mutations atomically to the database without replay protection. * @@ -83,6 +113,38 @@ public interface DatabaseClient { */ Timestamp writeAtLeastOnce(Iterable mutations) throws SpannerException; + /** + * Writes the given mutations atomically to the database without replay protection. + * + *

Since this method does not feature replay protection, it may attempt to apply {@code + * mutations} more than once; if the mutations are not idempotent, this may lead to a failure + * being reported when the mutation was applied once. For example, an insert may fail with {@link + * ErrorCode#ALREADY_EXISTS} even though the row did not exist before this method was called. For + * this reason, most users of the library will prefer to use {@link #write(Iterable)} instead. + * However, {@code writeAtLeastOnce()} requires only a single RPC, whereas {@code write()} + * requires two RPCs (one of which may be performed in advance), and so this method may be + * appropriate for latency sensitive and/or high throughput blind writing. + * + *

Example of unprotected blind write. + * + *

{@code
+   * long singerId = my_singer_id;
+   * Mutation mutation = Mutation.newInsertBuilder("Singers")
+   *         .set("SingerId")
+   *         .to(singerId)
+   *         .set("FirstName")
+   *         .to("Billy")
+   *         .set("LastName")
+   *         .to("Joel")
+   *         .build();
+   * dbClient.writeAtLeastOnce(Collections.singletonList(mutation));
+   * }
+ * + * @return a response with the timestamp at which the write was committed + */ + CommitResponse writeAtLeastOnceWithOptions( + Iterable mutations, TransactionOption... options) throws SpannerException; + /** * Returns a context in which a single read can be performed using {@link TimestampBound#strong()} * concurrency. This method will return a {@link ReadContext} that will not return the read diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java index 4dd10001c7..f6af000d6e 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java @@ -17,6 +17,7 @@ package com.google.cloud.spanner; import com.google.cloud.Timestamp; +import com.google.cloud.spanner.Options.TransactionOption; import com.google.cloud.spanner.SessionPool.PooledSessionFuture; import com.google.cloud.spanner.SpannerImpl.ClosedException; import com.google.common.annotations.VisibleForTesting; @@ -81,6 +82,13 @@ public Timestamp apply(Session session) { } } + @Override + public CommitResponse writeWithOptions(Iterable mutations, TransactionOption... options) + throws SpannerException { + final Timestamp commitTimestamp = write(mutations); + return new CommitResponse(commitTimestamp); + } + @Override public Timestamp writeAtLeastOnce(final Iterable mutations) throws SpannerException { Span span = tracer.spanBuilder(READ_WRITE_TRANSACTION).startSpan(); @@ -101,6 +109,13 @@ public Timestamp apply(Session session) { } } + @Override + public CommitResponse writeAtLeastOnceWithOptions( + Iterable mutations, TransactionOption... options) throws SpannerException { + final Timestamp commitTimestamp = writeAtLeastOnce(mutations); + return new CommitResponse(commitTimestamp); + } + @Override public ReadContext singleUse() { Span span = tracer.spanBuilder(READ_ONLY_TRANSACTION).startSpan(); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java index 879b632d17..217d81b886 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java @@ -33,6 +33,9 @@ public interface ReadOption {} /** Marker interface to mark options applicable to query operation. */ public interface QueryOption {} + /** Marker interface to mark options applicable to write operations */ + public interface TransactionOption {} + /** Marker interface to mark options applicable to list operations in admin API. */ public interface ListOption {} diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionImpl.java index 6a91d85fef..971dfc2ab1 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionImpl.java @@ -25,6 +25,7 @@ import com.google.cloud.spanner.AbstractReadContext.MultiUseReadOnlyTransaction; import com.google.cloud.spanner.AbstractReadContext.SingleReadContext; import com.google.cloud.spanner.AbstractReadContext.SingleUseReadOnlyTransaction; +import com.google.cloud.spanner.Options.TransactionOption; import com.google.cloud.spanner.SessionClient.SessionId; import com.google.cloud.spanner.TransactionRunnerImpl.TransactionContextImpl; import com.google.cloud.spanner.spi.v1.SpannerRpc; @@ -35,7 +36,6 @@ import com.google.protobuf.Empty; import com.google.spanner.v1.BeginTransactionRequest; import com.google.spanner.v1.CommitRequest; -import com.google.spanner.v1.CommitResponse; import com.google.spanner.v1.Transaction; import com.google.spanner.v1.TransactionOptions; import io.opencensus.common.Scope; @@ -139,6 +139,13 @@ public Void run(TransactionContext ctx) { return runner.getCommitTimestamp(); } + @Override + public CommitResponse writeWithOptions(Iterable mutations, TransactionOption... options) + throws SpannerException { + final Timestamp commitTimestamp = write(mutations); + return new CommitResponse(commitTimestamp); + } + @Override public Timestamp writeAtLeastOnce(Iterable mutations) throws SpannerException { setActive(null); @@ -154,7 +161,7 @@ public Timestamp writeAtLeastOnce(Iterable mutations) throws SpannerEx .build(); Span span = tracer.spanBuilder(SpannerImpl.COMMIT).startSpan(); try (Scope s = tracer.withSpan(span)) { - CommitResponse response = spanner.getRpc().commit(request, options); + com.google.spanner.v1.CommitResponse response = spanner.getRpc().commit(request, options); Timestamp t = Timestamp.fromProto(response.getCommitTimestamp()); return t; } catch (IllegalArgumentException e) { @@ -168,6 +175,13 @@ public Timestamp writeAtLeastOnce(Iterable mutations) throws SpannerEx } } + @Override + public CommitResponse writeAtLeastOnceWithOptions( + Iterable mutations, TransactionOption... options) throws SpannerException { + final Timestamp commitTimestamp = writeAtLeastOnce(mutations); + return new CommitResponse(commitTimestamp); + } + @Override public ReadContext singleUse() { return singleUse(TimestampBound.strong()); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPool.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPool.java index 2512024117..8d1e44fe9a 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPool.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPool.java @@ -47,10 +47,10 @@ import com.google.cloud.grpc.GrpcTransportOptions.ExecutorFactory; import com.google.cloud.spanner.Options.QueryOption; import com.google.cloud.spanner.Options.ReadOption; +import com.google.cloud.spanner.Options.TransactionOption; import com.google.cloud.spanner.SessionClient.SessionConsumer; import com.google.cloud.spanner.SpannerException.ResourceNotFoundException; import com.google.cloud.spanner.SpannerImpl.ClosedException; -import com.google.cloud.spanner.TransactionManager.TransactionState; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; import com.google.common.base.MoreObjects; @@ -1103,6 +1103,13 @@ public Timestamp write(Iterable mutations) throws SpannerException { } } + @Override + public CommitResponse writeWithOptions( + Iterable mutations, TransactionOption... options) throws SpannerException { + final Timestamp commitTimestamp = write(mutations); + return new CommitResponse(commitTimestamp); + } + @Override public Timestamp writeAtLeastOnce(Iterable mutations) throws SpannerException { try { @@ -1112,6 +1119,13 @@ public Timestamp writeAtLeastOnce(Iterable mutations) throws SpannerEx } } + @Override + public CommitResponse writeAtLeastOnceWithOptions( + Iterable mutations, TransactionOption... options) throws SpannerException { + final Timestamp commitTimestamp = writeAtLeastOnce(mutations); + return new CommitResponse(commitTimestamp); + } + @Override public ReadContext singleUse() { try { @@ -1347,6 +1361,13 @@ public Timestamp write(Iterable mutations) throws SpannerException { } } + @Override + public CommitResponse writeWithOptions( + Iterable mutations, TransactionOption... options) throws SpannerException { + final Timestamp commitTimestamp = write(mutations); + return new CommitResponse(commitTimestamp); + } + @Override public Timestamp writeAtLeastOnce(Iterable mutations) throws SpannerException { try { @@ -1357,6 +1378,13 @@ public Timestamp writeAtLeastOnce(Iterable mutations) throws SpannerEx } } + @Override + public CommitResponse writeAtLeastOnceWithOptions( + Iterable mutations, TransactionOption... options) throws SpannerException { + final Timestamp commitTimestamp = writeAtLeastOnce(mutations); + return new CommitResponse(commitTimestamp); + } + @Override public long executePartitionedUpdate(Statement stmt) throws SpannerException { try {