Skip to content

Commit 672c6dc

Browse files
Tomandersen/transaction options (#3664)
* - Changes to protoc version to make it working for m1 macs. * - Changes to protoc version to make it working for m1 macs. * - Changes to dependency versions to make it working for m1 macs. * - Reverting the robolectric version change * Revert "- Changes to dependency versions to make it working for m1 macs." This reverts commit 5a31b19 * - Updating robolectric versions to 4.7+ to get support for m1 macs. See: robolectric/robolectric#6311 * Reverting some changes to check * Add TransactionOptions to allow control over number of attempts to commit, before transaction fails. * Add TransactionOptions to allow control over number of attempts to commit, before transaction fails. * Add missing @nonnull annotation * Fix comments and add api.txt change. * Update CHANGELOG with TransactionOptions * Improve method comments for TransactionOptions. Co-authored-by: Eldhose Mathokkil Babu <[email protected]>
1 parent aa45160 commit 672c6dc

File tree

8 files changed

+193
-17
lines changed

8 files changed

+193
-17
lines changed

firebase-firestore/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ by opting into a release at
33
[go/firebase-android-release](http:go/firebase-android-release) (Googlers only).
44

55
# Unreleased
6+
- [changed] Added `TransactionOptions` to control how many times a transaction
7+
will retry commits before failing.
68
- [fixed] Fixed an issue where patching multiple fields shadows each other (#3528).
79

810
# 24.1.1

firebase-firestore/api.txt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ package com.google.firebase.firestore {
152152
method @NonNull public com.google.firebase.firestore.LoadBundleTask loadBundle(@NonNull java.nio.ByteBuffer);
153153
method @NonNull public com.google.android.gms.tasks.Task<java.lang.Void> runBatch(@NonNull com.google.firebase.firestore.WriteBatch.Function);
154154
method @NonNull public <TResult> com.google.android.gms.tasks.Task<TResult> runTransaction(@NonNull com.google.firebase.firestore.Transaction.Function<TResult>);
155+
method @NonNull public <TResult> com.google.android.gms.tasks.Task<TResult> runTransaction(@NonNull com.google.firebase.firestore.TransactionOptions, @NonNull com.google.firebase.firestore.Transaction.Function<TResult>);
155156
method public void setFirestoreSettings(@NonNull com.google.firebase.firestore.FirebaseFirestoreSettings);
156157
method @NonNull public com.google.android.gms.tasks.Task<java.lang.Void> setIndexConfiguration(@NonNull String);
157158
method public static void setLoggingEnabled(boolean);
@@ -387,6 +388,17 @@ package com.google.firebase.firestore {
387388
method @Nullable public TResult apply(@NonNull com.google.firebase.firestore.Transaction) throws com.google.firebase.firestore.FirebaseFirestoreException;
388389
}
389390

391+
public final class TransactionOptions {
392+
method public int getMaxAttempts();
393+
}
394+
395+
public static final class TransactionOptions.Builder {
396+
ctor public TransactionOptions.Builder();
397+
ctor public TransactionOptions.Builder(@NonNull com.google.firebase.firestore.TransactionOptions);
398+
method @NonNull public com.google.firebase.firestore.TransactionOptions build();
399+
method @NonNull public com.google.firebase.firestore.TransactionOptions.Builder setMaxAttempts(int);
400+
}
401+
390402
public class WriteBatch {
391403
method @NonNull public com.google.android.gms.tasks.Task<java.lang.Void> commit();
392404
method @NonNull public com.google.firebase.firestore.WriteBatch delete(@NonNull com.google.firebase.firestore.DocumentReference);

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: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -405,9 +405,10 @@ public Query collectionGroup(@NonNull String collectionId) {
405405
}
406406

407407
/**
408-
* Executes the given updateFunction and then attempts to commit the changes applied within the
409-
* transaction. If any document read within the transaction has changed, the updateFunction will
410-
* be retried. If it fails to commit after 5 attempts, the transaction will fail.
408+
* Executes the given {@code updateFunction} and then attempts to commit the changes applied
409+
* within the transaction. If any document read within the transaction has changed, the
410+
* updateFunction will be retried. If it fails to commit after 5 attempts (the default failure
411+
* limit), the transaction will fail.
411412
*
412413
* <p>The maximum number of writes allowed in a single transaction is 500, but note that each
413414
* usage of {@link FieldValue#serverTimestamp()}, {@link FieldValue#arrayUnion(Object...)}, {@link
@@ -419,7 +420,7 @@ public Query collectionGroup(@NonNull String collectionId) {
419420
* @return The task returned from the updateFunction.
420421
*/
421422
private <ResultT> Task<ResultT> runTransaction(
422-
Transaction.Function<ResultT> updateFunction, Executor executor) {
423+
TransactionOptions options, Transaction.Function<ResultT> updateFunction, Executor executor) {
423424
ensureClientConfigured();
424425

425426
// We wrap the function they provide in order to
@@ -434,23 +435,43 @@ private <ResultT> Task<ResultT> runTransaction(
434435
updateFunction.apply(
435436
new Transaction(internalTransaction, FirebaseFirestore.this)));
436437

437-
return client.transaction(wrappedUpdateFunction);
438+
return client.transaction(options, wrappedUpdateFunction);
438439
}
439440

440441
/**
441-
* Executes the given updateFunction and then attempts to commit the changes applied within the
442-
* transaction. If any document read within the transaction has changed, the updateFunction will
443-
* be retried. If it fails to commit after 5 attempts, the transaction will fail.
442+
* Executes the given {@code updateFunction} and then attempts to commit the changes applied
443+
* within the transaction. If any document read within the transaction has changed, the
444+
* updateFunction will be retried. If it fails to commit after 5 attempts (the default failure
445+
* limit), the transaction will fail. To have a different number of retries, use the {@link
446+
* FirebaseFirestore#runTransaction(TransactionOptions, Transaction.Function)} method instead.
444447
*
445448
* @param updateFunction The function to execute within the transaction context.
446449
* @return The task returned from the updateFunction.
447450
*/
448451
@NonNull
449452
public <TResult> Task<TResult> runTransaction(
450453
@NonNull Transaction.Function<TResult> updateFunction) {
454+
return runTransaction(TransactionOptions.DEFAULT, updateFunction);
455+
}
456+
457+
/**
458+
* Executes the given {@code updateFunction} and then attempts to commit the changes applied
459+
* within the transaction. If any document read within the transaction has changed, the
460+
* updateFunction will be retried. If it fails to commit after the maxmimum number of attempts
461+
* specified in transactionOptions, the transaction will fail.
462+
*
463+
* @param options The transaction options for controlling execution.
464+
* @param updateFunction The function to execute within the transaction context.
465+
* @return The task returned from the updateFunction.
466+
*/
467+
@NonNull
468+
public <TResult> Task<TResult> runTransaction(
469+
@NonNull TransactionOptions options, @NonNull Transaction.Function<TResult> updateFunction) {
451470
checkNotNull(updateFunction, "Provided transaction update function must not be null.");
452471
return runTransaction(
453-
updateFunction, com.google.firebase.firestore.core.Transaction.getDefaultExecutor());
472+
options,
473+
updateFunction,
474+
com.google.firebase.firestore.core.Transaction.getDefaultExecutor());
454475
}
455476

456477
/**
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
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+
import androidx.annotation.NonNull;
18+
19+
/**
20+
* Options to customize transaction behavior for {@link
21+
* FirebaseFirestore#runTransaction(TransactionOptions, Transaction.Function)}.
22+
*/
23+
public final class TransactionOptions {
24+
25+
static final TransactionOptions DEFAULT = new TransactionOptions.Builder().build();
26+
static final int DEFAULT_MAX_ATTEMPTS_COUNT = 5;
27+
28+
private final int maxAttempts;
29+
30+
private TransactionOptions(int maxAttempts) {
31+
this.maxAttempts = maxAttempts;
32+
}
33+
34+
/** A Builder for creating {@code TransactionOptions}. */
35+
public static final class Builder {
36+
private int maxAttempts = DEFAULT_MAX_ATTEMPTS_COUNT;
37+
38+
/** Constructs a new {@code TransactionOptions} Builder object. */
39+
public Builder() {}
40+
41+
/**
42+
* Constructs a new {@code TransactionOptions} Builder based on an existing {@code
43+
* TransactionOptions} object.
44+
*/
45+
public Builder(@NonNull TransactionOptions options) {
46+
maxAttempts = options.maxAttempts;
47+
}
48+
49+
/**
50+
* Set maximum number of attempts to commit, after which transaction fails.
51+
*
52+
* <p>The default value is 5. Setting the value to less than 1 will result in an {@link
53+
* IllegalArgumentException}.
54+
*
55+
* @return This builder
56+
*/
57+
@NonNull
58+
public Builder setMaxAttempts(int maxAttempts) {
59+
if (maxAttempts < 1) throw new IllegalArgumentException("Max attempts must be at least 1");
60+
this.maxAttempts = maxAttempts;
61+
return this;
62+
}
63+
64+
/**
65+
* Build the {@code TransactionOptions} object.
66+
*
67+
* @return The built {@code TransactionOptions} object
68+
*/
69+
@NonNull
70+
public TransactionOptions build() {
71+
return new TransactionOptions(maxAttempts);
72+
}
73+
}
74+
75+
/**
76+
* Get maximum number of attempts to commit, after which transaction fails. Default is 5.
77+
*
78+
* @return The maximum number of attempts
79+
*/
80+
public int getMaxAttempts() {
81+
return maxAttempts;
82+
}
83+
84+
@Override
85+
public boolean equals(Object o) {
86+
if (this == o) return true;
87+
if (o == null || getClass() != o.getClass()) return false;
88+
89+
TransactionOptions that = (TransactionOptions) o;
90+
91+
return maxAttempts == that.maxAttempts;
92+
}
93+
94+
@Override
95+
public int hashCode() {
96+
return maxAttempts;
97+
}
98+
99+
@Override
100+
public String toString() {
101+
return "TransactionOptions{" + "maxAttempts=" + maxAttempts + '}';
102+
}
103+
}

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)