From 67a2379883ffcd7103403eda8e5a989cd5e8b3ae Mon Sep 17 00:00:00 2001 From: Thiago Nunes Date: Wed, 21 Oct 2020 11:23:45 +1100 Subject: [PATCH] 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 | 13 ++++ .../google/cloud/spanner/DatabaseClient.java | 62 +++++++++++++++++++ .../cloud/spanner/DatabaseClientImpl.java | 15 +++++ .../com/google/cloud/spanner/Options.java | 3 + .../com/google/cloud/spanner/SessionImpl.java | 15 +++++ .../com/google/cloud/spanner/SessionPool.java | 29 +++++++++ .../google/cloud/spanner/WriteResponse.java | 52 ++++++++++++++++ 7 files changed, 189 insertions(+) create mode 100644 google-cloud-spanner/src/main/java/com/google/cloud/spanner/WriteResponse.java diff --git a/google-cloud-spanner/clirr-ignored-differences.xml b/google-cloud-spanner/clirr-ignored-differences.xml index 1f7beb76e9a..6f2ddb4ebcf 100644 --- a/google-cloud-spanner/clirr-ignored-differences.xml +++ b/google-cloud-spanner/clirr-ignored-differences.xml @@ -377,4 +377,17 @@ com/google/cloud/spanner/AsyncTransactionManager com.google.api.core.ApiFuture closeAsync() + + + + 7012 + com/google/cloud/spanner/DatabaseClient + com.google.cloud.spanner.WriteResponse writeWithOptions(java.lang.Iterable, com.google.cloud.spanner.Options$WriteOption[]) + + + 7012 + com/google/cloud/spanner/DatabaseClient + com.google.cloud.spanner.WriteResponse writeAtLeastOnceWithOptions(java.lang.Iterable, com.google.cloud.spanner.Options$WriteOption[]) + + 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 d52d1d892e5..05ed49a3e7e 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.WriteOption; /** * 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 write response with the timestamp at which the write was committed + */ + WriteResponse writeWithOptions(Iterable mutations, WriteOption... 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 write response with the timestamp at which the write was committed + */ + WriteResponse writeAtLeastOnceWithOptions(Iterable mutations, WriteOption... 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 4dd10001c70..aa2356787d6 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.WriteOption; 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 WriteResponse writeWithOptions(Iterable mutations, WriteOption... options) + throws SpannerException { + final Timestamp commitTimestamp = write(mutations); + return new WriteResponse(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 WriteResponse writeAtLeastOnceWithOptions( + Iterable mutations, WriteOption... options) throws SpannerException { + final Timestamp commitTimestamp = writeAtLeastOnce(mutations); + return new WriteResponse(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 879b632d175..473882f3d3c 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 WriteOption {} + /** 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 6a91d85fef4..9655e6b66d3 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.WriteOption; import com.google.cloud.spanner.SessionClient.SessionId; import com.google.cloud.spanner.TransactionRunnerImpl.TransactionContextImpl; import com.google.cloud.spanner.spi.v1.SpannerRpc; @@ -139,6 +140,13 @@ public Void run(TransactionContext ctx) { return runner.getCommitTimestamp(); } + @Override + public WriteResponse writeWithOptions(Iterable mutations, WriteOption... options) + throws SpannerException { + final Timestamp commitTimestamp = write(mutations); + return new WriteResponse(commitTimestamp); + } + @Override public Timestamp writeAtLeastOnce(Iterable mutations) throws SpannerException { setActive(null); @@ -168,6 +176,13 @@ public Timestamp writeAtLeastOnce(Iterable mutations) throws SpannerEx } } + @Override + public WriteResponse writeAtLeastOnceWithOptions( + Iterable mutations, WriteOption... options) throws SpannerException { + final Timestamp commitTimestamp = writeAtLeastOnce(mutations); + return new WriteResponse(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 2512024117c..90d737957cd 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,6 +47,7 @@ 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.WriteOption; import com.google.cloud.spanner.SessionClient.SessionConsumer; import com.google.cloud.spanner.SpannerException.ResourceNotFoundException; import com.google.cloud.spanner.SpannerImpl.ClosedException; @@ -1103,6 +1104,13 @@ public Timestamp write(Iterable mutations) throws SpannerException { } } + @Override + public WriteResponse writeWithOptions(Iterable mutations, WriteOption... options) + throws SpannerException { + final Timestamp commitTimestamp = write(mutations); + return new WriteResponse(commitTimestamp); + } + @Override public Timestamp writeAtLeastOnce(Iterable mutations) throws SpannerException { try { @@ -1112,6 +1120,13 @@ public Timestamp writeAtLeastOnce(Iterable mutations) throws SpannerEx } } + @Override + public WriteResponse writeAtLeastOnceWithOptions( + Iterable mutations, WriteOption... options) throws SpannerException { + final Timestamp commitTimestamp = writeAtLeastOnce(mutations); + return new WriteResponse(commitTimestamp); + } + @Override public ReadContext singleUse() { try { @@ -1347,6 +1362,13 @@ public Timestamp write(Iterable mutations) throws SpannerException { } } + @Override + public WriteResponse writeWithOptions(Iterable mutations, WriteOption... options) + throws SpannerException { + final Timestamp commitTimestamp = write(mutations); + return new WriteResponse(commitTimestamp); + } + @Override public Timestamp writeAtLeastOnce(Iterable mutations) throws SpannerException { try { @@ -1357,6 +1379,13 @@ public Timestamp writeAtLeastOnce(Iterable mutations) throws SpannerEx } } + @Override + public WriteResponse writeAtLeastOnceWithOptions( + Iterable mutations, WriteOption... options) throws SpannerException { + final Timestamp commitTimestamp = writeAtLeastOnce(mutations); + return new WriteResponse(commitTimestamp); + } + @Override public long executePartitionedUpdate(Statement stmt) throws SpannerException { try { diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/WriteResponse.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/WriteResponse.java new file mode 100644 index 00000000000..4bd3e186510 --- /dev/null +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/WriteResponse.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 write / writeAtLeast once operation. */ +public class WriteResponse { + + private final Timestamp commitTimestamp; + + public WriteResponse(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; + } + WriteResponse that = (WriteResponse) o; + return Objects.equals(commitTimestamp, that.commitTimestamp); + } + + @Override + public int hashCode() { + return Objects.hash(commitTimestamp); + } +}