Skip to content

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

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

Merged
merged 5 commits into from
Mar 9, 2023
Merged
Show file tree
Hide file tree
Changes from 4 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) {
newTargetData = newTargetData
.withResumeToken(
ByteString.EMPTY_BYTE_STRING,
Expand Down
9 changes: 8 additions & 1 deletion packages/firestore/src/local/target_data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,17 @@ export const enum TargetPurpose {
Listen,

/**
* The query target was used to refill a query after an existence filter mismatch.
* The query target was used to refill a query after an existence filter
* mismatch.
*/
ExistenceFilterMismatch,

/**
* The query target was used if the query is the result of a false positive in
* the bloom filter.
*/
ExistenceFilterMismatchBloom,

/** The query target was used to resolve a limbo document. */
LimboResolution
}
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
6 changes: 3 additions & 3 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 Down Expand Up @@ -615,7 +615,7 @@ function raiseWatchSnapshot(
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.bitCount === 0) {
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
9 changes: 9 additions & 0 deletions packages/firestore/test/unit/remote/remote_event.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,9 @@ describe('RemoteEvent', () => {
expectTargetChangeEquals(event.targetChanges.get(1)!, expected);
});

// TODO(Mila): Add test cases for existence filter with bloom filter, one will
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Put b/272564458 in the "TODO" rather than "Mila".

// skip the re-query, one will yield false positive result and clears target
// mapping. b/272564458
it('existence filters clears target mapping', () => {
const targets = listens(1, 2);

Expand Down Expand Up @@ -459,6 +462,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(
TargetPurpose.ExistenceFilterMismatch
);
expect(event.targetChanges.size).to.equal(1);

const expected = updateMapping(
Expand Down Expand Up @@ -499,6 +505,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