Skip to content

Commit f75534f

Browse files
committed
add retry
1 parent 86870f9 commit f75534f

File tree

2 files changed

+92
-67
lines changed

2 files changed

+92
-67
lines changed

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

Lines changed: 82 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -2067,73 +2067,98 @@ apiDescribe('Queries', (persistence: boolean) => {
20672067
// query is resumed is fixed.
20682068
(USE_EMULATOR || !persistence ? it.skip : it.only)(
20692069
'resuming a query should use bloom filter to avoid full requery',
2070-
() => {
2071-
// TODO(dconeybe) Add a loop to retry 2 or 3 times if the bloom filter
2072-
// experienced a false positive. Since there is a non-zero chance of a
2073-
// false positive, and the chance of experiencing two in a row is
2074-
// negligible, retrying 2 or 3 times should result in basically zero
2075-
// false positives.
2076-
2070+
async () => {
20772071
// Create 100 documents in a new collection.
20782072
const testDocs: { [key: string]: object } = {};
20792073
for (let i = 1; i <= 100; i++) {
20802074
testDocs['doc' + i] = { key: i };
20812075
}
20822076

2083-
return withTestCollection(persistence, testDocs, async (coll, db) => {
2084-
// Run a query to populate the local cache with the 100 documents and a
2085-
// resume token.
2086-
const snapshot1 = await getDocs(coll);
2087-
expect(snapshot1.size).to.equal(100);
2088-
2089-
// Delete 50 of the 100 documents. Do this in a transaction, rather than
2090-
// deleteDoc(), to avoid affecting the local cache.
2091-
await runTransaction(db, async txn => {
2092-
for (let i = 1; i <= 50; i++) {
2093-
txn.delete(doc(coll, 'doc' + i));
2077+
let attemptNumber = 0;
2078+
while (true) {
2079+
attemptNumber++;
2080+
const bloomFilterApplied = await withTestCollection(
2081+
persistence,
2082+
testDocs,
2083+
async (coll, db) => {
2084+
// Run a query to populate the local cache with the 100 documents and
2085+
// a resume token.
2086+
const snapshot1 = await getDocs(coll);
2087+
expect(snapshot1.size).to.equal(100);
2088+
2089+
// Delete 50 of the 100 documents. Do this in a transaction, rather
2090+
// than deleteDoc(), to avoid affecting the local cache.
2091+
await runTransaction(db, async txn => {
2092+
for (let i = 1; i <= 50; i++) {
2093+
txn.delete(doc(coll, 'doc' + i));
2094+
}
2095+
});
2096+
2097+
// Wait for 10 seconds, during which Watch will stop tracking the
2098+
// query and will send an existence filter rather than "delete" events
2099+
// when the query is resumed.
2100+
await new Promise(resolve => setTimeout(resolve, 10000));
2101+
2102+
// Resume the query and expect to get a snapshot with the 50 remaining
2103+
// documents. Use some internal testing hooks to "capture" the
2104+
// existence filter mismatches to later verify that Watch sent a bloom
2105+
// filter, and it was used to void the full requery.
2106+
const existenceFilterMismatches =
2107+
await captureExistenceFilterMismatches(async () => {
2108+
const snapshot2 = await getDocs(coll);
2109+
expect(snapshot2.size).to.equal(50);
2110+
});
2111+
2112+
// Verify that upon resuming the query that Watch sent an existence
2113+
// filter that included a bloom filter, and that that bloom filter was
2114+
// successfully used to avoid a full requery.
2115+
// TODO(b/NNNNNNNN) Replace this "if" condition with !USE_EMULATOR
2116+
// once the feature has been deployed to production. Note that there
2117+
// are no plans to implement the bloom filter in the existence filter
2118+
// responses sent from the Firestore emulator.
2119+
if (TARGET_BACKEND === 'nightly') {
2120+
expect(existenceFilterMismatches).to.have.length(1);
2121+
const existenceFilterMismatch = existenceFilterMismatches[0];
2122+
expect(existenceFilterMismatch.actualCount).to.equal(100);
2123+
expect(existenceFilterMismatch.expectedCount).to.equal(50);
2124+
const bloomFilter = existenceFilterMismatch.bloomFilter;
2125+
if (!bloomFilter) {
2126+
expect.fail('existence filter should have had a bloom filter');
2127+
throw new Error('should never get here');
2128+
}
2129+
2130+
// Although statistically rare, it _is_ possible to get a legitimate
2131+
// false positive when checking the bloom filter. If this happens,
2132+
// then retry the test with a different set of documents, and only
2133+
// fail if the retry _also_ experiences a false positive.
2134+
if (!bloomFilter.applied) {
2135+
if (attemptNumber < 2) {
2136+
return false;
2137+
} else {
2138+
expect.fail(
2139+
'bloom filter false positive occurred ' +
2140+
'multiple times; this is statistically ' +
2141+
'improbable and should be investigated.'
2142+
);
2143+
}
2144+
}
2145+
2146+
expect(bloomFilter.bitmapLength).to.be.above(0);
2147+
expect(bloomFilter.hashCount).to.be.above(0);
2148+
expect(bloomFilter.padding).to.be.above(0);
2149+
expect(bloomFilter.padding).to.be.below(8);
2150+
}
2151+
2152+
return true;
20942153
}
2095-
});
2154+
);
20962155

2097-
// Wait for 10 seconds, during which Watch will stop tracking the query
2098-
// and will send an existence filter rather than "delete" events when
2099-
// the query is resumed.
2100-
await new Promise(resolve => setTimeout(resolve, 10000));
2101-
2102-
// Resume the query and expect to get a snapshot with the 50 remaining
2103-
// documents. Use some internal testing hooks to "capture" the existence
2104-
// filter mismatches to later verify that Watch sent a bloom filter and
2105-
// it was used to void the full requery.
2106-
const existenceFilterMismatches =
2107-
await captureExistenceFilterMismatches(async () => {
2108-
const snapshot2 = await getDocs(coll);
2109-
expect(snapshot2.size).to.equal(50);
2110-
});
2111-
2112-
// Verify that upon resuming the query that Watch sent an existence
2113-
// filter that included a bloom filter, and that that bloom filter was
2114-
// successfully used to avoid a full requery.
2115-
// TODO(b/NNNNNNNN) Replace this "if" condition with !USE_EMULATOR once
2116-
// the feature has been deployed to production. Note that there are no
2117-
// plans to add bloom filter support to the Firestore emulator.
2118-
if (TARGET_BACKEND === 'nightly') {
2119-
expect(existenceFilterMismatches).to.have.length(1);
2120-
const existenceFilterMismatch = existenceFilterMismatches[0];
2121-
expect(existenceFilterMismatch.actualCount).to.equal(100);
2122-
expect(existenceFilterMismatch.expectedCount).to.equal(50);
2123-
const bloomFilter = existenceFilterMismatch.bloomFilter;
2124-
if (!bloomFilter) {
2125-
expect.fail('existence filter should have had a bloom filter');
2126-
throw new Error('should never get here');
2127-
}
2128-
expect(bloomFilter.applied).to.equal(true);
2129-
expect(bloomFilter.bitmapLength).to.be.above(0);
2130-
expect(bloomFilter.hashCount).to.be.above(0);
2131-
expect(bloomFilter.padding).to.be.above(0);
2132-
expect(bloomFilter.padding).to.be.below(8);
2156+
if (bloomFilterApplied) {
2157+
break;
21332158
}
2134-
});
2159+
}
21352160
}
2136-
).timeout('30s');
2161+
).timeout('90s');
21372162
});
21382163

21392164
function verifyDocumentChange<T>(

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

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -170,13 +170,13 @@ export function withTestDbs(
170170
fn
171171
);
172172
}
173-
export async function withTestDbsSettings(
173+
export async function withTestDbsSettings<T>(
174174
persistence: boolean,
175175
projectId: string,
176176
settings: PrivateSettings,
177177
numDbs: number,
178-
fn: (db: Firestore[]) => Promise<void>
179-
): Promise<void> {
178+
fn: (db: Firestore[]) => Promise<T>
179+
): Promise<T> {
180180
if (numDbs === 0) {
181181
throw new Error("Can't test with no databases");
182182
}
@@ -192,7 +192,7 @@ export async function withTestDbsSettings(
192192
}
193193

194194
try {
195-
await fn(dbs);
195+
return await fn(dbs);
196196
} finally {
197197
for (const db of dbs) {
198198
await terminate(db);
@@ -282,11 +282,11 @@ export function withTestDocAndInitialData(
282282
});
283283
}
284284

285-
export function withTestCollection(
285+
export function withTestCollection<T>(
286286
persistence: boolean,
287287
docs: { [key: string]: DocumentData },
288-
fn: (collection: CollectionReference, db: Firestore) => Promise<void>
289-
): Promise<void> {
288+
fn: (collection: CollectionReference, db: Firestore) => Promise<T>
289+
): Promise<T> {
290290
return withTestCollectionSettings(persistence, DEFAULT_SETTINGS, docs, fn);
291291
}
292292

@@ -299,12 +299,12 @@ export function withEmptyTestCollection(
299299

300300
// TODO(mikelehen): Once we wipe the database between tests, we can probably
301301
// return the same collection every time.
302-
export function withTestCollectionSettings(
302+
export function withTestCollectionSettings<T>(
303303
persistence: boolean,
304304
settings: PrivateSettings,
305305
docs: { [key: string]: DocumentData },
306-
fn: (collection: CollectionReference, db: Firestore) => Promise<void>
307-
): Promise<void> {
306+
fn: (collection: CollectionReference, db: Firestore) => Promise<T>
307+
): Promise<T> {
308308
return withTestDbsSettings(
309309
persistence,
310310
DEFAULT_PROJECT_ID,

0 commit comments

Comments
 (0)