A transaction in Firestore can be either read-write or read-only. + * + *
The default set of options is a read-write transaction with a maximum number of 5 attempts. + * This attempt count can be customized via the {@link + * ReadWriteOptionsBuilder#setNumberOfAttempts(int)} method. A new instance of a builder can be + * created by calling {@link #createReadWriteOptionsBuilder()}. + * + *
A read-only transaction can be configured via the {@link ReadOnlyOptionsBuilder} class. A new
+ * instance can be created by calling {@link #createReadOnlyOptionsBuilder()}.
+ *
+ * @see com.google.firestore.v1.TransactionOptions
+ */
public final class TransactionOptions {
+ private static final TransactionOptions DEFAULT_READ_WRITE_TRANSACTION_OPTIONS =
+ createReadWriteOptionsBuilder().build();
+
private static final int DEFAULT_NUM_ATTEMPTS = 5;
- private final int numberOfAttempts;
private final Executor executor;
+ private final TransactionOptionsType type;
+ private final int numberOfAttempts;
+ @Nullable private final Timestamp readTime;
- TransactionOptions(int maxAttempts, Executor executor) {
- this.numberOfAttempts = maxAttempts;
+ TransactionOptions(
+ Executor executor,
+ TransactionOptionsType type,
+ int numberOfAttempts,
+ @Nullable Timestamp readTime) {
this.executor = executor;
+ this.type = type;
+ this.numberOfAttempts = numberOfAttempts;
+ this.readTime = readTime;
}
+ /**
+ * Returns the maximum number of times a transaction will be attempted before resulting in an
+ * error.
+ *
+ * @return The max number of attempts to try and commit the transaction.
+ */
public int getNumberOfAttempts() {
return numberOfAttempts;
}
+ /** @return Executor to be used to run user callbacks on */
@Nullable
public Executor getExecutor() {
return executor;
}
/**
- * Create a default set of options suitable for most use cases. Transactions will be attempted 5
- * times.
+ * A type flag indicating the type of transaction represented.
+ *
+ * @return The type of transaction this represents. Either read-only or read-write.
+ */
+ @Nonnull
+ public TransactionOptionsType getType() {
+ return type;
+ }
+
+ /**
+ * A {@link Timestamp} specifying the time documents are to be read at. If null, the server will
+ * read documents at the most up to date available. If non-null, the specified {@code Timestamp}
+ * may not be more than 60 seconds in the past (evaluated when the request is processed by the
+ * server).
+ *
+ * @return The specific time to read documents at. A null value means reading the most up to date
+ * data.
+ */
+ @Nullable
+ public Timestamp getReadTime() {
+ // This if statement is not strictly necessary, however is kept here for clarity sake to show
+ // that readTime is only applicable to a read-only transaction type.
+ if (TransactionOptionsType.READ_ONLY.equals(type)) {
+ return readTime;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Create a default set of options suitable for most use cases. Transactions will be opened as
+ * ReadWrite transactions and attempted up to 5 times.
*
* @return The TransactionOptions object.
+ * @see #createReadWriteOptionsBuilder()
*/
@Nonnull
public static TransactionOptions create() {
- return new TransactionOptions(DEFAULT_NUM_ATTEMPTS, null);
+ return DEFAULT_READ_WRITE_TRANSACTION_OPTIONS;
}
/**
@@ -59,11 +125,13 @@ public static TransactionOptions create() {
*
* @param numberOfAttempts The number of execution attempts.
* @return The TransactionOptions object.
+ * @deprecated as of 2.0.0, replaced by {@link ReadWriteOptionsBuilder#setNumberOfAttempts(int)}
+ * @see #createReadWriteOptionsBuilder()
*/
@Nonnull
+ @Deprecated
public static TransactionOptions create(int numberOfAttempts) {
- Preconditions.checkArgument(numberOfAttempts > 0, "You must allow at least one attempt");
- return new TransactionOptions(numberOfAttempts, null);
+ return createReadWriteOptionsBuilder().setNumberOfAttempts(numberOfAttempts).build();
}
/**
@@ -71,10 +139,13 @@ public static TransactionOptions create(int numberOfAttempts) {
*
* @param executor The executor to run the user callback code on.
* @return The TransactionOptions object.
+ * @deprecated as of 2.0.0, replaced by {@link ReadWriteOptionsBuilder#setExecutor(Executor)}
+ * @see #createReadWriteOptionsBuilder()
*/
@Nonnull
- public static TransactionOptions create(@Nonnull Executor executor) {
- return new TransactionOptions(DEFAULT_NUM_ATTEMPTS, executor);
+ @Deprecated
+ public static TransactionOptions create(@Nullable Executor executor) {
+ return createReadWriteOptionsBuilder().setExecutor(executor).build();
}
/**
@@ -83,10 +154,166 @@ public static TransactionOptions create(@Nonnull Executor executor) {
* @param executor The executor to run the user callback code on.
* @param numberOfAttempts The number of execution attempts.
* @return The TransactionOptions object.
+ * @deprecated as of 2.0.0, replaced by {@link ReadWriteOptionsBuilder#setExecutor(Executor)} and
+ * {@link ReadWriteOptionsBuilder#setNumberOfAttempts(int)}
+ * @see #createReadWriteOptionsBuilder()
*/
@Nonnull
- public static TransactionOptions create(@Nonnull Executor executor, int numberOfAttempts) {
- Preconditions.checkArgument(numberOfAttempts > 0, "You must allow at least one attempt");
- return new TransactionOptions(numberOfAttempts, executor);
+ @Deprecated
+ public static TransactionOptions create(@Nullable Executor executor, int numberOfAttempts) {
+ return createReadWriteOptionsBuilder()
+ .setExecutor(executor)
+ .setNumberOfAttempts(numberOfAttempts)
+ .build();
+ }
+
+ /**
+ * @return a new Builder with default values applicable to configuring options for a read-write
+ * transaction.
+ */
+ @Nonnull
+ public static ReadWriteOptionsBuilder createReadWriteOptionsBuilder() {
+ return new ReadWriteOptionsBuilder(null, DEFAULT_NUM_ATTEMPTS);
+ }
+
+ /**
+ * @return a new Builder with default values applicable to configuring options for a read-only
+ * transaction.
+ */
+ @Nonnull
+ public static ReadOnlyOptionsBuilder createReadOnlyOptionsBuilder() {
+ return new ReadOnlyOptionsBuilder(null, null);
+ }
+
+ @InternalExtensionOnly
+ public abstract static class Builder> {
+ @Nullable protected Executor executor;
+
+ protected Builder(@Nullable Executor executor) {
+ this.executor = executor;
+ }
+
+ /**
+ * @return The {@link Executor} user callbacks will execute on, If null, the default executor
+ * will be used.
+ */
+ @Nullable
+ public Executor getExecutor() {
+ return executor;
+ }
+
+ /**
+ * @param executor The {@link Executor} user callbacks will executed on. If null, the default
+ * executor will be used.
+ * @return {@code this} builder
+ */
+ @Nonnull
+ @SuppressWarnings("unchecked")
+ public B setExecutor(@Nullable Executor executor) {
+ this.executor = executor;
+ return (B) this;
+ }
+
+ /** @return an instance of {@link TransactionOptions} from the values passed to this builder */
+ @Nonnull
+ public abstract TransactionOptions build();
+ }
+
+ /**
+ * A typesafe builder class representing those options that are applicable when configuring a
+ * transaction to be read-only. All methods function as "set" rather than returning a new copy
+ * with a value set on it.
+ */
+ public static final class ReadOnlyOptionsBuilder extends Builder, T> {
- private T userFunctionResult;
+ private final T userFunctionResult;
CommitTransactionCallback(T userFunctionResult) {
this.userFunctionResult = userFunctionResult;
diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/TransactionTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/TransactionTest.java
index ac3767c76..25c60c5f4 100644
--- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/TransactionTest.java
+++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/TransactionTest.java
@@ -34,12 +34,14 @@
import static com.google.cloud.firestore.LocalFirestoreHelper.rollbackResponse;
import static com.google.cloud.firestore.LocalFirestoreHelper.set;
import static com.google.cloud.firestore.LocalFirestoreHelper.update;
+import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
import com.google.api.core.ApiFuture;
import com.google.api.core.ApiFutures;
@@ -51,6 +53,9 @@
import com.google.api.gax.rpc.UnaryCallable;
import com.google.cloud.Timestamp;
import com.google.cloud.firestore.LocalFirestoreHelper.ResponseStubber;
+import com.google.cloud.firestore.TransactionOptions.ReadOnlyOptionsBuilder;
+import com.google.cloud.firestore.TransactionOptions.ReadWriteOptionsBuilder;
+import com.google.cloud.firestore.TransactionOptions.TransactionOptionsType;
import com.google.cloud.firestore.spi.v1.FirestoreRpc;
import com.google.firestore.v1.BatchGetDocumentsRequest;
import com.google.firestore.v1.DocumentMask;
@@ -64,6 +69,7 @@
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
@@ -74,10 +80,10 @@
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Matchers;
-import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.runners.MockitoJUnitRunner;
+@SuppressWarnings("deprecation")
@RunWith(MockitoJUnitRunner.class)
public class TransactionTest {
@@ -86,7 +92,7 @@ public class TransactionTest {
new ApiException(
new Exception("Test exception"), GrpcStatusCode.of(Status.Code.UNKNOWN), true));
- @Spy private FirestoreRpc firestoreRpc = Mockito.mock(FirestoreRpc.class);
+ @Spy private FirestoreRpc firestoreRpc = mock(FirestoreRpc.class);
@Spy
private FirestoreImpl firestoreMock =
@@ -865,4 +871,79 @@ public String updateCallback(Transaction transaction) {
assertEquals(begin(), requests.get(0));
assertEquals(commit(TRANSACTION_ID, writes.toArray(new Write[] {})), requests.get(1));
}
+
+ @Test
+ public void readOnlyTransactionOptionsBuilder_setReadTime() {
+ Executor executor = mock(Executor.class);
+ final com.google.protobuf.Timestamp.Builder readTime =
+ com.google.protobuf.Timestamp.getDefaultInstance().toBuilder().setSeconds(1).setNanos(0);
+ final ReadOnlyOptionsBuilder builder =
+ TransactionOptions.createReadOnlyOptionsBuilder()
+ .setExecutor(executor)
+ .setReadTime(readTime);
+
+ final TransactionOptions transactionOptions = builder.build();
+
+ assertThat(builder.getExecutor()).isSameInstanceAs(executor);
+ assertThat(builder.getReadTime()).isSameInstanceAs(readTime);
+
+ assertThat(transactionOptions.getExecutor()).isSameInstanceAs(executor);
+
+ assertThat(transactionOptions.getType()).isEqualTo(TransactionOptionsType.READ_ONLY);
+ assertThat(transactionOptions.getReadTime()).isEqualTo(readTime.build());
+ assertThat(transactionOptions.getNumberOfAttempts()).isEqualTo(1);
+ }
+
+ @Test
+ public void readOnlyTransactionOptionsBuilder_defaults() {
+ final ReadOnlyOptionsBuilder builder = TransactionOptions.createReadOnlyOptionsBuilder();
+
+ final TransactionOptions transactionOptions = builder.build();
+
+ assertThat(builder.getExecutor()).isNull();
+ assertThat(builder.getReadTime()).isNull();
+
+ assertThat(transactionOptions.getReadTime()).isNull();
+ assertThat(transactionOptions.getNumberOfAttempts()).isEqualTo(1);
+ }
+
+ @Test
+ public void readWriteTransactionOptionsBuilder_setNumberOfAttempts() {
+ Executor executor = mock(Executor.class);
+ final ReadWriteOptionsBuilder builder =
+ TransactionOptions.createReadWriteOptionsBuilder()
+ .setExecutor(executor)
+ .setNumberOfAttempts(2);
+
+ final TransactionOptions transactionOptions = builder.build();
+
+ assertThat(builder.getExecutor()).isSameInstanceAs(executor);
+ assertThat(builder.getNumberOfAttempts()).isEqualTo(2);
+
+ assertThat(transactionOptions.getExecutor()).isSameInstanceAs(executor);
+
+ assertThat(transactionOptions.getType()).isEqualTo(TransactionOptionsType.READ_WRITE);
+ assertThat(transactionOptions.getNumberOfAttempts()).isEqualTo(2);
+ assertThat(transactionOptions.getReadTime()).isNull();
+ }
+
+ @Test
+ public void readWriteTransactionOptionsBuilder_defaults() {
+ final TransactionOptions transactionOptions =
+ TransactionOptions.createReadWriteOptionsBuilder().build();
+
+ assertThat(transactionOptions.getExecutor()).isNull();
+ assertThat(transactionOptions.getNumberOfAttempts()).isEqualTo(5);
+ assertThat(transactionOptions.getReadTime()).isNull();
+ }
+
+ @Test
+ public void readWriteTransactionOptionsBuilder_errorAttemptingToSetNumAttemptsLessThanOne() {
+ try {
+ TransactionOptions.createReadWriteOptionsBuilder().setNumberOfAttempts(0);
+ fail("Error expected");
+ } catch (IllegalArgumentException ignore) {
+ // expected
+ }
+ }
}
diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITSystemTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITSystemTest.java
index d2a768181..1a1a3d4d1 100644
--- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITSystemTest.java
+++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITSystemTest.java
@@ -18,6 +18,7 @@
import static com.google.cloud.firestore.LocalFirestoreHelper.UPDATE_SINGLE_FIELD_OBJECT;
import static com.google.cloud.firestore.LocalFirestoreHelper.map;
+import static com.google.common.truth.Truth.assertThat;
import static java.util.Arrays.asList;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
@@ -56,12 +57,16 @@
import com.google.cloud.firestore.SetOptions;
import com.google.cloud.firestore.Transaction;
import com.google.cloud.firestore.Transaction.Function;
+import com.google.cloud.firestore.TransactionOptions;
import com.google.cloud.firestore.WriteBatch;
import com.google.cloud.firestore.WriteResult;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.firestore.v1.RunQueryRequest;
+import io.grpc.Status;
+import io.grpc.Status.Code;
+import io.grpc.StatusRuntimeException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -72,8 +77,12 @@
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
import javax.annotation.Nullable;
+import org.apache.commons.lang3.exception.ExceptionUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
@@ -1414,6 +1423,93 @@ public void bulkWriterWritesInOrder() throws Exception {
assertEquals(Collections.singletonMap("foo", "bar3"), result.get().getData());
}
+ @Test
+ public void readOnlyTransaction_successfulGet()
+ throws ExecutionException, InterruptedException, TimeoutException {
+ final DocumentReference documentReference = randomColl.add(SINGLE_FIELD_MAP).get();
+
+ final AtomicReference
> done = SettableApiFuture.create();