Skip to content

Commit 1743194

Browse files
authored
Merge be2052b into 671c879
2 parents 671c879 + be2052b commit 1743194

File tree

84 files changed

+4507
-422
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

84 files changed

+4507
-422
lines changed

Firestore/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
# Unreleased
2+
- [feature] Implemented an optimization in the local cache synchronization logic
3+
that reduces the number of billed document reads when documents were deleted
4+
on the server while the client was not actively listening to the query
5+
(e.g. while the client was offline). (#INSERT_PR_NUMBER_HERE)
6+
17
# 10.11.0
28
- [feature] Expose MultiDb API for public preview. (#10465)
39
- [fixed] Fixed a compilation warning related to integer casting. (#11332)

Firestore/Example/Firestore.xcodeproj/project.pbxproj

Lines changed: 407 additions & 3 deletions
Large diffs are not rendered by default.

Firestore/Example/Tests/Integration/API/FIRQueryTests.mm

Lines changed: 126 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1192,7 +1192,10 @@ - (void)testOrderByEquality {
11921192
matchesResult:@[ @"doc6", @"doc3" ]];
11931193
}
11941194

1195-
- (void)testResumingAQueryShouldUseExistenceFilterToDetectDeletes {
1195+
- (void)testResumingAQueryShouldUseBloomFilterToAvoidFullRequery {
1196+
using firebase::firestore::testutil::CaptureExistenceFilterMismatches;
1197+
using firebase::firestore::util::TestingHooks;
1198+
11961199
// Set this test to stop when the first failure occurs because some test assertion failures make
11971200
// the rest of the test not applicable or will even crash.
11981201
[self setContinueAfterFailure:NO];
@@ -1204,100 +1207,135 @@ - (void)testResumingAQueryShouldUseExistenceFilterToDetectDeletes {
12041207
[testDocs setValue:@{@"key" : @42} forKey:[NSString stringWithFormat:@"doc%@", @(1000 + i)]];
12051208
}
12061209

1207-
// Create 100 documents in a new collection.
1208-
FIRCollectionReference *collRef = [self collectionRefWithDocuments:testDocs];
1209-
1210-
// Run a query to populate the local cache with the 100 documents and a resume token.
1211-
FIRQuerySnapshot *querySnapshot1 = [self readDocumentSetForRef:collRef
1212-
source:FIRFirestoreSourceDefault];
1213-
XCTAssertEqual(querySnapshot1.count, 100, @"querySnapshot1.count has an unexpected value");
1214-
NSArray<FIRDocumentReference *> *createdDocuments =
1215-
FIRDocumentReferenceArrayFromQuerySnapshot(querySnapshot1);
1216-
1217-
// Delete 50 of the 100 documents. Do this in a transaction, rather than
1218-
// [FIRDocumentReference deleteDocument], to avoid affecting the local cache.
1219-
NSSet<NSString *> *deletedDocumentIds;
1220-
{
1221-
NSMutableArray<NSString *> *deletedDocumentIdsAccumulator = [[NSMutableArray alloc] init];
1222-
XCTestExpectation *expectation = [self expectationWithDescription:@"DeleteTransaction"];
1223-
[collRef.firestore
1224-
runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **) {
1225-
for (decltype(createdDocuments.count) i = 0; i < createdDocuments.count; i += 2) {
1226-
FIRDocumentReference *documentToDelete = createdDocuments[i];
1227-
[transaction deleteDocument:documentToDelete];
1228-
[deletedDocumentIdsAccumulator addObject:documentToDelete.documentID];
1229-
}
1230-
return @"document deletion successful";
1231-
}
1232-
completion:^(id, NSError *) {
1233-
[expectation fulfill];
1234-
}];
1235-
[self awaitExpectation:expectation];
1236-
deletedDocumentIds = [NSSet setWithArray:deletedDocumentIdsAccumulator];
1237-
}
1238-
XCTAssertEqual(deletedDocumentIds.count, 50u, @"deletedDocumentIds has the wrong size");
1239-
1240-
// Wait for 10 seconds, during which Watch will stop tracking the query and will send an existence
1241-
// filter rather than "delete" events when the query is resumed.
1242-
[NSThread sleepForTimeInterval:10.0f];
1243-
1244-
// Resume the query and save the resulting snapshot for verification.
1245-
// Use some internal testing hooks to "capture" the existence filter mismatches to verify them.
1246-
FIRQuerySnapshot *querySnapshot2;
1247-
std::vector<firebase::firestore::util::TestingHooks::ExistenceFilterMismatchInfo>
1248-
existence_filter_mismatches =
1249-
firebase::firestore::testutil::CaptureExistenceFilterMismatches([&] {
1250-
querySnapshot2 = [self readDocumentSetForRef:collRef source:FIRFirestoreSourceDefault];
1251-
});
1252-
1253-
// Verify that the snapshot from the resumed query contains the expected documents; that is,
1254-
// that it contains the 50 documents that were _not_ deleted.
1255-
// TODO(b/270731363): Remove the "if" condition below once the Firestore Emulator is fixed to
1256-
// send an existence filter. At the time of writing, the Firestore emulator fails to send an
1257-
// existence filter, resulting in the client including the deleted documents in the snapshot
1258-
// of the resumed query.
1259-
if (!([FSTIntegrationTestCase isRunningAgainstEmulator] && querySnapshot2.count == 100)) {
1260-
NSSet<NSString *> *actualDocumentIds =
1261-
[NSSet setWithArray:FIRQuerySnapshotGetIDs(querySnapshot2)];
1262-
NSSet<NSString *> *expectedDocumentIds;
1210+
// Each iteration of the "while" loop below runs a single iteration of the test. The test will
1211+
// be run multiple times only if a bloom filter false positive occurs.
1212+
int attemptNumber = 0;
1213+
while (true) {
1214+
attemptNumber++;
1215+
1216+
// Create 100 documents in a new collection.
1217+
FIRCollectionReference *collRef = [self collectionRefWithDocuments:testDocs];
1218+
1219+
// Run a query to populate the local cache with the 100 documents and a resume token.
1220+
FIRQuerySnapshot *querySnapshot1 = [self readDocumentSetForRef:collRef
1221+
source:FIRFirestoreSourceDefault];
1222+
XCTAssertEqual(querySnapshot1.count, 100, @"querySnapshot1.count has an unexpected value");
1223+
NSArray<FIRDocumentReference *> *createdDocuments =
1224+
FIRDocumentReferenceArrayFromQuerySnapshot(querySnapshot1);
1225+
1226+
// Delete 50 of the 100 documents. Do this in a transaction, rather than
1227+
// [FIRDocumentReference deleteDocument], to avoid affecting the local cache.
1228+
NSSet<NSString *> *deletedDocumentIds;
12631229
{
1264-
NSMutableArray<NSString *> *expectedDocumentIdsAccumulator = [[NSMutableArray alloc] init];
1265-
for (FIRDocumentReference *documentRef in createdDocuments) {
1266-
if (![deletedDocumentIds containsObject:documentRef.documentID]) {
1267-
[expectedDocumentIdsAccumulator addObject:documentRef.documentID];
1230+
NSMutableArray<NSString *> *deletedDocumentIdsAccumulator = [[NSMutableArray alloc] init];
1231+
XCTestExpectation *expectation = [self expectationWithDescription:@"DeleteTransaction"];
1232+
[collRef.firestore
1233+
runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **) {
1234+
for (decltype(createdDocuments.count) i = 0; i < createdDocuments.count; i += 2) {
1235+
FIRDocumentReference *documentToDelete = createdDocuments[i];
1236+
[transaction deleteDocument:documentToDelete];
1237+
[deletedDocumentIdsAccumulator addObject:documentToDelete.documentID];
1238+
}
1239+
return @"document deletion successful";
1240+
}
1241+
completion:^(id, NSError *) {
1242+
[expectation fulfill];
1243+
}];
1244+
[self awaitExpectation:expectation];
1245+
deletedDocumentIds = [NSSet setWithArray:deletedDocumentIdsAccumulator];
1246+
}
1247+
XCTAssertEqual(deletedDocumentIds.count, 50u, @"deletedDocumentIds has the wrong size");
1248+
1249+
// Wait for 10 seconds, during which Watch will stop tracking the query and will send an
1250+
// existence filter rather than "delete" events when the query is resumed.
1251+
[NSThread sleepForTimeInterval:10.0f];
1252+
1253+
// Resume the query and save the resulting snapshot for verification.
1254+
// Use some internal testing hooks to "capture" the existence filter mismatches to verify that
1255+
// Watch sent a bloom filter, and it was used to avert a full requery.
1256+
FIRQuerySnapshot *querySnapshot2;
1257+
std::vector<TestingHooks::ExistenceFilterMismatchInfo> existence_filter_mismatches =
1258+
CaptureExistenceFilterMismatches([&] {
1259+
querySnapshot2 = [self readDocumentSetForRef:collRef source:FIRFirestoreSourceDefault];
1260+
});
1261+
1262+
// Verify that the snapshot from the resumed query contains the expected documents; that is,
1263+
// that it contains the 50 documents that were _not_ deleted.
1264+
// TODO(b/270731363): Remove the "if" condition below once the Firestore Emulator is fixed to
1265+
// send an existence filter. At the time of writing, the Firestore emulator fails to send an
1266+
// existence filter, resulting in the client including the deleted documents in the snapshot
1267+
// of the resumed query.
1268+
if (!([FSTIntegrationTestCase isRunningAgainstEmulator] && querySnapshot2.count == 100)) {
1269+
NSSet<NSString *> *actualDocumentIds =
1270+
[NSSet setWithArray:FIRQuerySnapshotGetIDs(querySnapshot2)];
1271+
NSSet<NSString *> *expectedDocumentIds;
1272+
{
1273+
NSMutableArray<NSString *> *expectedDocumentIdsAccumulator = [[NSMutableArray alloc] init];
1274+
for (FIRDocumentReference *documentRef in createdDocuments) {
1275+
if (![deletedDocumentIds containsObject:documentRef.documentID]) {
1276+
[expectedDocumentIdsAccumulator addObject:documentRef.documentID];
1277+
}
12681278
}
1279+
expectedDocumentIds = [NSSet setWithArray:expectedDocumentIdsAccumulator];
1280+
}
1281+
if (![actualDocumentIds isEqualToSet:expectedDocumentIds]) {
1282+
NSArray<NSString *> *unexpectedDocumentIds =
1283+
SortedStringsNotIn(actualDocumentIds, expectedDocumentIds);
1284+
NSArray<NSString *> *missingDocumentIds =
1285+
SortedStringsNotIn(expectedDocumentIds, actualDocumentIds);
1286+
XCTFail(@"querySnapshot2 contained %lu documents (expected %lu): "
1287+
@"%lu unexpected and %lu missing; "
1288+
@"unexpected documents: %@; missing documents: %@",
1289+
(unsigned long)actualDocumentIds.count, (unsigned long)expectedDocumentIds.count,
1290+
(unsigned long)unexpectedDocumentIds.count, (unsigned long)missingDocumentIds.count,
1291+
[unexpectedDocumentIds componentsJoinedByString:@", "],
1292+
[missingDocumentIds componentsJoinedByString:@", "]);
12691293
}
1270-
expectedDocumentIds = [NSSet setWithArray:expectedDocumentIdsAccumulator];
12711294
}
1272-
if (![actualDocumentIds isEqualToSet:expectedDocumentIds]) {
1273-
NSArray<NSString *> *unexpectedDocumentIds =
1274-
SortedStringsNotIn(actualDocumentIds, expectedDocumentIds);
1275-
NSArray<NSString *> *missingDocumentIds =
1276-
SortedStringsNotIn(expectedDocumentIds, actualDocumentIds);
1277-
XCTFail(@"querySnapshot2 contained %lu documents (expected %lu): "
1278-
@"%lu unexpected and %lu missing; "
1279-
@"unexpected documents: %@; missing documents: %@",
1280-
(unsigned long)actualDocumentIds.count, (unsigned long)expectedDocumentIds.count,
1281-
(unsigned long)unexpectedDocumentIds.count, (unsigned long)missingDocumentIds.count,
1282-
[unexpectedDocumentIds componentsJoinedByString:@", "],
1283-
[missingDocumentIds componentsJoinedByString:@", "]);
1295+
1296+
// Skip the verification of the existence filter mismatch when testing against the Firestore
1297+
// emulator because the Firestore emulator fails to to send an existence filter at all.
1298+
// TODO(b/270731363): Enable the verification of the existence filter mismatch once the
1299+
// Firestore emulator is fixed to send an existence filter.
1300+
if ([FSTIntegrationTestCase isRunningAgainstEmulator]) {
1301+
return;
12841302
}
1285-
}
12861303

1287-
// Skip the verification of the existence filter mismatch when testing against the Firestore
1288-
// emulator because the Firestore emulator fails to to send an existence filter at all.
1289-
// TODO(b/270731363): Enable the verification of the existence filter mismatch once the Firestore
1290-
// emulator is fixed to send an existence filter.
1291-
if ([FSTIntegrationTestCase isRunningAgainstEmulator]) {
1292-
return;
1293-
}
1304+
// Verify that Watch sent an existence filter with the correct counts when the query was
1305+
// resumed.
1306+
XCTAssertEqual(existence_filter_mismatches.size(), size_t{1},
1307+
@"Watch should have sent exactly 1 existence filter");
1308+
const TestingHooks::ExistenceFilterMismatchInfo &existenceFilterMismatchInfo =
1309+
existence_filter_mismatches[0];
1310+
XCTAssertEqual(existenceFilterMismatchInfo.local_cache_count, 100);
1311+
XCTAssertEqual(existenceFilterMismatchInfo.existence_filter_count, 50);
1312+
1313+
// Verify that Watch sent a valid bloom filter.
1314+
const absl::optional<TestingHooks::BloomFilterInfo> &bloom_filter =
1315+
existence_filter_mismatches[0].bloom_filter;
1316+
XCTAssertTrue(bloom_filter.has_value(),
1317+
"Watch should have included a bloom filter in the existence filter");
1318+
XCTAssertGreaterThan(bloom_filter->hash_count, 0);
1319+
XCTAssertGreaterThan(bloom_filter->bitmap_length, 0);
1320+
XCTAssertGreaterThan(bloom_filter->padding, 0);
1321+
XCTAssertLessThan(bloom_filter->padding, 8);
1322+
1323+
// Verify that the bloom filter was successfully used to avert a full requery. If a false
1324+
// positive occurred then retry the entire test. Although statistically rare, false positives
1325+
// are expected to happen occasionally. When a false positive _does_ happen, just retry the test
1326+
// with a different set of documents. If that retry _also_ experiences a false positive, then
1327+
// fail the test because that is so improbable that something must have gone wrong.
1328+
if (attemptNumber == 1 && !bloom_filter->applied) {
1329+
continue;
1330+
}
1331+
1332+
XCTAssertTrue(bloom_filter->applied,
1333+
@"The bloom filter should have been successfully applied with attemptNumber=%@",
1334+
@(attemptNumber));
12941335

1295-
// Verify that Watch sent an existence filter with the correct counts when the query was resumed.
1296-
XCTAssertEqual(static_cast<int>(existence_filter_mismatches.size()), 1);
1297-
firebase::firestore::util::TestingHooks::ExistenceFilterMismatchInfo &info =
1298-
existence_filter_mismatches[0];
1299-
XCTAssertEqual(info.local_cache_count, 100);
1300-
XCTAssertEqual(info.existence_filter_count, 50);
1336+
// Break out of the test loop now that the test passes.
1337+
break;
1338+
}
13011339
}
13021340

13031341
@end

Firestore/Example/Tests/SpecTests/FSTMockDatastore.mm

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,11 @@ void WatchQuery(const TargetData& query) override {
115115
// Snapshot version is ignored on the wire
116116
TargetData sentTargetData =
117117
query.WithResumeToken(query.resume_token(), SnapshotVersion::None());
118+
119+
if (query.expected_count().has_value()) {
120+
sentTargetData = sentTargetData.WithExpectedCount(query.expected_count().value());
121+
}
122+
118123
datastore_->IncrementWatchStreamRequests();
119124
active_targets_[query.target_id()] = sentTargetData;
120125
}

Firestore/Example/Tests/SpecTests/FSTSpecTests.mm

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
#include "Firestore/core/src/model/types.h"
5757
#include "Firestore/core/src/nanopb/message.h"
5858
#include "Firestore/core/src/nanopb/nanopb_util.h"
59+
#include "Firestore/core/src/remote/bloom_filter.h"
5960
#include "Firestore/core/src/remote/existence_filter.h"
6061
#include "Firestore/core/src/remote/serializer.h"
6162
#include "Firestore/core/src/remote/watch_change.h"
@@ -71,6 +72,7 @@
7172
#include "Firestore/core/src/util/to_string.h"
7273
#include "Firestore/core/test/unit/testutil/testutil.h"
7374
#include "absl/memory/memory.h"
75+
#include "absl/strings/escaping.h"
7476
#include "absl/types/optional.h"
7577

7678
namespace objc = firebase::firestore::objc;
@@ -98,6 +100,8 @@
98100
using firebase::firestore::nanopb::ByteString;
99101
using firebase::firestore::nanopb::MakeByteString;
100102
using firebase::firestore::nanopb::Message;
103+
using firebase::firestore::remote::BloomFilter;
104+
using firebase::firestore::remote::BloomFilterParameters;
101105
using firebase::firestore::remote::DocumentWatchChange;
102106
using firebase::firestore::remote::ExistenceFilter;
103107
using firebase::firestore::remote::ExistenceFilterWatchChange;
@@ -115,6 +119,7 @@
115119
using firebase::firestore::util::MakeStringPtr;
116120
using firebase::firestore::util::Path;
117121
using firebase::firestore::util::Status;
122+
using firebase::firestore::util::StatusOr;
118123
using firebase::firestore::util::TimerId;
119124
using firebase::firestore::util::ToString;
120125
using firebase::firestore::util::WrapCompare;
@@ -327,13 +332,36 @@ - (SnapshotVersion)parseVersion:(NSNumber *_Nullable)version {
327332
return Version(version.longLongValue);
328333
}
329334

335+
- (absl::optional<BloomFilterParameters>)parseBloomFilterParameter:
336+
(NSDictionary *_Nullable)bloomFilterProto {
337+
if (bloomFilterProto == nil) {
338+
return absl::nullopt;
339+
}
340+
NSDictionary *bitsData = bloomFilterProto[@"bits"];
341+
342+
// Decode base64 string into uint8_t vector. If bitmap is not specified in proto, use default
343+
// empty string.
344+
NSString *bitmapEncoded = bitsData[@"bitmap"];
345+
std::string bitmapDecoded;
346+
absl::Base64Unescape([bitmapEncoded cStringUsingEncoding:NSASCIIStringEncoding], &bitmapDecoded);
347+
ByteString bitmap(bitmapDecoded);
348+
349+
// If not specified in proto, default padding and hashCount to 0.
350+
int32_t padding = [bitsData[@"padding"] intValue];
351+
int32_t hashCount = [bloomFilterProto[@"hashCount"] intValue];
352+
return BloomFilterParameters{std::move(bitmap), padding, hashCount};
353+
}
354+
330355
- (QueryPurpose)parseQueryPurpose:(NSString *)value {
331356
if ([value isEqualToString:@"TargetPurposeListen"]) {
332357
return QueryPurpose::Listen;
333358
}
334359
if ([value isEqualToString:@"TargetPurposeExistenceFilterMismatch"]) {
335360
return QueryPurpose::ExistenceFilterMismatch;
336361
}
362+
if ([value isEqualToString:@"TargetPurposeExistenceFilterMismatchBloom"]) {
363+
return QueryPurpose::ExistenceFilterMismatchBloom;
364+
}
337365
if ([value isEqualToString:@"TargetPurposeLimboResolution"]) {
338366
return QueryPurpose::LimboResolution;
339367
}
@@ -484,8 +512,11 @@ - (void)doWatchFilter:(NSDictionary *)watchFilter {
484512
NSArray<NSNumber *> *targets = watchFilter[@"targetIds"];
485513
HARD_ASSERT(targets.count == 1, "ExistenceFilters currently support exactly one target only.");
486514

487-
ExistenceFilter filter{static_cast<int>(keys.count)};
488-
ExistenceFilterWatchChange change{filter, targets[0].intValue};
515+
absl::optional<BloomFilterParameters> bloomFilterParameters =
516+
[self parseBloomFilterParameter:watchFilter[@"bloomFilter"]];
517+
518+
ExistenceFilter filter{static_cast<int>(keys.count), std::move(bloomFilterParameters)};
519+
ExistenceFilterWatchChange change{std::move(filter), targets[0].intValue};
489520
[self.driver receiveWatchChange:change snapshotVersion:SnapshotVersion::None()];
490521
}
491522

@@ -702,8 +733,18 @@ - (void)validateEvent:(FSTQueryEvent *)actual matches:(NSDictionary *)expected {
702733
}
703734

704735
XCTAssertEqual(actual.viewSnapshot.value().document_changes().size(), expectedChanges.size());
705-
for (size_t i = 0; i != expectedChanges.size(); ++i) {
706-
XCTAssertTrue((actual.viewSnapshot.value().document_changes()[i] == expectedChanges[i]));
736+
737+
auto comparator = [](const DocumentViewChange &lhs, const DocumentViewChange &rhs) {
738+
return lhs.document()->key() < rhs.document()->key();
739+
};
740+
741+
std::vector<DocumentViewChange> expectedChangesSorted = expectedChanges;
742+
std::sort(expectedChangesSorted.begin(), expectedChangesSorted.end(), comparator);
743+
std::vector<DocumentViewChange> actualChangesSorted =
744+
actual.viewSnapshot.value().document_changes();
745+
std::sort(actualChangesSorted.begin(), actualChangesSorted.end(), comparator);
746+
for (size_t i = 0; i != expectedChangesSorted.size(); ++i) {
747+
XCTAssertTrue((actualChangesSorted[i] == expectedChangesSorted[i]));
707748
}
708749

709750
BOOL expectedHasPendingWrites =
@@ -806,6 +847,10 @@ - (void)validateExpectedState:(nullable NSDictionary *)expectedState {
806847
target_data = target_data.WithResumeToken(
807848
ByteString(), [self parseVersion:queryData[@"readTime"]]);
808849
}
850+
851+
if ([queryData objectForKey:@"expectedCount"] != nil) {
852+
target_data = target_data.WithExpectedCount([queryData[@"expectedCount"] intValue]);
853+
}
809854
queries.push_back(std::move(target_data));
810855
}
811856
expectedActiveTargets[targetID] = std::move(queries);
@@ -924,7 +969,13 @@ - (void)validateActiveTargets {
924969
XCTAssertEqual(actual.target_id(), targetData.target_id());
925970
XCTAssertEqual(actual.snapshot_version(), targetData.snapshot_version());
926971
XCTAssertEqual(actual.resume_token(), targetData.resume_token());
927-
972+
if (targetData.expected_count().has_value()) {
973+
if (!actual.expected_count().has_value()) {
974+
XCTFail(@"Actual target data doesn't have an expected_count.");
975+
} else {
976+
XCTAssertEqual(actual.expected_count().value(), targetData.expected_count().value());
977+
}
978+
}
928979
actualTargets.erase(targetID);
929980
}
930981

0 commit comments

Comments
 (0)