@@ -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