Skip to content

Commit c29156a

Browse files
authored
Update test 'bloom filter should correctly encode special unicode characters' in query.test.ts (#7413)
1 parent 684bb4e commit c29156a

File tree

4 files changed

+137
-111
lines changed

4 files changed

+137
-111
lines changed

packages/firestore/src/remote/watch_change.ts

Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -433,15 +433,15 @@ export class WatchChangeAggregator {
433433
// raise a snapshot with `isFromCache:true`.
434434
if (currentSize !== expectedCount) {
435435
// Apply bloom filter to identify and mark removed documents.
436-
const status = this.applyBloomFilter(watchChange, currentSize);
436+
const applyResult = this.applyBloomFilter(watchChange, currentSize);
437437

438-
if (status !== BloomFilterApplicationStatus.Success) {
438+
if (applyResult.status !== BloomFilterApplicationStatus.Success) {
439439
// If bloom filter application fails, we reset the mapping and
440440
// trigger re-run of the query.
441441
this.resetTarget(targetId);
442442

443443
const purpose: TargetPurpose =
444-
status === BloomFilterApplicationStatus.FalsePositive
444+
applyResult.status === BloomFilterApplicationStatus.FalsePositive
445445
? TargetPurpose.ExistenceFilterMismatchBloom
446446
: TargetPurpose.ExistenceFilterMismatch;
447447
this.pendingTargetResets = this.pendingTargetResets.insert(
@@ -451,7 +451,8 @@ export class WatchChangeAggregator {
451451
}
452452
TestingHooks.instance?.notifyOnExistenceFilterMismatch(
453453
createExistenceFilterMismatchInfoForTestingHooks(
454-
status,
454+
applyResult.status,
455+
applyResult.bloomFilterMightContain ?? null,
455456
currentSize,
456457
watchChange.existenceFilter
457458
)
@@ -468,12 +469,15 @@ export class WatchChangeAggregator {
468469
private applyBloomFilter(
469470
watchChange: ExistenceFilterChange,
470471
currentCount: number
471-
): BloomFilterApplicationStatus {
472+
): {
473+
status: BloomFilterApplicationStatus;
474+
bloomFilterMightContain?: (documentPath: string) => boolean;
475+
} {
472476
const { unchangedNames, count: expectedCount } =
473477
watchChange.existenceFilter;
474478

475479
if (!unchangedNames || !unchangedNames.bits) {
476-
return BloomFilterApplicationStatus.Skipped;
480+
return { status: BloomFilterApplicationStatus.Skipped };
477481
}
478482

479483
const {
@@ -491,7 +495,7 @@ export class WatchChangeAggregator {
491495
err.message +
492496
'); ignoring the bloom filter and falling back to full re-query.'
493497
);
494-
return BloomFilterApplicationStatus.Skipped;
498+
return { status: BloomFilterApplicationStatus.Skipped };
495499
} else {
496500
throw err;
497501
}
@@ -507,23 +511,31 @@ export class WatchChangeAggregator {
507511
} else {
508512
logWarn('Applying bloom filter failed: ', err);
509513
}
510-
return BloomFilterApplicationStatus.Skipped;
514+
return { status: BloomFilterApplicationStatus.Skipped };
511515
}
512516

517+
const bloomFilterMightContain = (documentPath: string): boolean => {
518+
const databaseId = this.metadataProvider.getDatabaseId();
519+
return bloomFilter.mightContain(
520+
`projects/${databaseId.projectId}/databases/${databaseId.database}` +
521+
`/documents/${documentPath}`
522+
);
523+
};
524+
513525
if (bloomFilter.bitCount === 0) {
514-
return BloomFilterApplicationStatus.Skipped;
526+
return { status: BloomFilterApplicationStatus.Skipped };
515527
}
516528

517529
const removedDocumentCount = this.filterRemovedDocuments(
518530
watchChange.targetId,
519-
bloomFilter
531+
bloomFilterMightContain
520532
);
521533

522-
if (expectedCount !== currentCount - removedDocumentCount) {
523-
return BloomFilterApplicationStatus.FalsePositive;
524-
}
525-
526-
return BloomFilterApplicationStatus.Success;
534+
const status =
535+
expectedCount === currentCount - removedDocumentCount
536+
? BloomFilterApplicationStatus.Success
537+
: BloomFilterApplicationStatus.FalsePositive;
538+
return { status, bloomFilterMightContain };
527539
}
528540

529541
/**
@@ -532,18 +544,13 @@ export class WatchChangeAggregator {
532544
*/
533545
private filterRemovedDocuments(
534546
targetId: number,
535-
bloomFilter: BloomFilter
547+
bloomFilterMightContain: (documentPath: string) => boolean
536548
): number {
537549
const existingKeys = this.metadataProvider.getRemoteKeysForTarget(targetId);
538550
let removalCount = 0;
539551

540552
existingKeys.forEach(key => {
541-
const databaseId = this.metadataProvider.getDatabaseId();
542-
const documentPath = `projects/${databaseId.projectId}/databases/${
543-
databaseId.database
544-
}/documents/${key.path.canonicalString()}`;
545-
546-
if (!bloomFilter.mightContain(documentPath)) {
553+
if (!bloomFilterMightContain(key.path.canonicalString())) {
547554
this.removeDocumentFromTarget(targetId, key, /*updatedDocument=*/ null);
548555
removalCount++;
549556
}
@@ -829,6 +836,7 @@ function snapshotChangesMap(): SortedMap<DocumentKey, ChangeType> {
829836

830837
function createExistenceFilterMismatchInfoForTestingHooks(
831838
status: BloomFilterApplicationStatus,
839+
bloomFilterMightContain: null | ((documentPath: string) => boolean),
832840
localCacheCount: number,
833841
existenceFilter: ExistenceFilter
834842
): TestingHooksExistenceFilterMismatchInfo {
@@ -845,6 +853,9 @@ function createExistenceFilterMismatchInfoForTestingHooks(
845853
bitmapLength: unchangedNames?.bits?.bitmap?.length ?? 0,
846854
padding: unchangedNames?.bits?.padding ?? 0
847855
};
856+
if (bloomFilterMightContain) {
857+
result.bloomFilter.mightContain = bloomFilterMightContain;
858+
}
848859
}
849860

850861
return result;

packages/firestore/src/util/testing_hooks.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,21 @@ export interface ExistenceFilterMismatchInfo {
124124

125125
/** The number of bits of padding in the last byte of the bloom filter. */
126126
padding: number;
127+
128+
/**
129+
* Check if the given document path is contained in the bloom filter.
130+
*
131+
* The "path" of a document can be retrieved from the
132+
* `DocumentReference.path` property.
133+
*
134+
* Note that due to the probabilistic nature of a bloom filter, it is
135+
* possible that false positives may occur; that is, this function may
136+
* return `true` even though the given string is not in the bloom filter.
137+
*
138+
* This property is "optional"; if it is undefined then parsing the bloom
139+
* filter failed.
140+
*/
141+
mightContain?(documentPath: string): boolean;
127142
};
128143
}
129144

packages/firestore/test/integration/api/query.test.ts

Lines changed: 88 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -2243,99 +2243,98 @@ apiDescribe('Queries', persistence => {
22432243
return map;
22442244
}, {} as { [key: string]: DocumentData });
22452245

2246-
// Ensure that the local cache is configured to use LRU garbage
2247-
// collection (rather than eager garbage collection) so that the resume
2248-
// token and document data does not get prematurely evicted.
2246+
// Ensure that the local cache is configured to use LRU garbage collection
2247+
// (rather than eager garbage collection) so that the resume token and
2248+
// document data does not get prematurely evicted.
22492249
const lruPersistence = persistence.toLruGc();
22502250

2251-
return withRetry(async attemptNumber => {
2252-
return withTestCollection(
2253-
lruPersistence,
2254-
testDocs,
2255-
async (coll, db) => {
2256-
// Run a query to populate the local cache with documents that have
2257-
// names with complex Unicode characters.
2258-
const snapshot1 = await getDocs(coll);
2259-
const snapshot1DocumentIds = snapshot1.docs.map(
2260-
documentSnapshot => documentSnapshot.id
2261-
);
2262-
expect(
2263-
snapshot1DocumentIds,
2264-
'snapshot1DocumentIds'
2265-
).to.have.members(testDocIds);
2266-
2267-
// Delete one of the documents so that the next call to getDocs() will
2268-
// experience an existence filter mismatch. Do this deletion in a
2269-
// transaction, rather than using deleteDoc(), to avoid affecting the
2270-
// local cache.
2271-
await runTransaction(db, async txn => {
2272-
const snapshotOfDocumentToDelete = await txn.get(
2273-
doc(coll, 'DocumentToDelete')
2274-
);
2275-
expect(
2276-
snapshotOfDocumentToDelete.exists(),
2277-
'snapshotOfDocumentToDelete.exists()'
2278-
).to.be.true;
2279-
txn.delete(snapshotOfDocumentToDelete.ref);
2280-
});
2281-
2282-
// Wait for 10 seconds, during which Watch will stop tracking the
2283-
// query and will send an existence filter rather than "delete" events
2284-
// when the query is resumed.
2285-
await new Promise(resolve => setTimeout(resolve, 10000));
2286-
2287-
// Resume the query and save the resulting snapshot for verification.
2288-
// Use some internal testing hooks to "capture" the existence filter
2289-
// mismatches.
2290-
const [existenceFilterMismatches, snapshot2] =
2291-
await captureExistenceFilterMismatches(() => getDocs(coll));
2292-
const snapshot2DocumentIds = snapshot2.docs.map(
2293-
documentSnapshot => documentSnapshot.id
2294-
);
2295-
const testDocIdsMinusDeletedDocId = testDocIds.filter(
2296-
documentId => documentId !== 'DocumentToDelete'
2297-
);
2298-
expect(
2299-
snapshot2DocumentIds,
2300-
'snapshot2DocumentIds'
2301-
).to.have.members(testDocIdsMinusDeletedDocId);
2302-
2303-
// Verify that Watch sent an existence filter with the correct counts.
2304-
expect(
2305-
existenceFilterMismatches,
2306-
'existenceFilterMismatches'
2307-
).to.have.length(1);
2308-
const { localCacheCount, existenceFilterCount, bloomFilter } =
2309-
existenceFilterMismatches[0];
2310-
expect(localCacheCount, 'localCacheCount').to.equal(
2311-
testDocIds.length
2312-
);
2313-
expect(existenceFilterCount, 'existenceFilterCount').to.equal(
2314-
testDocIds.length - 1
2315-
);
2251+
return withTestCollection(lruPersistence, testDocs, async (coll, db) => {
2252+
// Run a query to populate the local cache with documents that have
2253+
// names with complex Unicode characters.
2254+
const snapshot1 = await getDocs(coll);
2255+
const snapshot1DocumentIds = snapshot1.docs.map(
2256+
documentSnapshot => documentSnapshot.id
2257+
);
2258+
expect(snapshot1DocumentIds, 'snapshot1DocumentIds').to.have.members(
2259+
testDocIds
2260+
);
23162261

2317-
// Verify that Watch sent a valid bloom filter.
2318-
if (!bloomFilter) {
2319-
expect.fail(
2320-
'The existence filter should have specified a bloom filter ' +
2321-
'in its `unchanged_names` field.'
2322-
);
2323-
throw new Error('should never get here');
2324-
}
2325-
2326-
// Verify that the bloom filter was successfully used to avert a full
2327-
// requery. If a false positive occurred, which is statistically rare,
2328-
// but technically possible, then retry the entire test.
2329-
if (attemptNumber === 1 && !bloomFilter.applied) {
2330-
throw new RetryError();
2331-
}
2332-
2333-
expect(
2334-
bloomFilter.applied,
2335-
`bloomFilter.applied with attemptNumber=${attemptNumber}`
2336-
).to.be.true;
2337-
}
2262+
// Delete one of the documents so that the next call to getDocs() will
2263+
// experience an existence filter mismatch. Do this deletion in a
2264+
// transaction, rather than using deleteDoc(), to avoid affecting the
2265+
// local cache.
2266+
await runTransaction(db, async txn => {
2267+
const snapshotOfDocumentToDelete = await txn.get(
2268+
doc(coll, 'DocumentToDelete')
2269+
);
2270+
expect(
2271+
snapshotOfDocumentToDelete.exists(),
2272+
'snapshotOfDocumentToDelete.exists()'
2273+
).to.be.true;
2274+
txn.delete(snapshotOfDocumentToDelete.ref);
2275+
});
2276+
2277+
// Wait for 10 seconds, during which Watch will stop tracking the query
2278+
// and will send an existence filter rather than "delete" events when
2279+
// the query is resumed.
2280+
await new Promise(resolve => setTimeout(resolve, 10000));
2281+
2282+
// Resume the query and save the resulting snapshot for verification.
2283+
// Use some internal testing hooks to "capture" the existence filter
2284+
// mismatches.
2285+
const [existenceFilterMismatches, snapshot2] =
2286+
await captureExistenceFilterMismatches(() => getDocs(coll));
2287+
const snapshot2DocumentIds = snapshot2.docs.map(
2288+
documentSnapshot => documentSnapshot.id
2289+
);
2290+
const testDocIdsMinusDeletedDocId = testDocIds.filter(
2291+
documentId => documentId !== 'DocumentToDelete'
2292+
);
2293+
expect(snapshot2DocumentIds, 'snapshot2DocumentIds').to.have.members(
2294+
testDocIdsMinusDeletedDocId
2295+
);
2296+
2297+
// Verify that Watch sent an existence filter with the correct counts.
2298+
expect(
2299+
existenceFilterMismatches,
2300+
'existenceFilterMismatches'
2301+
).to.have.length(1);
2302+
const existenceFilterMismatch = existenceFilterMismatches[0];
2303+
expect(
2304+
existenceFilterMismatch.localCacheCount,
2305+
'localCacheCount'
2306+
).to.equal(testDocIds.length);
2307+
expect(
2308+
existenceFilterMismatch.existenceFilterCount,
2309+
'existenceFilterCount'
2310+
).to.equal(testDocIds.length - 1);
2311+
2312+
// Verify that we got a bloom filter from Watch.
2313+
const bloomFilter = existenceFilterMismatch.bloomFilter!;
2314+
expect(bloomFilter?.mightContain, 'bloomFilter.mightContain').to.not.be
2315+
.undefined;
2316+
2317+
// The bloom filter application should statistically be successful
2318+
// almost every time; the _only_ time when it would _not_ be successful
2319+
// is if there is a false positive when testing for 'DocumentToDelete'
2320+
// in the bloom filter. So verify that the bloom filter application is
2321+
// successful, unless there was a false positive.
2322+
const isFalsePositive = bloomFilter.mightContain!(
2323+
`${coll.path}/DocumentToDelete`
2324+
);
2325+
expect(bloomFilter.applied, 'bloomFilter.applied').to.equal(
2326+
!isFalsePositive
23382327
);
2328+
2329+
// Verify that the bloom filter contains the document paths with complex
2330+
// Unicode characters.
2331+
for (const testDocId of testDocIdsMinusDeletedDocId) {
2332+
const testDocPath = `${coll.path}/${testDocId}`;
2333+
expect(
2334+
bloomFilter.mightContain!(testDocPath),
2335+
`bloomFilter.mightContain('${testDocPath}')`
2336+
).to.be.true;
2337+
}
23392338
});
23402339
}
23412340
).timeout('90s');

packages/firestore/test/integration/util/testing_hooks_util.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,5 +69,6 @@ export interface ExistenceFilterMismatchInfo {
6969
hashCount: number;
7070
bitmapLength: number;
7171
padding: number;
72+
mightContain?(documentPath: string): boolean;
7273
};
7374
}

0 commit comments

Comments
 (0)