Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add 'existence-filter-mismatch-bloom' to listen request labels and test with spec tests #7107

Merged
merged 5 commits into from
Mar 9, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions packages/firestore/src/core/sync_engine_impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ import { primitiveComparator } from '../util/misc';
import { ObjectMap } from '../util/obj_map';
import { Deferred } from '../util/promise';
import { SortedMap } from '../util/sorted_map';
import { SortedSet } from '../util/sorted_set';
import { BATCHID_UNKNOWN } from '../util/types';

import {
Expand Down Expand Up @@ -639,7 +638,9 @@ export async function syncEngineRejectListen(
const event = new RemoteEvent(
SnapshotVersion.min(),
/* targetChanges= */ new Map<TargetId, TargetChange>(),
/* targetMismatches= */ new SortedSet<TargetId>(primitiveComparator),
/* targetMismatches= */ new SortedMap<TargetId, TargetPurpose>(
primitiveComparator
),
documentUpdates,
resolvedLimboDocuments
);
Expand Down
2 changes: 1 addition & 1 deletion packages/firestore/src/local/local_store_impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -601,7 +601,7 @@ export function localStoreApplyRemoteEventToLocalCache(
let newTargetData = oldTargetData.withSequenceNumber(
txn.currentSequenceNumber
);
if (remoteEvent.targetMismatches.has(targetId)) {
if (remoteEvent.targetMismatches.get(targetId) != null) {
dconeybe marked this conversation as resolved.
Show resolved Hide resolved
newTargetData = newTargetData
.withResumeToken(
ByteString.EMPTY_BYTE_STRING,
Expand Down
5 changes: 5 additions & 0 deletions packages/firestore/src/local/target_data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ export const enum TargetPurpose {
*/
ExistenceFilterMismatch,

/**
* The query target was used if the query is the result of a false positive in the bloom filter.
dconeybe marked this conversation as resolved.
Show resolved Hide resolved
*/
ExistenceFilterMismatchBloom,

/** The query target was used to resolve a limbo document. */
LimboResolution
}
Expand Down
4 changes: 4 additions & 0 deletions packages/firestore/src/remote/bloom_filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ export class BloomFilter {
this.bitCountInInteger = Integer.fromNumber(this.bitCount);
}

isEmpty(): boolean {
dconeybe marked this conversation as resolved.
Show resolved Hide resolved
return this.bitCount === 0;
}

// Calculate the ith hash value based on the hashed 64bit integers,
// and calculate its corresponding bit index in the bitmap to be checked.
private getBitIndex(num1: Integer, num2: Integer, hashIndex: number): number {
Expand Down
16 changes: 9 additions & 7 deletions packages/firestore/src/remote/remote_event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,16 @@

import { SnapshotVersion } from '../core/snapshot_version';
import { TargetId } from '../core/types';
import { TargetPurpose } from '../local/target_data';
import {
documentKeySet,
DocumentKeySet,
mutableDocumentMap,
MutableDocumentMap,
targetIdSet
MutableDocumentMap
} from '../model/collections';
import { ByteString } from '../util/byte_string';
import { SortedSet } from '../util/sorted_set';
import { primitiveComparator } from '../util/misc';
import { SortedMap } from '../util/sorted_map';

/**
* An event from the RemoteStore. It is split into targetChanges (changes to the
Expand All @@ -43,10 +44,11 @@ export class RemoteEvent {
*/
readonly targetChanges: Map<TargetId, TargetChange>,
/**
* A set of targets that is known to be inconsistent. Listens for these
* targets should be re-established without resume tokens.
* A map of targets that is known to be inconsistent, and the purpose for
* re-listening. Listens for these targets should be re-established without
* resume tokens.
*/
readonly targetMismatches: SortedSet<TargetId>,
readonly targetMismatches: SortedMap<TargetId, TargetPurpose>,
/**
* A set of which documents have changed or been deleted, along with the
* doc's new values (if not deleted).
Expand Down Expand Up @@ -82,7 +84,7 @@ export class RemoteEvent {
return new RemoteEvent(
SnapshotVersion.min(),
targetChanges,
targetIdSet(),
new SortedMap<TargetId, TargetPurpose>(primitiveComparator),
mutableDocumentMap(),
documentKeySet()
);
Expand Down
11 changes: 7 additions & 4 deletions packages/firestore/src/remote/remote_store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
localStoreGetNextMutationBatch
} from '../local/local_store_impl';
import { isIndexedDbTransactionError } from '../local/simple_db';
import { TargetData, TargetPurpose } from '../local/target_data';
import { TargetData } from '../local/target_data';
import { MutationResult } from '../model/mutation';
import { MutationBatch, MutationBatchResult } from '../model/mutation_batch';
import { debugAssert, debugCast } from '../util/assert';
Expand Down Expand Up @@ -587,7 +587,7 @@ function raiseWatchSnapshot(

// Re-establish listens for the targets that have been invalidated by
// existence filter mismatches.
remoteEvent.targetMismatches.forEach(targetId => {
remoteEvent.targetMismatches.forEach((targetId, targetPurpose) => {
const targetData = remoteStoreImpl.listenTargets.get(targetId);
if (!targetData) {
// A watched target might have been removed already.
Expand All @@ -611,11 +611,14 @@ function raiseWatchSnapshot(
// Mark the target we send as being on behalf of an existence filter
// mismatch, but don't actually retain that in listenTargets. This ensures
// that we flag the first re-listen this way without impacting future
// listens of this target (that might happen e.g. on reconnect).
// listens of this target (that might happen e.g. on reconnect). The target
dconeybe marked this conversation as resolved.
Show resolved Hide resolved
// purpose will be `ExistenceFilterMismatchBloom` if there is a bloom filter
// but it yield false positive result, otherwise, it will be set to
// `ExistenceFilterMismatch`.
const requestTargetData = new TargetData(
targetData.target,
targetId,
TargetPurpose.ExistenceFilterMismatch,
targetPurpose,
targetData.sequenceNumber
);
sendWatchRequest(remoteStoreImpl, requestTargetData);
Expand Down
9 changes: 4 additions & 5 deletions packages/firestore/src/remote/serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1008,7 +1008,7 @@ export function toListenRequestLabels(
serializer: JsonProtoSerializer,
targetData: TargetData
): ProtoApiClientObjectMap<string> | null {
const value = toLabel(serializer, targetData.purpose);
const value = toLabel(targetData.purpose);
if (value == null) {
return null;
} else {
Expand All @@ -1018,15 +1018,14 @@ export function toListenRequestLabels(
}
}

function toLabel(
serializer: JsonProtoSerializer,
purpose: TargetPurpose
): string | null {
export function toLabel(purpose: TargetPurpose): string | null {
switch (purpose) {
case TargetPurpose.Listen:
return null;
case TargetPurpose.ExistenceFilterMismatch:
return 'existence-filter-mismatch';
case TargetPurpose.ExistenceFilterMismatchBloom:
return 'existence-filter-mismatch-bloom';
case TargetPurpose.LimboResolution:
return 'limbo-document';
default:
Expand Down
56 changes: 41 additions & 15 deletions packages/firestore/src/remote/watch_change.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ export const enum WatchTargetChangeState {
Reset
}

const enum BloomFilterApplicationStatus {
Success,
Skipped,
FalsePositive
}
export class WatchTargetChange {
constructor(
/** What kind of change occurred to the watch target. */
Expand Down Expand Up @@ -280,11 +285,13 @@ export class WatchChangeAggregator {
private pendingDocumentTargetMapping = documentTargetMap();

/**
* A list of targets with existence filter mismatches. These targets are
* A map of targets with existence filter mismatches. These targets are
* known to be inconsistent and their listens needs to be re-established by
* RemoteStore.
*/
private pendingTargetResets = new SortedSet<TargetId>(primitiveComparator);
private pendingTargetResets = new SortedMap<TargetId, TargetPurpose>(
primitiveComparator
);

/**
* Processes and adds the DocumentWatchChange to the current set of changes.
Expand Down Expand Up @@ -422,31 +429,40 @@ export class WatchChangeAggregator {
// raise a snapshot with `isFromCache:true`.
if (currentSize !== expectedCount) {
// Apply bloom filter to identify and mark removed documents.
const bloomFilterApplied = this.applyBloomFilter(
watchChange,
currentSize
);
if (!bloomFilterApplied) {
const status = this.applyBloomFilter(watchChange, currentSize);

if (status !== BloomFilterApplicationStatus.Success) {
// If bloom filter application fails, we reset the mapping and
// trigger re-run of the query.
this.resetTarget(targetId);
this.pendingTargetResets = this.pendingTargetResets.add(targetId);

const purpose: TargetPurpose =
status === BloomFilterApplicationStatus.FalsePositive
? TargetPurpose.ExistenceFilterMismatchBloom
: TargetPurpose.ExistenceFilterMismatch;
this.pendingTargetResets = this.pendingTargetResets.insert(
targetId,
purpose
);
}
}
}
}
}

/** Returns whether a bloom filter removed the deleted documents successfully. */
/**
* Apply bloom filter to remove the deleted documents, and return the
* application status.
*/
private applyBloomFilter(
watchChange: ExistenceFilterChange,
currentCount: number
): boolean {
): BloomFilterApplicationStatus {
const { unchangedNames, count: expectedCount } =
watchChange.existenceFilter;

if (!unchangedNames || !unchangedNames.bits) {
return false;
return BloomFilterApplicationStatus.Skipped;
}

const {
Expand All @@ -464,7 +480,7 @@ export class WatchChangeAggregator {
err.message +
'); ignoring the bloom filter and falling back to full re-query.'
);
return false;
return BloomFilterApplicationStatus.Skipped;
} else {
throw err;
}
Expand All @@ -480,15 +496,23 @@ export class WatchChangeAggregator {
} else {
logWarn('Applying bloom filter failed: ', err);
}
return false;
return BloomFilterApplicationStatus.Skipped;
}

if (bloomFilter.isEmpty()) {
return BloomFilterApplicationStatus.Skipped;
}

const removedDocumentCount = this.filterRemovedDocuments(
watchChange.targetId,
bloomFilter
);

return expectedCount === currentCount - removedDocumentCount;
if (expectedCount !== currentCount - removedDocumentCount) {
return BloomFilterApplicationStatus.FalsePositive;
}

return BloomFilterApplicationStatus.Success;
}

/**
Expand Down Expand Up @@ -599,7 +623,9 @@ export class WatchChangeAggregator {

this.pendingDocumentUpdates = mutableDocumentMap();
this.pendingDocumentTargetMapping = documentTargetMap();
this.pendingTargetResets = new SortedSet<TargetId>(primitiveComparator);
this.pendingTargetResets = new SortedMap<TargetId, TargetPurpose>(
primitiveComparator
);

return remoteEvent;
}
Expand Down
4 changes: 2 additions & 2 deletions packages/firestore/test/integration/api/query.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2061,8 +2061,8 @@ apiDescribe('Queries', (persistence: boolean) => {
});
});

// TODO(Mila): Skip the test when using emulator as there is a bug related to
// sending existence filter in response: b/270731363. Remove the condition
// TODO(Mila): Skip the test when using emulator as there is a bug related to
// sending existence filter in response: b/270731363. Remove the condition
// here once the bug is resolved.
// eslint-disable-next-line no-restricted-properties
(USE_EMULATOR ? it.skip : it)(
Expand Down
6 changes: 6 additions & 0 deletions packages/firestore/test/unit/remote/remote_event.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,9 @@ describe('RemoteEvent', () => {
event = aggregator.createRemoteEvent(version(3));
expect(event.documentUpdates.size).to.equal(0);
expect(event.targetMismatches.size).to.equal(1);
expect(event.targetMismatches.get(1)).to.equal(
dconeybe marked this conversation as resolved.
Show resolved Hide resolved
TargetPurpose.ExistenceFilterMismatch
);
expect(event.targetChanges.size).to.equal(1);

const expected = updateMapping(
Expand Down Expand Up @@ -499,6 +502,9 @@ describe('RemoteEvent', () => {
const event = aggregator.createRemoteEvent(version(3));
expect(event.documentUpdates.size).to.equal(1);
expect(event.targetMismatches.size).to.equal(1);
expect(event.targetMismatches.get(1)).to.equal(
TargetPurpose.ExistenceFilterMismatch
);
expect(event.targetChanges.get(1)!.current).to.be.false;
});

Expand Down
25 changes: 21 additions & 4 deletions packages/firestore/test/unit/specs/existence_filter_spec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/

import { newQueryForPath } from '../../../src/core/query';
import { TargetPurpose } from '../../../src/local/target_data';
import { Code } from '../../../src/util/error';
import {
deletedDoc,
Expand Down Expand Up @@ -305,7 +306,11 @@ describeSpec('Existence Filters:', [], () => {
// BloomFilter correctly identifies docC is deleted, but yields false
// positive results for docB. Re-run query is triggered.
.expectEvents(query1, { fromCache: true })
.expectActiveTargets({ query: query1, resumeToken: '' })
.expectActiveTargets({
query: query1,
resumeToken: '',
targetPurpose: TargetPurpose.ExistenceFilterMismatchBloom
})
);
}
);
Expand Down Expand Up @@ -394,7 +399,11 @@ describeSpec('Existence Filters:', [], () => {
.watchSnapshots(2000)
// Re-run query is triggered.
.expectEvents(query1, { fromCache: true })
.expectActiveTargets({ query: query1, resumeToken: '' })
.expectActiveTargets({
query: query1,
resumeToken: '',
targetPurpose: TargetPurpose.ExistenceFilterMismatch
})
);
}
);
Expand Down Expand Up @@ -424,7 +433,11 @@ describeSpec('Existence Filters:', [], () => {
.watchSnapshots(2000)
// Re-run query is triggered.
.expectEvents(query1, { fromCache: true })
.expectActiveTargets({ query: query1, resumeToken: '' })
.expectActiveTargets({
query: query1,
resumeToken: '',
targetPurpose: TargetPurpose.ExistenceFilterMismatch
})
);
}
);
Expand Down Expand Up @@ -452,7 +465,11 @@ describeSpec('Existence Filters:', [], () => {
.watchSnapshots(2000)
// Re-run query is triggered.
.expectEvents(query1, { fromCache: true })
.expectActiveTargets({ query: query1, resumeToken: '' })
.expectActiveTargets({
query: query1,
resumeToken: '',
targetPurpose: TargetPurpose.ExistenceFilterMismatch
})
);
});

Expand Down
Loading