Skip to content

Commit 0d29adc

Browse files
authored
Fix docs unnecessarily undergo limbo resolution while resuming query bug (#7740)
1 parent 00235ba commit 0d29adc

File tree

8 files changed

+83
-20
lines changed

8 files changed

+83
-20
lines changed

.changeset/olive-dogs-play.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@firebase/firestore': patch
3+
'firebase': patch
4+
---
5+
6+
Fixed an issue in the local cache synchronization logic where all locally-cached documents that matched a resumed query would be unnecessarily re-downloaded; with the fix it now only downloads the documents that are known to be out-of-sync.
7+

packages/firestore/src/core/firestore_client.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -729,7 +729,7 @@ async function executeQueryFromCache(
729729
const viewDocChanges = view.computeDocChanges(queryResult.documents);
730730
const viewChange = view.applyChanges(
731731
viewDocChanges,
732-
/* updateLimboDocuments= */ false
732+
/* limboResolutionEnabled= */ false
733733
);
734734
result.resolve(viewChange.snapshot!);
735735
} catch (e) {

packages/firestore/src/core/sync_engine_impl.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ async function initializeViewAndComputeSnapshot(
368368
);
369369
const viewChange = view.applyChanges(
370370
viewDocChanges,
371-
/* updateLimboDocuments= */ syncEngineImpl.isPrimaryClient,
371+
/* limboResolutionEnabled= */ syncEngineImpl.isPrimaryClient,
372372
synthesizedTargetChange
373373
);
374374
updateTrackedLimbos(syncEngineImpl, targetId, viewChange.limboChanges);
@@ -1081,10 +1081,13 @@ async function applyDocChanges(
10811081

10821082
const targetChange =
10831083
remoteEvent && remoteEvent.targetChanges.get(queryView.targetId);
1084+
const targetIsPendingReset =
1085+
remoteEvent && remoteEvent.targetMismatches.get(queryView.targetId) != null;
10841086
const viewChange = queryView.view.applyChanges(
10851087
viewDocChanges,
1086-
/* updateLimboDocuments= */ syncEngineImpl.isPrimaryClient,
1087-
targetChange
1088+
/* limboResolutionEnabled= */ syncEngineImpl.isPrimaryClient,
1089+
targetChange,
1090+
targetIsPendingReset
10881091
);
10891092
updateTrackedLimbos(
10901093
syncEngineImpl,

packages/firestore/src/core/view.ts

+21-9
Original file line numberDiff line numberDiff line change
@@ -271,17 +271,21 @@ export class View {
271271
* Updates the view with the given ViewDocumentChanges and optionally updates
272272
* limbo docs and sync state from the provided target change.
273273
* @param docChanges - The set of changes to make to the view's docs.
274-
* @param updateLimboDocuments - Whether to update limbo documents based on
274+
* @param limboResolutionEnabled - Whether to update limbo documents based on
275275
* this change.
276276
* @param targetChange - A target change to apply for computing limbo docs and
277277
* sync state.
278+
* @param targetIsPendingReset - Whether the target is pending to reset due to
279+
* existence filter mismatch. If not explicitly specified, it is treated
280+
* equivalently to `false`.
278281
* @returns A new ViewChange with the given docs, changes, and sync state.
279282
*/
280283
// PORTING NOTE: The iOS/Android clients always compute limbo document changes.
281284
applyChanges(
282285
docChanges: ViewDocumentChanges,
283-
updateLimboDocuments: boolean,
284-
targetChange?: TargetChange
286+
limboResolutionEnabled: boolean,
287+
targetChange?: TargetChange,
288+
targetIsPendingReset?: boolean
285289
): ViewChange {
286290
debugAssert(
287291
!docChanges.needsRefill,
@@ -300,10 +304,18 @@ export class View {
300304
});
301305

302306
this.applyTargetChange(targetChange);
303-
const limboChanges = updateLimboDocuments
304-
? this.updateLimboDocuments()
305-
: [];
306-
const synced = this.limboDocuments.size === 0 && this.current;
307+
308+
targetIsPendingReset = targetIsPendingReset ?? false;
309+
const limboChanges =
310+
limboResolutionEnabled && !targetIsPendingReset
311+
? this.updateLimboDocuments()
312+
: [];
313+
314+
// We are at synced state if there is no limbo docs are waiting to be resolved, view is current
315+
// with the backend, and the query is not pending to reset due to existence filter mismatch.
316+
const synced =
317+
this.limboDocuments.size === 0 && this.current && !targetIsPendingReset;
318+
307319
const newSyncState = synced ? SyncState.Synced : SyncState.Local;
308320
const syncStateChanged = newSyncState !== this.syncState;
309321
this.syncState = newSyncState;
@@ -350,7 +362,7 @@ export class View {
350362
mutatedKeys: this.mutatedKeys,
351363
needsRefill: false
352364
},
353-
/* updateLimboDocuments= */ false
365+
/* limboResolutionEnabled= */ false
354366
);
355367
} else {
356368
// No effect, just return a no-op ViewChange.
@@ -458,7 +470,7 @@ export class View {
458470
this._syncedDocuments = queryResult.remoteKeys;
459471
this.limboDocuments = documentKeySet();
460472
const docChanges = this.computeDocChanges(queryResult.documents);
461-
return this.applyChanges(docChanges, /*updateLimboDocuments=*/ true);
473+
return this.applyChanges(docChanges, /* limboResolutionEnabled= */ true);
462474
}
463475

464476
/**

packages/firestore/test/unit/core/view.test.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -304,19 +304,19 @@ describe('View', () => {
304304
let changes = view.computeDocChanges(documentUpdates(doc1));
305305
let viewChange = view.applyChanges(
306306
changes,
307-
/* updateLimboDocuments= */ true
307+
/* limboResolutionEnabled= */ true
308308
);
309309
expect(viewChange.snapshot!.fromCache).to.be.true;
310310

311311
// Add doc2 to generate a snapshot. Doc1 is still missing.
312312
changes = view.computeDocChanges(documentUpdates(doc2));
313-
viewChange = view.applyChanges(changes, /* updateLimboDocuments= */ true);
313+
viewChange = view.applyChanges(changes, /* limboResolutionEnabled= */ true);
314314
expect(viewChange.snapshot!.fromCache).to.be.true;
315315

316316
// Add doc2 to the backend's result set.
317317
viewChange = view.applyChanges(
318318
changes,
319-
/* updateLimboDocuments= */ true,
319+
/* limboResolutionEnabled= */ true,
320320
updateMapping(version(0), [doc2], [], [], /* current= */ true)
321321
);
322322
// We are CURRENT but doc1 is in limbo.
@@ -325,7 +325,7 @@ describe('View', () => {
325325
// Add doc1 to the backend's result set.
326326
viewChange = view.applyChanges(
327327
changes,
328-
/* updateLimboDocuments= */ true,
328+
/* limboResolutionEnabled= */ true,
329329
updateMapping(version(0), [doc1], [], [], /* current= */ true)
330330
);
331331
expect(viewChange.snapshot!.fromCache).to.be.false;

packages/firestore/test/unit/local/query_engine.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ function genericQueryEngineTest(
253253
const viewDocChanges = view.computeDocChanges(docs);
254254
return view.applyChanges(
255255
viewDocChanges,
256-
/*updateLimboDocuments=*/ true
256+
/* limboResolutionEnabled= */ true
257257
).snapshot!.docs;
258258
});
259259
});

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

+42-2
Original file line numberDiff line numberDiff line change
@@ -889,6 +889,7 @@ describeSpec('Limbo Documents:', [], () => {
889889
// to just send the docBs with an existence filter with a count of 3.
890890
.watchSends({ affects: [query1] }, docB1, docB2, docB3)
891891
.watchFilters([query1], [docB1.key, docB2.key, docB3.key])
892+
.watchCurrents(query1, 'resume-token-1001')
892893
.watchSnapshots(1001)
893894
.expectEvents(query1, {
894895
added: [docB1, docB2, docB3],
@@ -927,6 +928,46 @@ describeSpec('Limbo Documents:', [], () => {
927928
}
928929
);
929930

931+
// Regression test for the bug in https://github.com/firebase/firebase-android-sdk/issues/5357
932+
specTest(
933+
'Limbo resolution should wait for full re-query result if there is an existence filter mismatch ',
934+
[],
935+
() => {
936+
const query1 = query('collection');
937+
const docA = doc('collection/a', 1000, { v: 1 });
938+
const docB = doc('collection/b', 1000, { v: 2 });
939+
return (
940+
spec()
941+
.userListens(query1)
942+
.watchAcksFull(query1, 1000, docA, docB)
943+
.expectEvents(query1, { added: [docA, docB] })
944+
// Simulate that the client loses network connection.
945+
.disableNetwork()
946+
.expectEvents(query1, { fromCache: true })
947+
.enableNetwork()
948+
.restoreListen(query1, 'resume-token-1000')
949+
.watchAcks(query1)
950+
// DocB is deleted in the next sync.
951+
.watchFilters([query1], [docA.key])
952+
.watchCurrents(query1, 'resume-token-2000')
953+
.watchSnapshots(2000)
954+
.expectActiveTargets({
955+
query: query1,
956+
resumeToken: '',
957+
targetPurpose: TargetPurpose.ExistenceFilterMismatch
958+
})
959+
.watchRemoves(query1)
960+
.watchAcksFull(query1, 3000, docA)
961+
// Only the deleted doc is moved to limbo after re-query result.
962+
.expectLimboDocs(docB.key)
963+
.ackLimbo(3000, deletedDoc(docB.key.toString(), 3000))
964+
.expectLimboDocs()
965+
.expectEvents(query1, {
966+
removed: [docB]
967+
})
968+
);
969+
}
970+
);
930971
specTest(
931972
'Limbo resolution throttling with bloom filter application',
932973
[],
@@ -968,6 +1009,7 @@ describeSpec('Limbo Documents:', [], () => {
9681009
[docB1.key, docB2.key, docB3.key],
9691010
bloomFilterProto
9701011
)
1012+
.watchCurrents(query1, 'resume-token-1001')
9711013
.watchSnapshots(1001)
9721014
.expectEvents(query1, {
9731015
added: [docB1, docB2, docB3],
@@ -978,8 +1020,6 @@ describeSpec('Limbo Documents:', [], () => {
9781020
// existence filter mismatch. Bloom filter checks membership of the
9791021
// docs, and filters out docAs, while docBs returns true. Number of
9801022
// existing docs matches the expected count, so skip the re-query.
981-
.watchCurrents(query1, 'resume-token-1002')
982-
.watchSnapshots(1002)
9831023
// The docAs are now in limbo; the client begins limbo resolution.
9841024
.expectLimboDocs(docA1.key, docA2.key)
9851025
.expectEnqueuedLimboDocs(docA3.key)

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

+1
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,7 @@ describeSpec('Limits:', [], () => {
343343
// out of sync.
344344
.watchSends({ affects: [limitQuery] }, secondDocument)
345345
.watchFilters([limitQuery], [secondDocument.key])
346+
.watchCurrents(limitQuery, 'resume-token-1004')
346347
.watchSnapshots(1004)
347348
.expectActiveTargets({
348349
query: limitQuery,

0 commit comments

Comments
 (0)