Skip to content

Commit b11cf61

Browse files
Merge master into release
2 parents 1b9a21d + 146b699 commit b11cf61

File tree

8 files changed

+763
-687
lines changed

8 files changed

+763
-687
lines changed

.github/workflows/canary-deploy.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,15 @@ jobs:
7676
NPM_TOKEN_APP_CHECK_COMPAT: ${{ secrets.NPM_TOKEN_APP_CHECK_COMPAT }}
7777
NPM_TOKEN_API_DOCUMENTER: ${{ secrets.NPM_TOKEN_API_DOCUMENTER }}
7878
CI: true
79+
- name: Launch E2E tests workflow
80+
# Trigger e2e-test.yml
81+
run: |
82+
VERSION_SCRIPT="const pkg = require('./packages/firebase/package.json'); console.log(pkg.version);"
83+
VERSION_OR_TAG=`node -e "${VERSION_SCRIPT}"`
84+
OSS_BOT_GITHUB_TOKEN=${{ secrets.OSS_BOT_GITHUB_TOKEN }}
85+
curl -X POST \
86+
-H "Content-Type:application/json" \
87+
-H "Accept:application/vnd.github.v3+json" \
88+
-H "Authorization:Bearer $OSS_BOT_GITHUB_TOKEN" \
89+
-d "{\"event_type\":\"canary-tests\", \"client_payload\":{\"versionOrTag\":\"$VERSION_OR_TAG\"}}" \
90+
https://api.github.com/repos/firebase/firebase-js-sdk/dispatches

.github/workflows/e2e-test.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name: E2E Smoke Tests
33
# Allows REST trigger. Currently triggered by release-cli script during a staging run.
44
on:
55
repository_dispatch:
6-
types: [staging-tests]
6+
types: [staging-tests,canary-tests]
77

88
jobs:
99
test:
@@ -62,6 +62,8 @@ jobs:
6262
- name: Tests succeeded
6363
if: success()
6464
run: node scripts/ci/notify-test-result.js success
65+
# we don't want THIS step erroring to trigger the failure notification
66+
continue-on-error: true
6567
env:
6668
WEBHOOK_URL: ${{ secrets.JSCORE_CHAT_WEBHOOK_URL }}
6769
RELEASE_TRACKER_URL: ${{ secrets.RELEASE_TRACKER_URL }}

.github/workflows/test-all.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ on:
77
env:
88
# make chromedriver detect installed Chrome version and download the corresponding driver
99
DETECT_CHROMEDRIVER_VERSION: true
10-
artifactRetentionDays: 2
10+
artifactRetentionDays: 14
1111

1212
jobs:
1313
build:

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: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ import {
6262
toChangesArray,
6363
toDataArray,
6464
toIds,
65+
PERSISTENCE_MODE_UNSPECIFIED,
6566
withEmptyTestCollection,
6667
withRetry,
6768
withTestCollection,
@@ -2106,16 +2107,18 @@ apiDescribe('Queries', persistence => {
21062107
snapshot => snapshot.ref
21072108
);
21082109

2109-
// Delete 50 of the 100 documents. Use a WriteBatch, rather than
2110-
// deleteDoc(), to avoid affecting the local cache.
2110+
// Delete 50 of the 100 documents. Use a different Firestore
2111+
// instance to avoid affecting the local cache.
21112112
const deletedDocumentIds = new Set<string>();
2112-
const writeBatchForDocumentDeletes = writeBatch(db);
2113-
for (let i = 0; i < createdDocuments.length; i += 2) {
2114-
const documentToDelete = createdDocuments[i];
2115-
writeBatchForDocumentDeletes.delete(documentToDelete);
2116-
deletedDocumentIds.add(documentToDelete.id);
2117-
}
2118-
await writeBatchForDocumentDeletes.commit();
2113+
await withTestDb(PERSISTENCE_MODE_UNSPECIFIED, async db2 => {
2114+
const batch = writeBatch(db2);
2115+
for (let i = 0; i < createdDocuments.length; i += 2) {
2116+
const documentToDelete = doc(db2, createdDocuments[i].path);
2117+
batch.delete(documentToDelete);
2118+
deletedDocumentIds.add(documentToDelete.id);
2119+
}
2120+
await batch.commit();
2121+
});
21192122

21202123
// Wait for 10 seconds, during which Watch will stop tracking the
21212124
// query and will send an existence filter rather than "delete"
@@ -2258,11 +2261,12 @@ apiDescribe('Queries', persistence => {
22582261
);
22592262

22602263
// Delete one of the documents so that the next call to getDocs() will
2261-
// experience an existence filter mismatch. Use a WriteBatch, rather
2262-
// than deleteDoc(), to avoid affecting the local cache.
2263-
const writeBatchForDocumentDeletes = writeBatch(db);
2264-
writeBatchForDocumentDeletes.delete(doc(coll, 'DocumentToDelete'));
2265-
await writeBatchForDocumentDeletes.commit();
2264+
// experience an existence filter mismatch. Use a different Firestore
2265+
// instance to avoid affecting the local cache.
2266+
const documentToDelete = doc(coll, 'DocumentToDelete');
2267+
await withTestDb(PERSISTENCE_MODE_UNSPECIFIED, async db2 => {
2268+
await deleteDoc(doc(db2, documentToDelete.path));
2269+
});
22662270

22672271
// Wait for 10 seconds, during which Watch will stop tracking the query
22682272
// and will send an existence filter rather than "delete" events when
@@ -2278,7 +2282,7 @@ apiDescribe('Queries', persistence => {
22782282
documentSnapshot => documentSnapshot.id
22792283
);
22802284
const testDocIdsMinusDeletedDocId = testDocIds.filter(
2281-
documentId => documentId !== 'DocumentToDelete'
2285+
documentId => documentId !== documentToDelete.id
22822286
);
22832287
expect(snapshot2DocumentIds, 'snapshot2DocumentIds').to.have.members(
22842288
testDocIdsMinusDeletedDocId
@@ -2309,20 +2313,17 @@ apiDescribe('Queries', persistence => {
23092313
// is if there is a false positive when testing for 'DocumentToDelete'
23102314
// in the bloom filter. So verify that the bloom filter application is
23112315
// successful, unless there was a false positive.
2312-
const isFalsePositive = bloomFilter.mightContain!(
2313-
`${coll.path}/DocumentToDelete`
2314-
);
2316+
const isFalsePositive = bloomFilter.mightContain(documentToDelete);
23152317
expect(bloomFilter.applied, 'bloomFilter.applied').to.equal(
23162318
!isFalsePositive
23172319
);
23182320

23192321
// Verify that the bloom filter contains the document paths with complex
23202322
// Unicode characters.
2321-
for (const testDocId of testDocIdsMinusDeletedDocId) {
2322-
const testDocPath = `${coll.path}/${testDocId}`;
2323+
for (const testDoc of snapshot2.docs.map(snapshot => snapshot.ref)) {
23232324
expect(
2324-
bloomFilter.mightContain!(testDocPath),
2325-
`bloomFilter.mightContain('${testDocPath}')`
2325+
bloomFilter.mightContain(testDoc),
2326+
`bloomFilter.mightContain('${testDoc.path}')`
23262327
).to.be.true;
23272328
}
23282329
});

0 commit comments

Comments
 (0)