Skip to content

Commit 1908508

Browse files
authored
Merge 3798032 into 87c1f02
2 parents 87c1f02 + 3798032 commit 1908508

File tree

2 files changed

+1533
-0
lines changed

2 files changed

+1533
-0
lines changed

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

+164
Original file line numberDiff line numberDiff line change
@@ -1165,6 +1165,170 @@ public void resumingAQueryShouldUseBloomFilterToAvoidFullRequery() throws Except
11651165
}
11661166
}
11671167

1168+
@Test
1169+
public void
1170+
bloomFilterShouldAvertAFullRequeryWhenDocumentsWereAddedDeletedRemovedUpdatedAndUnchangedSinceTheResumeToken()
1171+
throws Exception {
1172+
// TODO(b/291365820): Stop skipping this test when running against the Firestore emulator once
1173+
// the emulator is improved to include a bloom filter in the existence filter messages that it
1174+
// sends.
1175+
assumeFalse(
1176+
"Skip this test when running against the Firestore emulator because the emulator does not "
1177+
+ "include a bloom filter when it sends existence filter messages, making it "
1178+
+ "impossible for this test to verify the correctness of the bloom filter.",
1179+
isRunningAgainstEmulator());
1180+
1181+
// Prepare the names and contents of the 20 documents to create.
1182+
Map<String, Map<String, Object>> testData = new HashMap<>();
1183+
for (int i = 0; i < 20; i++) {
1184+
testData.put("doc" + (1000 + i), map("key", 42, "removed", false));
1185+
}
1186+
1187+
// Each iteration of the "while" loop below runs a single iteration of the test. The test will
1188+
// be run multiple times only if a bloom filter false positive occurs.
1189+
int attemptNumber = 0;
1190+
while (true) {
1191+
attemptNumber++;
1192+
1193+
// Create 20 documents in a new collection.
1194+
CollectionReference collection = testCollectionWithDocs(testData);
1195+
Query query = collection.whereEqualTo("removed", false);
1196+
1197+
// Run a query to populate the local cache with the 20 documents and a resume token.
1198+
List<DocumentReference> createdDocuments = new ArrayList<>();
1199+
{
1200+
QuerySnapshot querySnapshot = waitFor(query.get());
1201+
assertWithMessage("querySnapshot1").that(querySnapshot.size()).isEqualTo(20);
1202+
for (DocumentSnapshot documentSnapshot : querySnapshot.getDocuments()) {
1203+
createdDocuments.add(documentSnapshot.getReference());
1204+
}
1205+
}
1206+
assertWithMessage("createdDocuments").that(createdDocuments).hasSize(20);
1207+
1208+
// Out of the 20 existing documents, leave 5 docs untouched, delete 5 docs, remove 5 docs,
1209+
// update 5 docs, and add 15 new docs.
1210+
HashSet<String> deletedDocumentIds = new HashSet<>();
1211+
HashSet<String> removedDocumentIds = new HashSet<>();
1212+
HashSet<String> updatedDocumentIds = new HashSet<>();
1213+
HashSet<String> addedDocumentIds = new HashSet<>();
1214+
1215+
{
1216+
FirebaseFirestore db2 = testFirestore();
1217+
WriteBatch batch = db2.batch();
1218+
1219+
for (int i = 0; i < createdDocuments.size(); i += 4) {
1220+
DocumentReference documentToDelete = db2.document(createdDocuments.get(i).getPath());
1221+
batch.delete(documentToDelete);
1222+
deletedDocumentIds.add(documentToDelete.getId());
1223+
}
1224+
assertWithMessage("deletedDocumentIds").that(deletedDocumentIds).hasSize(5);
1225+
1226+
// Update 5 documents to no longer match the query.
1227+
for (int i = 1; i < createdDocuments.size(); i += 4) {
1228+
DocumentReference documentToRemove = db2.document(createdDocuments.get(i).getPath());
1229+
batch.update(documentToRemove, map("removed", true));
1230+
removedDocumentIds.add(documentToRemove.getId());
1231+
}
1232+
assertWithMessage("removedDocumentIds").that(removedDocumentIds).hasSize(5);
1233+
1234+
// Update 5 documents, but ensure they still match the query.
1235+
for (int i = 2; i < createdDocuments.size(); i += 4) {
1236+
DocumentReference documentToUpdate = db2.document(createdDocuments.get(i).getPath());
1237+
batch.update(documentToUpdate, map("key", 43));
1238+
updatedDocumentIds.add(documentToUpdate.getId());
1239+
}
1240+
assertWithMessage("updatedDocumentIds").that(updatedDocumentIds).hasSize(5);
1241+
1242+
for (int i = 0; i < 15; i += 1) {
1243+
DocumentReference documentToUpdate =
1244+
db2.document(collection.getPath() + "/newDoc" + (1000 + i));
1245+
batch.set(documentToUpdate, map("key", 42, "removed", false));
1246+
addedDocumentIds.add(documentToUpdate.getId());
1247+
}
1248+
1249+
// Ensure the sets above are disjoint.
1250+
HashSet<String> mergedSet = new HashSet<>();
1251+
mergedSet.addAll(deletedDocumentIds);
1252+
mergedSet.addAll(removedDocumentIds);
1253+
mergedSet.addAll(updatedDocumentIds);
1254+
mergedSet.addAll(addedDocumentIds);
1255+
assertWithMessage("mergedSet").that(mergedSet).hasSize(30);
1256+
1257+
waitFor(batch.commit());
1258+
}
1259+
1260+
// Wait for 10 seconds, during which Watch will stop tracking the query and will send an
1261+
// existence filter rather than "delete" events when the query is resumed.
1262+
Thread.sleep(10000);
1263+
1264+
// Resume the query and save the resulting snapshot for verification. Use some internal
1265+
// testing hooks to "capture" the existence filter mismatches to verify that Watch sent a
1266+
// bloom filter, and it was used to avert a full requery.
1267+
AtomicReference<QuerySnapshot> snapshot2Ref = new AtomicReference<>();
1268+
ArrayList<ExistenceFilterMismatchInfo> existenceFilterMismatches =
1269+
captureExistenceFilterMismatches(
1270+
() -> {
1271+
QuerySnapshot querySnapshot = waitFor(query.get());
1272+
snapshot2Ref.set(querySnapshot);
1273+
});
1274+
QuerySnapshot snapshot2 = snapshot2Ref.get();
1275+
1276+
// Verify that the snapshot from the resumed query contains the expected documents; that is,
1277+
// 10 existing documents that still match the query, and 15 documents that are newly added.
1278+
HashSet<String> actualDocumentIds = new HashSet<>();
1279+
for (DocumentSnapshot documentSnapshot : snapshot2.getDocuments()) {
1280+
actualDocumentIds.add(documentSnapshot.getId());
1281+
}
1282+
HashSet<String> expectedDocumentIds = new HashSet<>();
1283+
for (DocumentReference documentRef : createdDocuments) {
1284+
if (!deletedDocumentIds.contains(documentRef.getId())
1285+
&& !removedDocumentIds.contains(documentRef.getId())) {
1286+
expectedDocumentIds.add(documentRef.getId());
1287+
}
1288+
}
1289+
expectedDocumentIds.addAll(addedDocumentIds);
1290+
assertWithMessage("snapshot2.docs")
1291+
.that(actualDocumentIds)
1292+
.containsExactlyElementsIn(expectedDocumentIds);
1293+
assertWithMessage("actualDocumentIds").that(actualDocumentIds).hasSize(25);
1294+
1295+
// Verify that Watch sent an existence filter with the correct counts when the query was
1296+
// resumed.
1297+
assertWithMessage("Watch should have sent exactly 1 existence filter")
1298+
.that(existenceFilterMismatches)
1299+
.hasSize(1);
1300+
ExistenceFilterMismatchInfo existenceFilterMismatchInfo = existenceFilterMismatches.get(0);
1301+
assertWithMessage("localCacheCount")
1302+
.that(existenceFilterMismatchInfo.localCacheCount())
1303+
.isEqualTo(35);
1304+
assertWithMessage("existenceFilterCount")
1305+
.that(existenceFilterMismatchInfo.existenceFilterCount())
1306+
.isEqualTo(25);
1307+
1308+
// Verify that Watch sent a valid bloom filter.
1309+
ExistenceFilterBloomFilterInfo bloomFilter = existenceFilterMismatchInfo.bloomFilter();
1310+
assertWithMessage("The bloom filter specified in the existence filter")
1311+
.that(bloomFilter)
1312+
.isNotNull();
1313+
1314+
// Verify that the bloom filter was successfully used to avert a full requery. If a false
1315+
// positive occurred then retry the entire test. Although statistically rare, false positives
1316+
// are expected to happen occasionally. When a false positive _does_ happen, just retry the
1317+
// test with a different set of documents. If that retry _also_ experiences a false positive,
1318+
// then fail the test because that is so improbable that something must have gone wrong.
1319+
if (attemptNumber == 1 && !bloomFilter.applied()) {
1320+
continue;
1321+
}
1322+
1323+
assertWithMessage("bloom filter successfully applied with attemptNumber=" + attemptNumber)
1324+
.that(bloomFilter.applied())
1325+
.isTrue();
1326+
1327+
// Break out of the test loop now that the test passes.
1328+
break;
1329+
}
1330+
}
1331+
11681332
private static String unicodeNormalize(String s) {
11691333
return Normalizer.normalize(s, Normalizer.Form.NFC);
11701334
}

0 commit comments

Comments
 (0)