|
14 | 14 |
|
15 | 15 | package com.google.firebase.firestore;
|
16 | 16 |
|
| 17 | +import static com.google.common.truth.Truth.assertWithMessage; |
17 | 18 | import static com.google.firebase.firestore.testutil.IntegrationTestUtil.isRunningAgainstEmulator;
|
18 | 19 | import static com.google.firebase.firestore.testutil.IntegrationTestUtil.nullList;
|
19 | 20 | import static com.google.firebase.firestore.testutil.IntegrationTestUtil.querySnapshotToIds;
|
|
30 | 31 | import static org.junit.Assert.assertFalse;
|
31 | 32 | import static org.junit.Assert.assertNull;
|
32 | 33 | import static org.junit.Assert.assertTrue;
|
33 |
| -import static org.junit.Assume.assumeFalse; |
34 | 34 | import static org.junit.Assume.assumeTrue;
|
35 | 35 |
|
36 | 36 | import androidx.test.ext.junit.runners.AndroidJUnit4;
|
37 | 37 | import com.google.android.gms.tasks.Task;
|
38 | 38 | import com.google.common.collect.Lists;
|
39 | 39 | import com.google.firebase.firestore.Query.Direction;
|
| 40 | +import com.google.firebase.firestore.remote.WatchChangeAggregatorTestingHooksAccessor; |
40 | 41 | import com.google.firebase.firestore.testutil.EventAccumulator;
|
41 | 42 | import com.google.firebase.firestore.testutil.IntegrationTestUtil;
|
42 | 43 | import java.util.ArrayList;
|
43 | 44 | import java.util.HashMap;
|
| 45 | +import java.util.HashSet; |
44 | 46 | import java.util.LinkedHashMap;
|
45 | 47 | import java.util.List;
|
46 | 48 | import java.util.Map;
|
@@ -1033,43 +1035,151 @@ public void testMultipleUpdatesWhileOffline() {
|
1033 | 1035 | }
|
1034 | 1036 |
|
1035 | 1037 | @Test
|
1036 |
| - public void resumingQueryShouldRemoveDeletedDocumentsIndicatedByExistenceFilter() |
1037 |
| - throws InterruptedException { |
1038 |
| - assumeFalse( |
1039 |
| - "Skip this test when running against the Firestore emulator as there is a bug related to " |
1040 |
| - + "sending existence filter in response: b/270731363.", |
1041 |
| - isRunningAgainstEmulator()); |
1042 |
| - |
| 1038 | + public void resumingAQueryShouldUseBloomFilterToAvoidFullRequery() throws Exception { |
| 1039 | + // Prepare the names and contents of the 100 documents to create. |
1043 | 1040 | Map<String, Map<String, Object>> testData = new HashMap<>();
|
1044 |
| - for (int i = 1; i <= 100; i++) { |
1045 |
| - testData.put("doc" + i, map("key", i)); |
| 1041 | + for (int i = 0; i < 100; i++) { |
| 1042 | + testData.put("doc" + (1000 + i), map("key", 42)); |
1046 | 1043 | }
|
1047 |
| - CollectionReference collection = testCollectionWithDocs(testData); |
1048 |
| - |
1049 |
| - // Populate the cache and save the resume token. |
1050 |
| - QuerySnapshot snapshot1 = waitFor(collection.get()); |
1051 |
| - assertEquals(snapshot1.size(), 100); |
1052 |
| - List<DocumentSnapshot> documents = snapshot1.getDocuments(); |
1053 | 1044 |
|
1054 |
| - // Delete 50 docs in transaction so that it doesn't affect local cache. |
1055 |
| - waitFor( |
1056 |
| - collection |
1057 |
| - .getFirestore() |
1058 |
| - .runTransaction( |
1059 |
| - transaction -> { |
1060 |
| - for (int i = 1; i <= 50; i++) { |
1061 |
| - DocumentReference docRef = documents.get(i).getReference(); |
1062 |
| - transaction.delete(docRef); |
1063 |
| - } |
1064 |
| - return null; |
1065 |
| - })); |
1066 |
| - |
1067 |
| - // Wait 10 seconds, during which Watch will stop tracking the query |
1068 |
| - // and will send an existence filter rather than "delete" events. |
1069 |
| - Thread.sleep(10000); |
1070 |
| - |
1071 |
| - QuerySnapshot snapshot2 = waitFor(collection.get()); |
1072 |
| - assertEquals(snapshot2.size(), 50); |
| 1045 | + // Each iteration of the "while" loop below runs a single iteration of the test. The test will |
| 1046 | + // be run multiple times only if a bloom filter false positive occurs. |
| 1047 | + int attemptNumber = 0; |
| 1048 | + while (true) { |
| 1049 | + attemptNumber++; |
| 1050 | + |
| 1051 | + // Create 100 documents in a new collection. |
| 1052 | + CollectionReference collection = testCollectionWithDocs(testData); |
| 1053 | + |
| 1054 | + // Run a query to populate the local cache with the 100 documents and a resume token. |
| 1055 | + List<DocumentReference> createdDocuments = new ArrayList<>(); |
| 1056 | + { |
| 1057 | + QuerySnapshot querySnapshot = waitFor(collection.get()); |
| 1058 | + assertWithMessage("querySnapshot1").that(querySnapshot.size()).isEqualTo(100); |
| 1059 | + for (DocumentSnapshot documentSnapshot : querySnapshot.getDocuments()) { |
| 1060 | + createdDocuments.add(documentSnapshot.getReference()); |
| 1061 | + } |
| 1062 | + } |
| 1063 | + |
| 1064 | + // Delete 50 of the 100 documents. Do this in a transaction, rather than |
| 1065 | + // DocumentReference.delete(), to avoid affecting the local cache. |
| 1066 | + HashSet<String> deletedDocumentIds = new HashSet<>(); |
| 1067 | + waitFor( |
| 1068 | + collection |
| 1069 | + .getFirestore() |
| 1070 | + .runTransaction( |
| 1071 | + transaction -> { |
| 1072 | + for (int i = 0; i < createdDocuments.size(); i += 2) { |
| 1073 | + DocumentReference documentToDelete = createdDocuments.get(i); |
| 1074 | + transaction.delete(documentToDelete); |
| 1075 | + deletedDocumentIds.add(documentToDelete.getId()); |
| 1076 | + } |
| 1077 | + return null; |
| 1078 | + })); |
| 1079 | + |
| 1080 | + // Wait for 10 seconds, during which Watch will stop tracking the query and will send an |
| 1081 | + // existence filter rather than "delete" events when the query is resumed. |
| 1082 | + Thread.sleep(10000); |
| 1083 | + |
| 1084 | + // Resume the query and save the resulting snapshot for verification. Use some internal |
| 1085 | + // testing hooks to "capture" the existence filter mismatches to verify that Watch sent a |
| 1086 | + // bloom filter, and it was used to avert a full requery. |
| 1087 | + QuerySnapshot snapshot2; |
| 1088 | + WatchChangeAggregatorTestingHooksAccessor.ExistenceFilterMismatchInfo |
| 1089 | + existenceFilterMismatchInfo; |
| 1090 | + WatchChangeAggregatorTestingHooksAccessor.ExistenceFilterMismatchAccumulator |
| 1091 | + existenceFilterMismatchAccumulator = |
| 1092 | + new WatchChangeAggregatorTestingHooksAccessor.ExistenceFilterMismatchAccumulator(); |
| 1093 | + existenceFilterMismatchAccumulator.register(); |
| 1094 | + try { |
| 1095 | + snapshot2 = waitFor(collection.get()); |
| 1096 | + // TODO(b/270731363): Remove the "if" condition below once the Firestore Emulator is fixed |
| 1097 | + // to send an existence filter. |
| 1098 | + if (isRunningAgainstEmulator()) { |
| 1099 | + existenceFilterMismatchInfo = null; |
| 1100 | + } else { |
| 1101 | + existenceFilterMismatchInfo = |
| 1102 | + existenceFilterMismatchAccumulator.waitForExistenceFilterMismatch( |
| 1103 | + /*timeoutMillis=*/ 5000); |
| 1104 | + } |
| 1105 | + } finally { |
| 1106 | + existenceFilterMismatchAccumulator.unregister(); |
| 1107 | + } |
| 1108 | + |
| 1109 | + // Verify that the snapshot from the resumed query contains the expected documents; that is, |
| 1110 | + // that it contains the 50 documents that were _not_ deleted. |
| 1111 | + // TODO(b/270731363): Remove the "if" condition below once the Firestore Emulator is fixed to |
| 1112 | + // send an existence filter. At the time of writing, the Firestore emulator fails to send an |
| 1113 | + // existence filter, resulting in the client including the deleted documents in the snapshot |
| 1114 | + // of the resumed query. |
| 1115 | + if (!(isRunningAgainstEmulator() && snapshot2.size() == 100)) { |
| 1116 | + HashSet<String> actualDocumentIds = new HashSet<>(); |
| 1117 | + for (DocumentSnapshot documentSnapshot : snapshot2.getDocuments()) { |
| 1118 | + actualDocumentIds.add(documentSnapshot.getId()); |
| 1119 | + } |
| 1120 | + HashSet<String> expectedDocumentIds = new HashSet<>(); |
| 1121 | + for (DocumentReference documentRef : createdDocuments) { |
| 1122 | + if (!deletedDocumentIds.contains(documentRef.getId())) { |
| 1123 | + expectedDocumentIds.add(documentRef.getId()); |
| 1124 | + } |
| 1125 | + } |
| 1126 | + assertWithMessage("snapshot2.docs") |
| 1127 | + .that(actualDocumentIds) |
| 1128 | + .containsExactlyElementsIn(expectedDocumentIds); |
| 1129 | + } |
| 1130 | + |
| 1131 | + // Skip the verification of the existence filter mismatch when testing against the Firestore |
| 1132 | + // emulator because the Firestore emulator does not include the `unchanged_names` bloom filter |
| 1133 | + // when it sends ExistenceFilter messages. Some day the emulator _may_ implement this logic, |
| 1134 | + // at which time this short-circuit can be removed. |
| 1135 | + if (isRunningAgainstEmulator()) { |
| 1136 | + return; |
| 1137 | + } |
| 1138 | + |
| 1139 | + // Verify that Watch sent an existence filter with the correct counts when the query was |
| 1140 | + // resumed. |
| 1141 | + assertWithMessage("Watch should have sent an existence filter") |
| 1142 | + .that(existenceFilterMismatchInfo) |
| 1143 | + .isNotNull(); |
| 1144 | + assertWithMessage("localCacheCount") |
| 1145 | + .that(existenceFilterMismatchInfo.localCacheCount()) |
| 1146 | + .isEqualTo(100); |
| 1147 | + assertWithMessage("existenceFilterCount") |
| 1148 | + .that(existenceFilterMismatchInfo.existenceFilterCount()) |
| 1149 | + .isEqualTo(50); |
| 1150 | + |
| 1151 | + // Skip the verification of the bloom filter when testing against production because the bloom |
| 1152 | + // filter is only implemented in nightly. |
| 1153 | + // TODO(b/271949433) Remove this "if" block once the bloom filter logic is deployed to |
| 1154 | + // production. |
| 1155 | + if (IntegrationTestUtil.getTargetBackend() != IntegrationTestUtil.TargetBackend.NIGHTLY) { |
| 1156 | + return; |
| 1157 | + } |
| 1158 | + |
| 1159 | + // Verify that Watch sent a valid bloom filter. |
| 1160 | + WatchChangeAggregatorTestingHooksAccessor.ExistenceFilterBloomFilterInfo bloomFilter = |
| 1161 | + existenceFilterMismatchInfo.bloomFilter(); |
| 1162 | + assertWithMessage("The bloom filter specified in the existence filter") |
| 1163 | + .that(bloomFilter) |
| 1164 | + .isNotNull(); |
| 1165 | + assertWithMessage("hashCount").that(bloomFilter.hashCount()).isGreaterThan(0); |
| 1166 | + assertWithMessage("bitmapLength").that(bloomFilter.bitmapLength()).isGreaterThan(0); |
| 1167 | + assertWithMessage("padding").that(bloomFilter.padding()).isGreaterThan(0); |
| 1168 | + assertWithMessage("padding").that(bloomFilter.padding()).isLessThan(8); |
| 1169 | + |
| 1170 | + // Verify that the bloom filter was successfully used to avert a full requery. If a false |
| 1171 | + // positive occurred then retry the entire test. Although statistically rare, false positives |
| 1172 | + // are expected to happen occasionally. When a false positive _does_ happen, just retry the |
| 1173 | + // test with a different set of documents. If that retry _also_ experiences a false positive, |
| 1174 | + // then fail the test because that is so improbable that something must have gone wrong. |
| 1175 | + if (attemptNumber == 1 && !bloomFilter.applied()) { |
| 1176 | + continue; |
| 1177 | + } |
| 1178 | + |
| 1179 | + assertWithMessage("bloom filter successfully applied with attemptNumber=" + attemptNumber) |
| 1180 | + .that(bloomFilter.applied()) |
| 1181 | + .isTrue(); |
| 1182 | + } |
1073 | 1183 | }
|
1074 | 1184 |
|
1075 | 1185 | @Test
|
|
0 commit comments