@@ -1165,6 +1165,170 @@ public void resumingAQueryShouldUseBloomFilterToAvoidFullRequery() throws Except
1165
1165
}
1166
1166
}
1167
1167
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
+
1168
1332
private static String unicodeNormalize (String s ) {
1169
1333
return Normalizer .normalize (s , Normalizer .Form .NFC );
1170
1334
}
0 commit comments