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); + } +}