Skip to content

Commit 83d7652

Browse files
committed
Merge branch 'master' of github.com:firebase/firebase-android-sdk into ankita_fis
2 parents ce22251 + 576da00 commit 83d7652

File tree

5 files changed

+96
-17
lines changed

5 files changed

+96
-17
lines changed

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

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,7 @@ public void testUpdateTransactionally() {
375375
ArrayList<Task<Void>> readTasks = new ArrayList<>();
376376
// A barrier to make sure every transaction reaches the same spot.
377377
TaskCompletionSource<Void> barrier = new TaskCompletionSource<>();
378-
AtomicInteger started = new AtomicInteger(0);
378+
AtomicInteger counter = new AtomicInteger(0);
379379

380380
FirebaseFirestore firestore = testFirestore();
381381
DocumentReference doc = firestore.collection("counters").document();
@@ -388,9 +388,9 @@ public void testUpdateTransactionally() {
388388
transactionTasks.add(
389389
firestore.runTransaction(
390390
transaction -> {
391+
counter.incrementAndGet();
391392
DocumentSnapshot snapshot = transaction.get(doc);
392393
assertNotNull(snapshot);
393-
started.incrementAndGet();
394394
resolveRead.trySetResult(null);
395395
waitFor(barrier.getTask());
396396
transaction.update(doc, map("count", snapshot.getDouble("count") + 1.0));
@@ -400,10 +400,12 @@ public void testUpdateTransactionally() {
400400

401401
// Let all of the transactions fetch the old value and stop once.
402402
waitFor(Tasks.whenAll(readTasks));
403-
assertEquals(3, started.intValue());
404-
// Let all of the transactions continue and wait for them to finish.
403+
// There should be 3 initial transaction runs.
404+
assertEquals(3, counter.get());
405405
barrier.setResult(null);
406406
waitFor(Tasks.whenAll(transactionTasks));
407+
// There should be a maximum of 3 retries: once for the 2nd update, and twice for the 3rd update
408+
assertTrue(counter.get() <= 6);
407409
// Now all transaction should be completed, so check the result.
408410
DocumentSnapshot snapshot = waitFor(doc.get());
409411
assertEquals(8, snapshot.getDouble("count").intValue());
@@ -546,15 +548,17 @@ public void testReadingADocTwiceWithDifferentVersions() {
546548
FirebaseFirestore firestore = testFirestore();
547549
DocumentReference doc = firestore.collection("counters").document();
548550
waitFor(doc.set(map("count", 15.0)));
551+
AtomicInteger counter = new AtomicInteger(0);
549552
Exception e =
550553
waitForException(
551554
firestore.runTransaction(
552555
transaction -> {
556+
counter.incrementAndGet();
553557
// Get the doc once.
554558
DocumentSnapshot snapshot1 = transaction.get(doc);
555-
assertEquals(15, snapshot1.getDouble("count").intValue());
556-
// Do a write outside of the transaction.
557-
waitFor(doc.set(map("count", 1234.0)));
559+
// Do a write outside of the transaction. Because the transaction will retry, set
560+
// the document to a different value each time.
561+
waitFor(doc.set(map("count", 1234.0 + counter.get())));
558562
// Get the doc again in the transaction with the new version.
559563
DocumentSnapshot snapshot2 = transaction.get(doc);
560564
// The get itself will fail, because we already read an earlier version of this
@@ -563,8 +567,6 @@ public void testReadingADocTwiceWithDifferentVersions() {
563567
return null;
564568
}));
565569
assertEquals(Code.ABORTED, ((FirebaseFirestoreException) e).getCode());
566-
DocumentSnapshot snapshot = waitFor(doc.get());
567-
assertEquals(1234, snapshot.getDouble("count").intValue());
568570
}
569571

570572
@Test
@@ -622,6 +624,28 @@ public void testCannotHaveAGetWithoutMutations() {
622624
assertEquals("Every document read in a transaction must also be written.", e.getMessage());
623625
}
624626

627+
@Test
628+
public void testDoesNotRetryOnPermanentError() {
629+
final FirebaseFirestore firestore = testFirestore();
630+
AtomicInteger count = new AtomicInteger(0);
631+
// Make a transaction that should fail with a permanent error
632+
Task<Void> transactionTask =
633+
firestore.runTransaction(
634+
transaction -> {
635+
count.incrementAndGet();
636+
// Get and update a document that doesn't exist so that the transaction fails
637+
DocumentSnapshot doc =
638+
transaction.get(firestore.collection("nonexistent").document());
639+
transaction.update(doc.getReference(), "foo", "bar");
640+
return null;
641+
});
642+
643+
// Let all of the transactions fetch the old value and stop once.
644+
Exception e = waitForException(transactionTask);
645+
assertEquals(Code.INVALID_ARGUMENT, ((FirebaseFirestoreException) e).getCode());
646+
assertEquals(1, count.get());
647+
}
648+
625649
@Test
626650
public void testSuccessWithNoTransactionOperations() {
627651
FirebaseFirestore firestore = testFirestore();

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

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import com.google.common.base.Function;
2525
import com.google.firebase.database.collection.ImmutableSortedMap;
2626
import com.google.firebase.database.collection.ImmutableSortedSet;
27+
import com.google.firebase.firestore.FirebaseFirestoreException;
2728
import com.google.firebase.firestore.auth.User;
2829
import com.google.firebase.firestore.local.LocalStore;
2930
import com.google.firebase.firestore.local.LocalViewChanges;
@@ -38,6 +39,7 @@
3839
import com.google.firebase.firestore.model.SnapshotVersion;
3940
import com.google.firebase.firestore.model.mutation.Mutation;
4041
import com.google.firebase.firestore.model.mutation.MutationBatchResult;
42+
import com.google.firebase.firestore.remote.Datastore;
4143
import com.google.firebase.firestore.remote.RemoteEvent;
4244
import com.google.firebase.firestore.remote.RemoteStore;
4345
import com.google.firebase.firestore.remote.TargetChange;
@@ -262,6 +264,9 @@ public <TResult> Task<TResult> transaction(
262264
asyncQueue.getExecutor(),
263265
userTask -> {
264266
if (!userTask.isSuccessful()) {
267+
if (retries > 0 && isRetryableTransactionError(userTask.getException())) {
268+
return transaction(asyncQueue, updateFunction, retries - 1);
269+
}
265270
return userTask;
266271
}
267272
return transaction
@@ -272,11 +277,11 @@ public <TResult> Task<TResult> transaction(
272277
if (commitTask.isSuccessful()) {
273278
return Tasks.forResult(userTask.getResult());
274279
}
275-
// TODO: Only retry on real transaction failures.
276-
if (retries == 0) {
277-
return Tasks.forException(commitTask.getException());
280+
Exception e = commitTask.getException();
281+
if (retries > 0 && isRetryableTransactionError(e)) {
282+
return transaction(asyncQueue, updateFunction, retries - 1);
278283
}
279-
return transaction(asyncQueue, updateFunction, retries - 1);
284+
return Tasks.forException(e);
280285
});
281286
});
282287
}
@@ -589,4 +594,16 @@ private boolean errorIsInteresting(Status error) {
589594

590595
return false;
591596
}
597+
598+
private boolean isRetryableTransactionError(Exception e) {
599+
if (e instanceof FirebaseFirestoreException) {
600+
// In transactions, the backend will fail outdated reads with FAILED_PRECONDITION and
601+
// non-matching document versions with ABORTED. These errors should be retried.
602+
FirebaseFirestoreException.Code code = ((FirebaseFirestoreException) e).getCode();
603+
return code == FirebaseFirestoreException.Code.ABORTED
604+
|| code == FirebaseFirestoreException.Code.FAILED_PRECONDITION
605+
|| !Datastore.isPermanentError(((FirebaseFirestoreException) e).getCode());
606+
}
607+
return false;
608+
}
592609
}

firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteRemoteDocumentCache.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import static com.google.firebase.firestore.util.Assert.fail;
1818
import static com.google.firebase.firestore.util.Assert.hardAssert;
1919

20+
import com.google.firebase.Timestamp;
2021
import com.google.firebase.database.collection.ImmutableSortedMap;
2122
import com.google.firebase.firestore.core.Query;
2223
import com.google.firebase.firestore.model.Document;
@@ -51,13 +52,18 @@ final class SQLiteRemoteDocumentCache implements RemoteDocumentCache {
5152
@Override
5253
public void add(MaybeDocument maybeDocument) {
5354
String path = pathForKey(maybeDocument.getKey());
55+
Timestamp timestamp = maybeDocument.getVersion().getTimestamp();
5456
MessageLite message = serializer.encodeMaybeDocument(maybeDocument);
5557

5658
statsCollector.recordRowsWritten(STATS_TAG, 1);
5759

5860
db.execute(
59-
"INSERT OR REPLACE INTO remote_documents (path, contents) VALUES (?, ?)",
61+
"INSERT OR REPLACE INTO remote_documents "
62+
+ "(path, update_time_seconds, update_time_nanos, contents) "
63+
+ "VALUES (?, ?, ?, ?)",
6064
path,
65+
timestamp.getSeconds(),
66+
timestamp.getNanoseconds(),
6167
message.toByteArray());
6268

6369
db.getIndexManager().addToCollectionParentIndex(maybeDocument.getKey().getPath().popLast());

firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteSchema.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,16 @@ class SQLiteSchema {
4545
/**
4646
* The version of the schema. Increase this by one for each migration added to runMigrations
4747
* below.
48+
*
49+
* <p>TODO(index-free): The migration to schema version 9 doesn't backfill `update_time` as this
50+
* requires rewriting the RemoteDocumentCache. For index-free queries to efficiently handle
51+
* existing documents, we still need to populate update_time for all existing entries, drop the
52+
* RemoteDocumentCache or ask users to invoke `clearPersistence()` manually. If we decide to
53+
* backfill or drop the contents of the RemoteDocumentCache, we need to perform an additional
54+
* schema migration.
4855
*/
49-
static final int VERSION = 8;
56+
static final int VERSION = 9;
57+
5058
// Remove this constant and increment VERSION to enable indexing support
5159
static final int INDEXING_SUPPORT_VERSION = VERSION + 1;
5260

@@ -127,6 +135,10 @@ void runMigrations(int fromVersion, int toVersion) {
127135
createV8CollectionParentsIndex();
128136
}
129137

138+
if (fromVersion < 9 && toVersion >= 9) {
139+
addUpdateTime();
140+
}
141+
130142
/*
131143
* Adding a new migration? READ THIS FIRST!
132144
*
@@ -351,6 +363,16 @@ private void addSequenceNumber() {
351363
}
352364
}
353365

366+
private void addUpdateTime() {
367+
if (!tableContainsColumn("remote_documents", "update_time_seconds")) {
368+
hardAssert(
369+
!tableContainsColumn("remote_documents", "update_time_nanos"),
370+
"Table contained update_time_seconds, but is missing update_time_nanos");
371+
db.execSQL("ALTER TABLE remote_documents ADD COLUMN update_time_seconds INTEGER");
372+
db.execSQL("ALTER TABLE remote_documents ADD COLUMN update_time_nanos INTEGER");
373+
}
374+
}
375+
354376
/**
355377
* Ensures that each entry in the remote document cache has a corresponding sentinel row. Any
356378
* entries that lack a sentinel row are given one with the sequence number set to the highest

firebase-firestore/src/main/java/com/google/firebase/firestore/remote/Datastore.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,18 @@ public Task<List<MaybeDocument>> lookup(List<DocumentKey> keys) {
189189
* @see #isPermanentWriteError for classifying write errors.
190190
*/
191191
public static boolean isPermanentError(Status status) {
192+
return isPermanentError(FirebaseFirestoreException.Code.fromValue(status.getCode().value()));
193+
}
194+
195+
/**
196+
* Determines whether the given error code represents a permanent error when received in response
197+
* to a non-write operation.
198+
*
199+
* @see #isPermanentWriteError for classifying write errors.
200+
*/
201+
public static boolean isPermanentError(FirebaseFirestoreException.Code code) {
192202
// See go/firestore-client-errors
193-
switch (status.getCode()) {
203+
switch (code) {
194204
case OK:
195205
throw new IllegalArgumentException("Treated status OK as error");
196206
case CANCELLED:
@@ -217,7 +227,7 @@ public static boolean isPermanentError(Status status) {
217227
case DATA_LOSS:
218228
return true;
219229
default:
220-
throw new IllegalArgumentException("Unknown gRPC status code: " + status.getCode());
230+
throw new IllegalArgumentException("Unknown gRPC status code: " + code);
221231
}
222232
}
223233

0 commit comments

Comments
 (0)