Skip to content

Commit 84d928c

Browse files
committed
Various bloom filter logic cleanup ported from Android
1 parent cd15480 commit 84d928c

File tree

4 files changed

+138
-76
lines changed

4 files changed

+138
-76
lines changed

packages/firestore/src/remote/watch_change.ts

Lines changed: 59 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -433,15 +433,18 @@ 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 applyResult = this.applyBloomFilter(watchChange, currentSize);
436+
const bloomFilter = this.parseBloomFilter(watchChange);
437+
const status = bloomFilter
438+
? this.applyBloomFilter(bloomFilter, watchChange, currentSize)
439+
: BloomFilterApplicationStatus.Skipped;
437440

438-
if (applyResult.status !== BloomFilterApplicationStatus.Success) {
441+
if (status !== BloomFilterApplicationStatus.Success) {
439442
// If bloom filter application fails, we reset the mapping and
440443
// trigger re-run of the query.
441444
this.resetTarget(targetId);
442445

443446
const purpose: TargetPurpose =
444-
applyResult.status === BloomFilterApplicationStatus.FalsePositive
447+
status === BloomFilterApplicationStatus.FalsePositive
445448
? TargetPurpose.ExistenceFilterMismatchBloom
446449
: TargetPurpose.ExistenceFilterMismatch;
447450
this.pendingTargetResets = this.pendingTargetResets.insert(
@@ -451,10 +454,11 @@ export class WatchChangeAggregator {
451454
}
452455
TestingHooks.instance?.notifyOnExistenceFilterMismatch(
453456
createExistenceFilterMismatchInfoForTestingHooks(
454-
applyResult.status,
455-
applyResult.bloomFilterMightContain ?? null,
456457
currentSize,
457-
watchChange.existenceFilter
458+
watchChange.existenceFilter,
459+
this.metadataProvider.getDatabaseId(),
460+
bloomFilter,
461+
status
458462
)
459463
);
460464
}
@@ -463,21 +467,15 @@ export class WatchChangeAggregator {
463467
}
464468

465469
/**
466-
* Apply bloom filter to remove the deleted documents, and return the
467-
* application status.
470+
* Parse the bloom filter from the "unchanged_names" field of an existence
471+
* filter.
468472
*/
469-
private applyBloomFilter(
470-
watchChange: ExistenceFilterChange,
471-
currentCount: number
472-
): {
473-
status: BloomFilterApplicationStatus;
474-
bloomFilterMightContain?: (documentPath: string) => boolean;
475-
} {
476-
const { unchangedNames, count: expectedCount } =
477-
watchChange.existenceFilter;
478-
473+
private parseBloomFilter(
474+
watchChange: ExistenceFilterChange
475+
): BloomFilter | null {
476+
const unchangedNames = watchChange.existenceFilter.unchangedNames;
479477
if (!unchangedNames || !unchangedNames.bits) {
480-
return { status: BloomFilterApplicationStatus.Skipped };
478+
return null;
481479
}
482480

483481
const {
@@ -495,7 +493,7 @@ export class WatchChangeAggregator {
495493
err.message +
496494
'); ignoring the bloom filter and falling back to full re-query.'
497495
);
498-
return { status: BloomFilterApplicationStatus.Skipped };
496+
return null;
499497
} else {
500498
throw err;
501499
}
@@ -511,46 +509,56 @@ export class WatchChangeAggregator {
511509
} else {
512510
logWarn('Applying bloom filter failed: ', err);
513511
}
514-
return { status: BloomFilterApplicationStatus.Skipped };
512+
return null;
515513
}
516514

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-
525515
if (bloomFilter.bitCount === 0) {
526-
return { status: BloomFilterApplicationStatus.Skipped };
516+
return null;
527517
}
528518

519+
return bloomFilter;
520+
}
521+
522+
/**
523+
* Apply bloom filter to remove the deleted documents, and return the
524+
* application status.
525+
*/
526+
private applyBloomFilter(
527+
bloomFilter: BloomFilter,
528+
watchChange: ExistenceFilterChange,
529+
currentCount: number
530+
): BloomFilterApplicationStatus {
531+
const expectedCount = watchChange.existenceFilter.count;
532+
529533
const removedDocumentCount = this.filterRemovedDocuments(
530-
watchChange.targetId,
531-
bloomFilterMightContain
534+
bloomFilter,
535+
watchChange.targetId
532536
);
533537

534-
const status =
535-
expectedCount === currentCount - removedDocumentCount
536-
? BloomFilterApplicationStatus.Success
537-
: BloomFilterApplicationStatus.FalsePositive;
538-
return { status, bloomFilterMightContain };
538+
return expectedCount === currentCount - removedDocumentCount
539+
? BloomFilterApplicationStatus.Success
540+
: BloomFilterApplicationStatus.FalsePositive;
539541
}
540542

541543
/**
542544
* Filter out removed documents based on bloom filter membership result and
543545
* return number of documents removed.
544546
*/
545547
private filterRemovedDocuments(
546-
targetId: number,
547-
bloomFilterMightContain: (documentPath: string) => boolean
548+
bloomFilter: BloomFilter,
549+
targetId: number
548550
): number {
549551
const existingKeys = this.metadataProvider.getRemoteKeysForTarget(targetId);
550552
let removalCount = 0;
551553

552554
existingKeys.forEach(key => {
553-
if (!bloomFilterMightContain(key.path.canonicalString())) {
555+
const databaseId = this.metadataProvider.getDatabaseId();
556+
const documentPath =
557+
`projects/${databaseId.projectId}` +
558+
`/databases/${databaseId.database}` +
559+
`/documents/${key.path.canonicalString()}`;
560+
561+
if (!bloomFilter.mightContain(documentPath)) {
554562
this.removeDocumentFromTarget(targetId, key, /*updatedDocument=*/ null);
555563
removalCount++;
556564
}
@@ -835,27 +843,29 @@ function snapshotChangesMap(): SortedMap<DocumentKey, ChangeType> {
835843
}
836844

837845
function createExistenceFilterMismatchInfoForTestingHooks(
838-
status: BloomFilterApplicationStatus,
839-
bloomFilterMightContain: null | ((documentPath: string) => boolean),
840846
localCacheCount: number,
841-
existenceFilter: ExistenceFilter
847+
existenceFilter: ExistenceFilter,
848+
databaseId: DatabaseId,
849+
bloomFilter: BloomFilter | null,
850+
bloomFilterStatus: BloomFilterApplicationStatus
842851
): TestingHooksExistenceFilterMismatchInfo {
843852
const result: TestingHooksExistenceFilterMismatchInfo = {
844853
localCacheCount,
845-
existenceFilterCount: existenceFilter.count
854+
existenceFilterCount: existenceFilter.count,
855+
databaseId: databaseId.database,
856+
projectId: databaseId.projectId
846857
};
847858

848859
const unchangedNames = existenceFilter.unchangedNames;
849860
if (unchangedNames) {
850861
result.bloomFilter = {
851-
applied: status === BloomFilterApplicationStatus.Success,
862+
applied: bloomFilterStatus === BloomFilterApplicationStatus.Success,
852863
hashCount: unchangedNames?.hashCount ?? 0,
853864
bitmapLength: unchangedNames?.bits?.bitmap?.length ?? 0,
854-
padding: unchangedNames?.bits?.padding ?? 0
865+
padding: unchangedNames?.bits?.padding ?? 0,
866+
mightContain: (value: string): boolean =>
867+
bloomFilter?.mightContain(value) ?? false
855868
};
856-
if (bloomFilterMightContain) {
857-
result.bloomFilter.mightContain = bloomFilterMightContain;
858-
}
859869
}
860870

861871
return result;

packages/firestore/src/util/testing_hooks.ts

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,18 @@ export interface ExistenceFilterMismatchInfo {
103103
*/
104104
existenceFilterCount: number;
105105

106+
/**
107+
* The projectId used when checking documents for membership in the bloom
108+
* filter.
109+
*/
110+
projectId: string;
111+
112+
/**
113+
* The databaseId used when checking documents for membership in the bloom
114+
* filter.
115+
*/
116+
databaseId: string;
117+
106118
/**
107119
* Information about the bloom filter provided by Watch in the ExistenceFilter
108120
* message's `unchangedNames` field. If this property is omitted or undefined
@@ -126,19 +138,11 @@ export interface ExistenceFilterMismatchInfo {
126138
padding: number;
127139

128140
/**
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.
141+
* Tests the given string for membership in the bloom filter created from
142+
* the existence filter; will be undefined if creating the bloom filter
143+
* failed.
140144
*/
141-
mightContain?(documentPath: string): boolean;
145+
mightContain?: (value: string) => boolean;
142146
};
143147
}
144148

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

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2260,8 +2260,9 @@ apiDescribe('Queries', persistence => {
22602260
// Delete one of the documents so that the next call to getDocs() will
22612261
// experience an existence filter mismatch. Use a WriteBatch, rather
22622262
// than deleteDoc(), to avoid affecting the local cache.
2263+
const documentToDelete = doc(coll, 'DocumentToDelete');
22632264
const writeBatchForDocumentDeletes = writeBatch(db);
2264-
writeBatchForDocumentDeletes.delete(doc(coll, 'DocumentToDelete'));
2265+
writeBatchForDocumentDeletes.delete(documentToDelete);
22652266
await writeBatchForDocumentDeletes.commit();
22662267

22672268
// Wait for 10 seconds, during which Watch will stop tracking the query
@@ -2278,7 +2279,7 @@ apiDescribe('Queries', persistence => {
22782279
documentSnapshot => documentSnapshot.id
22792280
);
22802281
const testDocIdsMinusDeletedDocId = testDocIds.filter(
2281-
documentId => documentId !== 'DocumentToDelete'
2282+
documentId => documentId !== documentToDelete.id
22822283
);
22832284
expect(snapshot2DocumentIds, 'snapshot2DocumentIds').to.have.members(
22842285
testDocIdsMinusDeletedDocId
@@ -2309,20 +2310,17 @@ apiDescribe('Queries', persistence => {
23092310
// is if there is a false positive when testing for 'DocumentToDelete'
23102311
// in the bloom filter. So verify that the bloom filter application is
23112312
// successful, unless there was a false positive.
2312-
const isFalsePositive = bloomFilter.mightContain!(
2313-
`${coll.path}/DocumentToDelete`
2314-
);
2313+
const isFalsePositive = bloomFilter.mightContain(documentToDelete);
23152314
expect(bloomFilter.applied, 'bloomFilter.applied').to.equal(
23162315
!isFalsePositive
23172316
);
23182317

23192318
// Verify that the bloom filter contains the document paths with complex
23202319
// Unicode characters.
2321-
for (const testDocId of testDocIdsMinusDeletedDocId) {
2322-
const testDocPath = `${coll.path}/${testDocId}`;
2320+
for (const testDoc of snapshot2.docs.map(snapshot => snapshot.ref)) {
23232321
expect(
2324-
bloomFilter.mightContain!(testDocPath),
2325-
`bloomFilter.mightContain('${testDocPath}')`
2322+
bloomFilter.mightContain(testDoc),
2323+
`bloomFilter.mightContain('${testDoc.path}')`
23262324
).to.be.true;
23272325
}
23282326
});

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

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515
* limitations under the License.
1616
*/
1717

18-
import { _TestingHooks as TestingHooks } from './firebase_export';
18+
import {
19+
DocumentReference,
20+
_TestingHooks as TestingHooks
21+
} from './firebase_export';
1922

2023
/**
2124
* Captures all existence filter mismatches in the Watch 'Listen' stream that
@@ -30,9 +33,9 @@ export async function captureExistenceFilterMismatches<T>(
3033
): Promise<[ExistenceFilterMismatchInfo[], T]> {
3134
const results: ExistenceFilterMismatchInfo[] = [];
3235
const onExistenceFilterMismatchCallback = (
33-
info: ExistenceFilterMismatchInfo
36+
info: ExistenceFilterMismatchInfoInternal
3437
): void => {
35-
results.push(info);
38+
results.push(createExistenceFilterMismatchInfoFrom(info));
3639
};
3740

3841
const unregister =
@@ -51,8 +54,7 @@ export async function captureExistenceFilterMismatches<T>(
5154
}
5255

5356
/**
54-
* Information about an existence filter mismatch, captured during an invocation
55-
* of `captureExistenceFilterMismatches()`.
57+
* A copy of `ExistenceFilterMismatchInfo` as defined in `testing_hooks.ts`.
5658
*
5759
* See the documentation of `TestingHooks.notifyOnExistenceFilterMismatch()`
5860
* for the meaning of these values.
@@ -61,6 +63,27 @@ export async function captureExistenceFilterMismatches<T>(
6163
* testing_hooks.ts. I tried to do this but couldn't figure out how to get it to
6264
* work in a way that survived bundling and minification.
6365
*/
66+
interface ExistenceFilterMismatchInfoInternal {
67+
localCacheCount: number;
68+
existenceFilterCount: number;
69+
projectId: string;
70+
databaseId: string;
71+
bloomFilter?: {
72+
applied: boolean;
73+
hashCount: number;
74+
bitmapLength: number;
75+
padding: number;
76+
mightContain?: (value: string) => boolean;
77+
};
78+
}
79+
80+
/**
81+
* Information about an existence filter mismatch, captured during an invocation
82+
* of `captureExistenceFilterMismatches()`.
83+
*
84+
* See the documentation of `TestingHooks.notifyOnExistenceFilterMismatch()`
85+
* for the meaning of these values.
86+
*/
6487
export interface ExistenceFilterMismatchInfo {
6588
localCacheCount: number;
6689
existenceFilterCount: number;
@@ -69,6 +92,33 @@ export interface ExistenceFilterMismatchInfo {
6992
hashCount: number;
7093
bitmapLength: number;
7194
padding: number;
72-
mightContain?(documentPath: string): boolean;
95+
mightContain(documentRef: DocumentReference): boolean;
7396
};
7497
}
98+
99+
function createExistenceFilterMismatchInfoFrom(
100+
internalInfo: ExistenceFilterMismatchInfoInternal
101+
): ExistenceFilterMismatchInfo {
102+
const info: ExistenceFilterMismatchInfo = {
103+
localCacheCount: internalInfo.localCacheCount,
104+
existenceFilterCount: internalInfo.existenceFilterCount
105+
};
106+
107+
const internalBloomFilter = internalInfo.bloomFilter;
108+
if (internalBloomFilter) {
109+
info.bloomFilter = {
110+
applied: internalBloomFilter.applied,
111+
hashCount: internalBloomFilter.hashCount,
112+
bitmapLength: internalBloomFilter.bitmapLength,
113+
padding: internalBloomFilter.padding,
114+
mightContain: (documentRef: DocumentReference): boolean =>
115+
internalBloomFilter.mightContain?.(
116+
`projects/${internalInfo.projectId}` +
117+
`/databases/${internalInfo.databaseId}` +
118+
`/documents/${documentRef.path}`
119+
) ?? false
120+
};
121+
}
122+
123+
return info;
124+
}

0 commit comments

Comments
 (0)