Skip to content

Commit c0088ad

Browse files
authored
Merge 03a9c22 into 92d6e50
2 parents 92d6e50 + 03a9c22 commit c0088ad

File tree

4 files changed

+181
-32
lines changed

4 files changed

+181
-32
lines changed

.changeset/eight-bananas-nail.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@firebase/firestore': patch
3+
---
4+
5+
Fixes a bug where local cache inconsistencies were unnecessarily being resolved.

packages/firestore/src/core/sync_engine_impl.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import { MaybeDocument, NoDocument } from '../model/document';
5151
import { DocumentKey } from '../model/document_key';
5252
import { Mutation } from '../model/mutation';
5353
import { MutationBatchResult } from '../model/mutation_batch';
54+
import { ResourcePath } from '../model/path';
5455
import { RemoteEvent, TargetChange } from '../remote/remote_event';
5556
import {
5657
canUseNetwork,
@@ -208,9 +209,14 @@ class SyncEngineImpl implements SyncEngine {
208209
queriesByTarget = new Map<TargetId, Query[]>();
209210
/**
210211
* The keys of documents that are in limbo for which we haven't yet started a
211-
* limbo resolution query.
212+
* limbo resolution query. The strings in this set are the result of calling
213+
* `key.path.canonicalString()` where `key` is a `DocumentKey` object.
214+
*
215+
* The `Set` type was chosen because it provides efficient lookup and removal
216+
* of arbitrary elements and it also maintains insertion order, providing the
217+
* desired queue-like FIFO semantics.
212218
*/
213-
enqueuedLimboResolutions: DocumentKey[] = [];
219+
enqueuedLimboResolutions = new Set<string>();
214220
/**
215221
* Keeps track of the target ID for each document that is in limbo with an
216222
* active target.
@@ -876,6 +882,8 @@ function removeLimboTarget(
876882
syncEngineImpl: SyncEngineImpl,
877883
key: DocumentKey
878884
): void {
885+
syncEngineImpl.enqueuedLimboResolutions.delete(key.path.canonicalString());
886+
879887
// It's possible that the target already got removed because the query failed. In that case,
880888
// the key won't exist in `limboTargetsByKey`. Only do the cleanup if we still have the target.
881889
const limboTargetId = syncEngineImpl.activeLimboTargetsByKey.get(key);
@@ -925,9 +933,13 @@ function trackLimboChange(
925933
limboChange: AddedLimboDocument
926934
): void {
927935
const key = limboChange.key;
928-
if (!syncEngineImpl.activeLimboTargetsByKey.get(key)) {
936+
const keyString = key.path.canonicalString();
937+
if (
938+
!syncEngineImpl.activeLimboTargetsByKey.get(key) &&
939+
!syncEngineImpl.enqueuedLimboResolutions.has(keyString)
940+
) {
929941
logDebug(LOG_TAG, 'New document in limbo: ' + key);
930-
syncEngineImpl.enqueuedLimboResolutions.push(key);
942+
syncEngineImpl.enqueuedLimboResolutions.add(keyString);
931943
pumpEnqueuedLimboResolutions(syncEngineImpl);
932944
}
933945
}
@@ -942,11 +954,14 @@ function trackLimboChange(
942954
*/
943955
function pumpEnqueuedLimboResolutions(syncEngineImpl: SyncEngineImpl): void {
944956
while (
945-
syncEngineImpl.enqueuedLimboResolutions.length > 0 &&
957+
syncEngineImpl.enqueuedLimboResolutions.size > 0 &&
946958
syncEngineImpl.activeLimboTargetsByKey.size <
947959
syncEngineImpl.maxConcurrentLimboResolutions
948960
) {
949-
const key = syncEngineImpl.enqueuedLimboResolutions.shift()!;
961+
const keyString = syncEngineImpl.enqueuedLimboResolutions.values().next()
962+
.value;
963+
syncEngineImpl.enqueuedLimboResolutions.delete(keyString);
964+
const key = new DocumentKey(ResourcePath.fromString(keyString));
950965
const limboTargetId = syncEngineImpl.limboTargetIdGenerator.next();
951966
syncEngineImpl.activeLimboResolutionsByTarget.set(
952967
limboTargetId,
@@ -979,7 +994,7 @@ export function syncEngineGetActiveLimboDocumentResolutions(
979994
// Visible for testing
980995
export function syncEngineGetEnqueuedLimboDocumentResolutions(
981996
syncEngine: SyncEngine
982-
): DocumentKey[] {
997+
): Set<string> {
983998
const syncEngineImpl = debugCast(syncEngine, SyncEngineImpl);
984999
return syncEngineImpl.enqueuedLimboResolutions;
9851000
}

packages/firestore/test/unit/specs/limbo_spec.test.ts

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -922,4 +922,149 @@ describeSpec('Limbo Documents:', [], () => {
922922
);
923923
}
924924
);
925+
926+
specTest(
927+
'A limbo resolution for a document should not be started if one is already active',
928+
[],
929+
() => {
930+
const doc1 = doc('collection/doc', 1000, { key: 1 });
931+
const fullQuery = query('collection');
932+
const filteredQuery1 = query('collection', filter('key', '==', 1));
933+
const filteredQuery2 = query('collection', filter('key', '>=', 1));
934+
935+
return (
936+
spec()
937+
.withGCEnabled(false)
938+
939+
// Start a limbo resolution listen for a document (doc1).
940+
.userListens(fullQuery)
941+
.watchAcksFull(fullQuery, 1000, doc1)
942+
.expectEvents(fullQuery, { added: [doc1] })
943+
.userUnlistens(fullQuery)
944+
.userListens(filteredQuery1)
945+
.expectEvents(filteredQuery1, { added: [doc1], fromCache: true })
946+
.watchAcksFull(filteredQuery1, 1001)
947+
.expectLimboDocs(doc1.key)
948+
.expectEnqueuedLimboDocs()
949+
950+
// Put doc1 into limbo in a different query; verify that another limbo
951+
// resolution is neither started nor enqueued.
952+
.userListens(filteredQuery2)
953+
.expectEvents(filteredQuery2, { added: [doc1], fromCache: true })
954+
.watchAcksFull(filteredQuery2, 1002)
955+
.expectLimboDocs(doc1.key)
956+
.expectEnqueuedLimboDocs()
957+
);
958+
}
959+
);
960+
961+
specTest(
962+
'A limbo resolution for a document should not be enqueued if one is already enqueued',
963+
[],
964+
() => {
965+
const doc1 = doc('collection1/doc1', 1000, { key: 1 });
966+
const fullQuery1 = query('collection1');
967+
const filteredQuery1 = query('collection1', filter('key', '==', 1));
968+
const doc2 = doc('collection2/doc2', 1000, { key: 2 });
969+
const fullQuery2 = query('collection2');
970+
const filteredQuery2a = query('collection2', filter('key', '==', 2));
971+
const filteredQuery2b = query('collection2', filter('key', '>=', 2));
972+
973+
return (
974+
spec()
975+
.withGCEnabled(false)
976+
.withMaxConcurrentLimboResolutions(1)
977+
978+
// Max out the number of active limbo resolutions.
979+
.userListens(fullQuery1)
980+
.watchAcksFull(fullQuery1, 1000, doc1)
981+
.expectEvents(fullQuery1, { added: [doc1] })
982+
.userUnlistens(fullQuery1)
983+
.userListens(filteredQuery1)
984+
.expectEvents(filteredQuery1, { added: [doc1], fromCache: true })
985+
.watchAcksFull(filteredQuery1, 1001)
986+
.expectLimboDocs(doc1.key)
987+
988+
// Start a limbo resolution listen for a different document (doc2).
989+
.userListens(fullQuery2)
990+
.watchAcksFull(fullQuery2, 1002, doc2)
991+
.expectEvents(fullQuery2, { added: [doc2] })
992+
.userUnlistens(fullQuery2)
993+
.userListens(filteredQuery2a)
994+
.expectEvents(filteredQuery2a, { added: [doc2], fromCache: true })
995+
.watchAcksFull(filteredQuery2a, 1003)
996+
.expectLimboDocs(doc1.key)
997+
.expectEnqueuedLimboDocs(doc2.key)
998+
999+
// Put doc2 into limbo in a different query and verify that it's not
1000+
// added to the limbo resolution queue again.
1001+
.userListens(filteredQuery2b)
1002+
.expectEvents(filteredQuery2b, { added: [doc2], fromCache: true })
1003+
.watchAcksFull(filteredQuery2b, 1004)
1004+
.expectLimboDocs(doc1.key)
1005+
.expectEnqueuedLimboDocs(doc2.key)
1006+
);
1007+
}
1008+
);
1009+
1010+
specTest(
1011+
'A limbo resolution for a document should be removed from the queue when the last query listen stops',
1012+
[],
1013+
() => {
1014+
const doc1 = doc('collection1/doc', 1000, { key: 1 });
1015+
const fullQuery1 = query('collection1');
1016+
const filteredQuery1 = query('collection1', filter('key', '==', 1));
1017+
1018+
const doc2 = doc('collection2/doc', 1000, { key: 2 });
1019+
const fullQuery2 = query('collection2');
1020+
const filteredQuery2a = query('collection2', filter('key', '==', 2));
1021+
const filteredQuery2b = query('collection2', filter('key', '>=', 2));
1022+
1023+
return (
1024+
spec()
1025+
.withGCEnabled(false)
1026+
.withMaxConcurrentLimboResolutions(1)
1027+
1028+
// Max out the number of active limbo resolutions.
1029+
.userListens(fullQuery1)
1030+
.watchAcksFull(fullQuery1, 1000, doc1)
1031+
.expectEvents(fullQuery1, { added: [doc1] })
1032+
.userUnlistens(fullQuery1)
1033+
.userListens(filteredQuery1)
1034+
.expectEvents(filteredQuery1, { added: [doc1], fromCache: true })
1035+
.watchAcksFull(filteredQuery1, 1001)
1036+
.expectLimboDocs(doc1.key)
1037+
1038+
// Enqueue a limbo resolution for doc2.
1039+
.userListens(fullQuery2)
1040+
.watchAcksFull(fullQuery2, 1002, doc2)
1041+
.expectEvents(fullQuery2, { added: [doc2] })
1042+
.userUnlistens(fullQuery2)
1043+
.userListens(filteredQuery2a)
1044+
.expectEvents(filteredQuery2a, { added: [doc2], fromCache: true })
1045+
.watchAcksFull(filteredQuery2a, 1003)
1046+
.expectLimboDocs(doc1.key)
1047+
.expectEnqueuedLimboDocs(doc2.key)
1048+
1049+
// Start another query that puts doc2 into limbo again.
1050+
.userListens(filteredQuery2b)
1051+
.expectEvents(filteredQuery2b, { added: [doc2], fromCache: true })
1052+
.watchAcksFull(filteredQuery2b, 1004)
1053+
.expectLimboDocs(doc1.key)
1054+
.expectEnqueuedLimboDocs(doc2.key)
1055+
1056+
// Stop one of the queries that enqueued a limbo resolution for doc2;
1057+
// verify that doc2 is not removed from the limbo resolution queue.
1058+
.userUnlistens(filteredQuery2b)
1059+
.expectLimboDocs(doc1.key)
1060+
.expectEnqueuedLimboDocs(doc2.key)
1061+
1062+
// Stop the other query that enqueued a limbo resolution for doc2;
1063+
// verify that doc2 *is* removed from the limbo resolution queue.
1064+
.userUnlistens(filteredQuery2a)
1065+
.expectLimboDocs(doc1.key)
1066+
.expectEnqueuedLimboDocs()
1067+
);
1068+
}
1069+
);
9251070
});

packages/firestore/test/unit/specs/spec_test_runner.ts

Lines changed: 9 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,6 @@ import { primitiveComparator } from '../../../src/util/misc';
118118
import { forEach, objectSize } from '../../../src/util/obj';
119119
import { ObjectMap } from '../../../src/util/obj_map';
120120
import { Deferred, sequence } from '../../../src/util/promise';
121-
import { SortedSet } from '../../../src/util/sorted_set';
122121
import {
123122
byteStringFromString,
124123
deletedDoc,
@@ -1008,31 +1007,16 @@ abstract class TestRunner {
10081007
}
10091008

10101009
private validateEnqueuedLimboDocs(): void {
1011-
let actualLimboDocs = new SortedSet<DocumentKey>(DocumentKey.comparator);
1012-
syncEngineGetEnqueuedLimboDocumentResolutions(this.syncEngine).forEach(
1013-
key => {
1014-
actualLimboDocs = actualLimboDocs.add(key);
1015-
}
1010+
const actualLimboDocs = Array.from(
1011+
syncEngineGetEnqueuedLimboDocumentResolutions(this.syncEngine)
1012+
);
1013+
const expectedLimboDocs = Array.from(this.expectedEnqueuedLimboDocs, key =>
1014+
key.path.canonicalString()
1015+
);
1016+
expect(actualLimboDocs).to.have.members(
1017+
expectedLimboDocs,
1018+
'The set of enqueued limbo documents is incorrect'
10161019
);
1017-
let expectedLimboDocs = new SortedSet<DocumentKey>(DocumentKey.comparator);
1018-
this.expectedEnqueuedLimboDocs.forEach(key => {
1019-
expectedLimboDocs = expectedLimboDocs.add(key);
1020-
});
1021-
actualLimboDocs.forEach(key => {
1022-
expect(expectedLimboDocs.has(key)).to.equal(
1023-
true,
1024-
`Found enqueued limbo doc ${key.toString()}, but it was not in ` +
1025-
`the set of expected enqueued limbo documents ` +
1026-
`(${expectedLimboDocs.toString()})`
1027-
);
1028-
});
1029-
expectedLimboDocs.forEach(key => {
1030-
expect(actualLimboDocs.has(key)).to.equal(
1031-
true,
1032-
`Expected doc ${key.toString()} to be enqueued for limbo resolution, ` +
1033-
`but it was not in the queue (${actualLimboDocs.toString()})`
1034-
);
1035-
});
10361020
}
10371021

10381022
private async validateActiveTargets(): Promise<void> {

0 commit comments

Comments
 (0)