|
38 | 38 | import com.google.android.gms.tasks.Task;
|
39 | 39 | import com.google.common.collect.Lists;
|
40 | 40 | import com.google.firebase.firestore.Query.Direction;
|
| 41 | +import com.google.firebase.firestore.remote.TestingHooksUtil.ExistenceFilterBloomFilterInfo; |
41 | 42 | import com.google.firebase.firestore.remote.TestingHooksUtil.ExistenceFilterMismatchInfo;
|
42 | 43 | import com.google.firebase.firestore.testutil.EventAccumulator;
|
43 | 44 | import com.google.firebase.firestore.testutil.IntegrationTestUtil;
|
@@ -1037,101 +1038,135 @@ public void testMultipleUpdatesWhileOffline() {
|
1037 | 1038 | }
|
1038 | 1039 |
|
1039 | 1040 | @Test
|
1040 |
| - public void resumingAQueryShouldUseExistenceFilterToDetectDeletes() throws Exception { |
| 1041 | + public void resumingAQueryShouldUseBloomFilterToAvoidFullRequery() throws Exception { |
1041 | 1042 | // Prepare the names and contents of the 100 documents to create.
|
1042 | 1043 | Map<String, Map<String, Object>> testData = new HashMap<>();
|
1043 | 1044 | for (int i = 0; i < 100; i++) {
|
1044 | 1045 | testData.put("doc" + (1000 + i), map("key", 42));
|
1045 | 1046 | }
|
1046 | 1047 |
|
1047 |
| - // Create 100 documents in a new collection. |
1048 |
| - CollectionReference collection = testCollectionWithDocs(testData); |
1049 |
| - |
1050 |
| - // Run a query to populate the local cache with the 100 documents and a resume token. |
1051 |
| - List<DocumentReference> createdDocuments = new ArrayList<>(); |
1052 |
| - { |
1053 |
| - QuerySnapshot querySnapshot = waitFor(collection.get()); |
1054 |
| - assertWithMessage("querySnapshot1").that(querySnapshot.size()).isEqualTo(100); |
1055 |
| - for (DocumentSnapshot documentSnapshot : querySnapshot.getDocuments()) { |
1056 |
| - createdDocuments.add(documentSnapshot.getReference()); |
| 1048 | + // Each iteration of the "while" loop below runs a single iteration of the test. The test will |
| 1049 | + // be run multiple times only if a bloom filter false positive occurs. |
| 1050 | + int attemptNumber = 0; |
| 1051 | + while (true) { |
| 1052 | + attemptNumber++; |
| 1053 | + |
| 1054 | + // Create 100 documents in a new collection. |
| 1055 | + CollectionReference collection = testCollectionWithDocs(testData); |
| 1056 | + |
| 1057 | + // Run a query to populate the local cache with the 100 documents and a resume token. |
| 1058 | + List<DocumentReference> createdDocuments = new ArrayList<>(); |
| 1059 | + { |
| 1060 | + QuerySnapshot querySnapshot = waitFor(collection.get()); |
| 1061 | + assertWithMessage("querySnapshot1").that(querySnapshot.size()).isEqualTo(100); |
| 1062 | + for (DocumentSnapshot documentSnapshot : querySnapshot.getDocuments()) { |
| 1063 | + createdDocuments.add(documentSnapshot.getReference()); |
| 1064 | + } |
| 1065 | + } |
| 1066 | + assertWithMessage("createdDocuments").that(createdDocuments).hasSize(100); |
| 1067 | + |
| 1068 | + // Delete 50 of the 100 documents. Do this in a transaction, rather than |
| 1069 | + // DocumentReference.delete(), to avoid affecting the local cache. |
| 1070 | + HashSet<String> deletedDocumentIds = new HashSet<>(); |
| 1071 | + waitFor( |
| 1072 | + collection |
| 1073 | + .getFirestore() |
| 1074 | + .runTransaction( |
| 1075 | + transaction -> { |
| 1076 | + for (int i = 0; i < createdDocuments.size(); i += 2) { |
| 1077 | + DocumentReference documentToDelete = createdDocuments.get(i); |
| 1078 | + transaction.delete(documentToDelete); |
| 1079 | + deletedDocumentIds.add(documentToDelete.getId()); |
| 1080 | + } |
| 1081 | + return null; |
| 1082 | + })); |
| 1083 | + assertWithMessage("deletedDocumentIds").that(deletedDocumentIds).hasSize(50); |
| 1084 | + |
| 1085 | + // Wait for 10 seconds, during which Watch will stop tracking the query and will send an |
| 1086 | + // existence filter rather than "delete" events when the query is resumed. |
| 1087 | + Thread.sleep(10000); |
| 1088 | + |
| 1089 | + // Resume the query and save the resulting snapshot for verification. Use some internal |
| 1090 | + // testing hooks to "capture" the existence filter mismatches to verify that Watch sent a |
| 1091 | + // bloom filter, and it was used to avert a full requery. |
| 1092 | + AtomicReference<QuerySnapshot> snapshot2Ref = new AtomicReference<>(); |
| 1093 | + ArrayList<ExistenceFilterMismatchInfo> existenceFilterMismatches = |
| 1094 | + captureExistenceFilterMismatches( |
| 1095 | + () -> { |
| 1096 | + QuerySnapshot querySnapshot = waitFor(collection.get()); |
| 1097 | + snapshot2Ref.set(querySnapshot); |
| 1098 | + }); |
| 1099 | + QuerySnapshot snapshot2 = snapshot2Ref.get(); |
| 1100 | + |
| 1101 | + // Verify that the snapshot from the resumed query contains the expected documents; that is, |
| 1102 | + // that it contains the 50 documents that were _not_ deleted. |
| 1103 | + // TODO(b/270731363): Remove the "if" condition below once the Firestore Emulator is fixed to |
| 1104 | + // send an existence filter. At the time of writing, the Firestore emulator fails to send an |
| 1105 | + // existence filter, resulting in the client including the deleted documents in the snapshot |
| 1106 | + // of the resumed query. |
| 1107 | + if (!(isRunningAgainstEmulator() && snapshot2.size() == 100)) { |
| 1108 | + HashSet<String> actualDocumentIds = new HashSet<>(); |
| 1109 | + for (DocumentSnapshot documentSnapshot : snapshot2.getDocuments()) { |
| 1110 | + actualDocumentIds.add(documentSnapshot.getId()); |
| 1111 | + } |
| 1112 | + HashSet<String> expectedDocumentIds = new HashSet<>(); |
| 1113 | + for (DocumentReference documentRef : createdDocuments) { |
| 1114 | + if (!deletedDocumentIds.contains(documentRef.getId())) { |
| 1115 | + expectedDocumentIds.add(documentRef.getId()); |
| 1116 | + } |
| 1117 | + } |
| 1118 | + assertWithMessage("snapshot2.docs") |
| 1119 | + .that(actualDocumentIds) |
| 1120 | + .containsExactlyElementsIn(expectedDocumentIds); |
1057 | 1121 | }
|
1058 |
| - } |
1059 |
| - assertWithMessage("createdDocuments").that(createdDocuments).hasSize(100); |
1060 | 1122 |
|
1061 |
| - // Delete 50 of the 100 documents. Do this in a transaction, rather than |
1062 |
| - // DocumentReference.delete(), to avoid affecting the local cache. |
1063 |
| - HashSet<String> deletedDocumentIds = new HashSet<>(); |
1064 |
| - waitFor( |
1065 |
| - collection |
1066 |
| - .getFirestore() |
1067 |
| - .runTransaction( |
1068 |
| - transaction -> { |
1069 |
| - for (int i = 0; i < createdDocuments.size(); i += 2) { |
1070 |
| - DocumentReference documentToDelete = createdDocuments.get(i); |
1071 |
| - transaction.delete(documentToDelete); |
1072 |
| - deletedDocumentIds.add(documentToDelete.getId()); |
1073 |
| - } |
1074 |
| - return null; |
1075 |
| - })); |
1076 |
| - assertWithMessage("deletedDocumentIds").that(deletedDocumentIds).hasSize(50); |
1077 |
| - |
1078 |
| - // Wait for 10 seconds, during which Watch will stop tracking the query and will send an |
1079 |
| - // existence filter rather than "delete" events when the query is resumed. |
1080 |
| - Thread.sleep(10000); |
1081 |
| - |
1082 |
| - // Resume the query and save the resulting snapshot for verification. Use some internal testing |
1083 |
| - // hooks to "capture" the existence filter mismatches to verify them. |
1084 |
| - AtomicReference<QuerySnapshot> snapshot2Ref = new AtomicReference<>(); |
1085 |
| - ArrayList<ExistenceFilterMismatchInfo> existenceFilterMismatches = |
1086 |
| - captureExistenceFilterMismatches( |
1087 |
| - () -> { |
1088 |
| - QuerySnapshot querySnapshot = waitFor(collection.get()); |
1089 |
| - snapshot2Ref.set(querySnapshot); |
1090 |
| - }); |
1091 |
| - QuerySnapshot snapshot2 = snapshot2Ref.get(); |
1092 |
| - |
1093 |
| - // Verify that the snapshot from the resumed query contains the expected documents; that is, |
1094 |
| - // that it contains the 50 documents that were _not_ deleted. |
1095 |
| - // TODO(b/270731363): Remove the "if" condition below once the Firestore Emulator is fixed to |
1096 |
| - // send an existence filter. At the time of writing, the Firestore emulator fails to send an |
1097 |
| - // existence filter, resulting in the client including the deleted documents in the snapshot |
1098 |
| - // of the resumed query. |
1099 |
| - if (!(isRunningAgainstEmulator() && snapshot2.size() == 100)) { |
1100 |
| - HashSet<String> actualDocumentIds = new HashSet<>(); |
1101 |
| - for (DocumentSnapshot documentSnapshot : snapshot2.getDocuments()) { |
1102 |
| - actualDocumentIds.add(documentSnapshot.getId()); |
| 1123 | + // Skip the verification of the existence filter mismatch when testing against the Firestore |
| 1124 | + // emulator because the Firestore emulator does not include the `unchanged_names` bloom filter |
| 1125 | + // when it sends ExistenceFilter messages. Some day the emulator _may_ implement this logic, |
| 1126 | + // at which time this short-circuit can be removed. |
| 1127 | + if (isRunningAgainstEmulator()) { |
| 1128 | + return; |
1103 | 1129 | }
|
1104 |
| - HashSet<String> expectedDocumentIds = new HashSet<>(); |
1105 |
| - for (DocumentReference documentRef : createdDocuments) { |
1106 |
| - if (!deletedDocumentIds.contains(documentRef.getId())) { |
1107 |
| - expectedDocumentIds.add(documentRef.getId()); |
1108 |
| - } |
| 1130 | + |
| 1131 | + // Verify that Watch sent an existence filter with the correct counts when the query was |
| 1132 | + // resumed. |
| 1133 | + assertWithMessage("Watch should have sent exactly 1 existence filter") |
| 1134 | + .that(existenceFilterMismatches) |
| 1135 | + .hasSize(1); |
| 1136 | + ExistenceFilterMismatchInfo existenceFilterMismatchInfo = existenceFilterMismatches.get(0); |
| 1137 | + assertWithMessage("localCacheCount") |
| 1138 | + .that(existenceFilterMismatchInfo.localCacheCount()) |
| 1139 | + .isEqualTo(100); |
| 1140 | + assertWithMessage("existenceFilterCount") |
| 1141 | + .that(existenceFilterMismatchInfo.existenceFilterCount()) |
| 1142 | + .isEqualTo(50); |
| 1143 | + |
| 1144 | + // Verify that Watch sent a valid bloom filter. |
| 1145 | + ExistenceFilterBloomFilterInfo bloomFilter = existenceFilterMismatchInfo.bloomFilter(); |
| 1146 | + assertWithMessage("The bloom filter specified in the existence filter") |
| 1147 | + .that(bloomFilter) |
| 1148 | + .isNotNull(); |
| 1149 | + assertWithMessage("hashCount").that(bloomFilter.hashCount()).isGreaterThan(0); |
| 1150 | + assertWithMessage("bitmapLength").that(bloomFilter.bitmapLength()).isGreaterThan(0); |
| 1151 | + assertWithMessage("padding").that(bloomFilter.padding()).isGreaterThan(0); |
| 1152 | + assertWithMessage("padding").that(bloomFilter.padding()).isLessThan(8); |
| 1153 | + |
| 1154 | + // Verify that the bloom filter was successfully used to avert a full requery. If a false |
| 1155 | + // positive occurred then retry the entire test. Although statistically rare, false positives |
| 1156 | + // are expected to happen occasionally. When a false positive _does_ happen, just retry the |
| 1157 | + // test with a different set of documents. If that retry _also_ experiences a false positive, |
| 1158 | + // then fail the test because that is so improbable that something must have gone wrong. |
| 1159 | + if (attemptNumber == 1 && !bloomFilter.applied()) { |
| 1160 | + continue; |
1109 | 1161 | }
|
1110 |
| - assertWithMessage("snapshot2.docs") |
1111 |
| - .that(actualDocumentIds) |
1112 |
| - .containsExactlyElementsIn(expectedDocumentIds); |
1113 |
| - } |
1114 | 1162 |
|
1115 |
| - // Skip the verification of the existence filter mismatch when testing against the Firestore |
1116 |
| - // emulator because the Firestore emulator fails to to send an existence filter at all. |
1117 |
| - // TODO(b/270731363): Enable the verification of the existence filter mismatch once the |
1118 |
| - // Firestore emulator is fixed to send an existence filter. |
1119 |
| - if (isRunningAgainstEmulator()) { |
1120 |
| - return; |
1121 |
| - } |
| 1163 | + assertWithMessage("bloom filter successfully applied with attemptNumber=" + attemptNumber) |
| 1164 | + .that(bloomFilter.applied()) |
| 1165 | + .isTrue(); |
1122 | 1166 |
|
1123 |
| - // Verify that Watch sent an existence filter with the correct counts when the query was |
1124 |
| - // resumed. |
1125 |
| - assertWithMessage("Watch should have sent exactly 1 existence filter") |
1126 |
| - .that(existenceFilterMismatches) |
1127 |
| - .hasSize(1); |
1128 |
| - ExistenceFilterMismatchInfo existenceFilterMismatchInfo = existenceFilterMismatches.get(0); |
1129 |
| - assertWithMessage("localCacheCount") |
1130 |
| - .that(existenceFilterMismatchInfo.localCacheCount()) |
1131 |
| - .isEqualTo(100); |
1132 |
| - assertWithMessage("existenceFilterCount") |
1133 |
| - .that(existenceFilterMismatchInfo.existenceFilterCount()) |
1134 |
| - .isEqualTo(50); |
| 1167 | + // Break out of the test loop now that the test passes. |
| 1168 | + break; |
| 1169 | + } |
1135 | 1170 | }
|
1136 | 1171 |
|
1137 | 1172 | @Test
|
|
0 commit comments