Skip to content

Commit a93bfce

Browse files
committed
Add TransactionOptions to allow control over number of attempts to commit, before transaction fails.
1 parent af391ae commit a93bfce

File tree

6 files changed

+162
-11
lines changed

6 files changed

+162
-11
lines changed

firebase-firestore/src/androidTest/java/com/google/firebase/firestore/TransactionTest.java

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import static org.junit.Assert.assertEquals;
2222
import static org.junit.Assert.assertFalse;
2323
import static org.junit.Assert.assertNotNull;
24+
import static org.junit.Assert.assertThrows;
2425
import static org.junit.Assert.assertTrue;
2526
import static org.junit.Assert.fail;
2627

@@ -29,7 +30,6 @@
2930
import com.google.android.gms.tasks.TaskCompletionSource;
3031
import com.google.android.gms.tasks.Tasks;
3132
import com.google.firebase.firestore.FirebaseFirestoreException.Code;
32-
import com.google.firebase.firestore.core.TransactionRunner;
3333
import com.google.firebase.firestore.testutil.IntegrationTestUtil;
3434
import com.google.firebase.firestore.util.AsyncQueue.TimerId;
3535
import java.util.ArrayList;
@@ -651,7 +651,38 @@ public void testMakesDefaultMaxAttempts() {
651651

652652
Exception e = waitForException(transactionTask);
653653
assertEquals(Code.FAILED_PRECONDITION, ((FirebaseFirestoreException) e).getCode());
654-
assertEquals(TransactionRunner.DEFAULT_MAX_ATTEMPTS_COUNT, count.get());
654+
assertEquals(TransactionOptions.DEFAULT_MAX_ATTEMPTS_COUNT, count.get());
655+
}
656+
657+
@Test
658+
public void testMakesOptionSpecifiedMaxAttempts() {
659+
TransactionOptions options = new TransactionOptions.Builder().setMaxAttempts(1).build();
660+
661+
FirebaseFirestore firestore = testFirestore();
662+
DocumentReference doc1 = firestore.collection("counters").document();
663+
AtomicInteger count = new AtomicInteger(0);
664+
waitFor(doc1.set(map("count", 15)));
665+
Task<Void> transactionTask =
666+
firestore.runTransaction(
667+
options,
668+
transaction -> {
669+
// Get the first doc.
670+
transaction.get(doc1);
671+
// Do a write outside of the transaction to cause the transaction to fail.
672+
waitFor(doc1.set(map("count", 1234 + count.incrementAndGet())));
673+
return null;
674+
});
675+
676+
Exception e = waitForException(transactionTask);
677+
assertEquals(Code.FAILED_PRECONDITION, ((FirebaseFirestoreException) e).getCode());
678+
assertEquals(options.getMaxAttempts(), count.get());
679+
}
680+
681+
@Test
682+
public void testTransactionOptionsZeroMaxAttempts_shouldThrowIllegalArgumentException() {
683+
assertThrows(
684+
IllegalArgumentException.class,
685+
() -> new TransactionOptions.Builder().setMaxAttempts(0).build());
655686
}
656687

657688
@Test

firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,7 @@ public Query collectionGroup(@NonNull String collectionId) {
419419
* @return The task returned from the updateFunction.
420420
*/
421421
private <ResultT> Task<ResultT> runTransaction(
422-
Transaction.Function<ResultT> updateFunction, Executor executor) {
422+
TransactionOptions options, Transaction.Function<ResultT> updateFunction, Executor executor) {
423423
ensureClientConfigured();
424424

425425
// We wrap the function they provide in order to
@@ -434,7 +434,7 @@ private <ResultT> Task<ResultT> runTransaction(
434434
updateFunction.apply(
435435
new Transaction(internalTransaction, FirebaseFirestore.this)));
436436

437-
return client.transaction(wrappedUpdateFunction);
437+
return client.transaction(options, wrappedUpdateFunction);
438438
}
439439

440440
/**
@@ -448,9 +448,26 @@ private <ResultT> Task<ResultT> runTransaction(
448448
@NonNull
449449
public <TResult> Task<TResult> runTransaction(
450450
@NonNull Transaction.Function<TResult> updateFunction) {
451+
return runTransaction(TransactionOptions.DEFAULT, updateFunction);
452+
}
453+
454+
/**
455+
* Executes the given updateFunction and then attempts to commit the changes applied within the
456+
* transaction. If any document read within the transaction has changed, the updateFunction will
457+
* be retried. If it fails to commit after the maxmimum number of attempts specified in
458+
* transactionOptions, the transaction will fail.
459+
*
460+
* @param options The function to execute within the transaction context.
461+
* @param updateFunction The function to execute within the transaction context.
462+
* @return The task returned from the updateFunction.
463+
*/
464+
public <TResult> Task<TResult> runTransaction(
465+
@NonNull TransactionOptions options, @NonNull Transaction.Function<TResult> updateFunction) {
451466
checkNotNull(updateFunction, "Provided transaction update function must not be null.");
452467
return runTransaction(
453-
updateFunction, com.google.firebase.firestore.core.Transaction.getDefaultExecutor());
468+
options,
469+
updateFunction,
470+
com.google.firebase.firestore.core.Transaction.getDefaultExecutor());
454471
}
455472

456473
/**
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// Copyright 2022 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.firestore;
16+
17+
/**
18+
* Parameter for {@link FirebaseFirestore#runTransaction(TransactionOptions, Transaction.Function)}.
19+
*/
20+
public final class TransactionOptions {
21+
22+
static final TransactionOptions DEFAULT = new TransactionOptions.Builder().build();
23+
static final int DEFAULT_MAX_ATTEMPTS_COUNT = 5;
24+
25+
private final int maxAttempts;
26+
27+
private TransactionOptions(int maxAttempts) {
28+
this.maxAttempts = maxAttempts;
29+
}
30+
31+
/** A Builder for creating {@code TransactionOptions}. */
32+
public static final class Builder {
33+
private int maxAttempts = DEFAULT_MAX_ATTEMPTS_COUNT;
34+
35+
/** Constructs a new {@code TransactionOptions} Builder object. */
36+
public Builder() {}
37+
38+
/**
39+
* Constructs a new {@code TransactionOptions} Builder based on an existing {@code
40+
* TransactionOptions} object.
41+
*/
42+
public Builder(TransactionOptions options) {
43+
maxAttempts = options.maxAttempts;
44+
}
45+
46+
/**
47+
* Set maximum number of attempts to commit, after which transaction fails. Default is 5.
48+
*
49+
* @return this builder
50+
*/
51+
public Builder setMaxAttempts(int maxAttempts) {
52+
if (maxAttempts < 1) throw new IllegalArgumentException("Max attempts must be at least 1");
53+
this.maxAttempts = maxAttempts;
54+
return this;
55+
}
56+
57+
58+
/**
59+
* Build the {@code TransactionOptions} object.
60+
*
61+
* @return the built {@code TransactionOptions} object
62+
*/
63+
public TransactionOptions build() {
64+
return new TransactionOptions(maxAttempts);
65+
}
66+
}
67+
68+
/**
69+
* Get maximum number of attempts to commit, after which transaction fails. Default is 5.
70+
*
71+
* @return maximum number of attempts
72+
*/
73+
public int getMaxAttempts() {
74+
return maxAttempts;
75+
}
76+
77+
@Override
78+
public boolean equals(Object o) {
79+
if (this == o) return true;
80+
if (o == null || getClass() != o.getClass()) return false;
81+
82+
TransactionOptions that = (TransactionOptions) o;
83+
84+
return maxAttempts == that.maxAttempts;
85+
}
86+
87+
@Override
88+
public int hashCode() {
89+
return maxAttempts;
90+
}
91+
92+
@Override
93+
public String toString() {
94+
return "TransactionOptions{" + "maxAttempts=" + maxAttempts + '}';
95+
}
96+
}

firebase-firestore/src/main/java/com/google/firebase/firestore/core/FirestoreClient.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import com.google.firebase.firestore.FirebaseFirestoreException.Code;
2727
import com.google.firebase.firestore.FirebaseFirestoreSettings;
2828
import com.google.firebase.firestore.LoadBundleTask;
29+
import com.google.firebase.firestore.TransactionOptions;
2930
import com.google.firebase.firestore.auth.CredentialsProvider;
3031
import com.google.firebase.firestore.auth.User;
3132
import com.google.firebase.firestore.bundle.BundleReader;
@@ -228,10 +229,12 @@ public Task<Void> write(final List<Mutation> mutations) {
228229
}
229230

230231
/** Tries to execute the transaction in updateFunction. */
231-
public <TResult> Task<TResult> transaction(Function<Transaction, Task<TResult>> updateFunction) {
232+
public <TResult> Task<TResult> transaction(
233+
TransactionOptions options, Function<Transaction, Task<TResult>> updateFunction) {
232234
this.verifyNotTerminated();
233235
return AsyncQueue.callTask(
234-
asyncQueue.getExecutor(), () -> syncEngine.transaction(asyncQueue, updateFunction));
236+
asyncQueue.getExecutor(),
237+
() -> syncEngine.transaction(asyncQueue, options, updateFunction));
235238
}
236239

237240
/**

firebase-firestore/src/main/java/com/google/firebase/firestore/core/SyncEngine.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import com.google.firebase.firestore.FirebaseFirestoreException;
2727
import com.google.firebase.firestore.LoadBundleTask;
2828
import com.google.firebase.firestore.LoadBundleTaskProgress;
29+
import com.google.firebase.firestore.TransactionOptions;
2930
import com.google.firebase.firestore.auth.User;
3031
import com.google.firebase.firestore.bundle.BundleElement;
3132
import com.google.firebase.firestore.bundle.BundleLoader;
@@ -307,8 +308,10 @@ private void addUserCallback(int batchId, TaskCompletionSource<Void> userTask) {
307308
* <p>The Task returned is resolved when the transaction is fully committed.
308309
*/
309310
public <TResult> Task<TResult> transaction(
310-
AsyncQueue asyncQueue, Function<Transaction, Task<TResult>> updateFunction) {
311-
return new TransactionRunner<TResult>(asyncQueue, remoteStore, updateFunction).run();
311+
AsyncQueue asyncQueue,
312+
TransactionOptions options,
313+
Function<Transaction, Task<TResult>> updateFunction) {
314+
return new TransactionRunner<TResult>(asyncQueue, remoteStore, options, updateFunction).run();
312315
}
313316

314317
/** Called by FirestoreClient to notify us of a new remote event. */

firebase-firestore/src/main/java/com/google/firebase/firestore/core/TransactionRunner.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import com.google.android.gms.tasks.Task;
1919
import com.google.android.gms.tasks.TaskCompletionSource;
2020
import com.google.firebase.firestore.FirebaseFirestoreException;
21+
import com.google.firebase.firestore.TransactionOptions;
2122
import com.google.firebase.firestore.remote.Datastore;
2223
import com.google.firebase.firestore.remote.RemoteStore;
2324
import com.google.firebase.firestore.util.AsyncQueue;
@@ -27,7 +28,6 @@
2728

2829
/** TransactionRunner encapsulates the logic needed to run and retry transactions with backoff. */
2930
public class TransactionRunner<TResult> {
30-
public static final int DEFAULT_MAX_ATTEMPTS_COUNT = 5;
3131
private AsyncQueue asyncQueue;
3232
private RemoteStore remoteStore;
3333
private Function<Transaction, Task<TResult>> updateFunction;
@@ -39,12 +39,13 @@ public class TransactionRunner<TResult> {
3939
public TransactionRunner(
4040
AsyncQueue asyncQueue,
4141
RemoteStore remoteStore,
42+
TransactionOptions options,
4243
Function<Transaction, Task<TResult>> updateFunction) {
4344

4445
this.asyncQueue = asyncQueue;
4546
this.remoteStore = remoteStore;
4647
this.updateFunction = updateFunction;
47-
this.attemptsRemaining = DEFAULT_MAX_ATTEMPTS_COUNT;
48+
this.attemptsRemaining = options.getMaxAttempts();
4849

4950
backoff = new ExponentialBackoff(asyncQueue, TimerId.RETRY_TRANSACTION);
5051
}

0 commit comments

Comments
 (0)