Skip to content

Commit a1d363b

Browse files
Don't persist documents that we already have (#720)
1 parent 8bb5912 commit a1d363b

File tree

5 files changed

+68
-332
lines changed

5 files changed

+68
-332
lines changed

firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalStore.java

Lines changed: 23 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -325,54 +325,41 @@ public SnapshotVersion getLastRemoteSnapshotVersion() {
325325
* <p>LocalDocuments are re-calculated if there are remaining mutations in the queue.
326326
*/
327327
public ImmutableSortedMap<DocumentKey, MaybeDocument> applyRemoteEvent(RemoteEvent remoteEvent) {
328+
SnapshotVersion remoteVersion = remoteEvent.getSnapshotVersion();
329+
328330
// TODO: Call queryEngine.handleDocumentChange() appropriately.
329331
return persistence.runTransaction(
330332
"Apply remote event",
331333
() -> {
334+
Map<Integer, TargetChange> targetChanges = remoteEvent.getTargetChanges();
332335
long sequenceNumber = persistence.getReferenceDelegate().getCurrentSequenceNumber();
333-
Set<DocumentKey> authoritativeUpdates = new HashSet<>();
334336

335-
Map<Integer, TargetChange> targetChanges = remoteEvent.getTargetChanges();
336337
for (Map.Entry<Integer, TargetChange> entry : targetChanges.entrySet()) {
337338
Integer boxedTargetId = entry.getKey();
338339
int targetId = boxedTargetId;
339340
TargetChange change = entry.getValue();
340341

341-
// Do not ref/unref unassigned targetIds - it may lead to leaks.
342-
QueryData queryData = targetIds.get(targetId);
343-
if (queryData == null) {
342+
QueryData oldQueryData = targetIds.get(targetId);
343+
if (oldQueryData == null) {
344+
// We don't update the remote keys if the query is not active. This ensures that
345+
// we persist the updated query data along with the updated assignment.
344346
continue;
345347
}
346348

347-
// When a global snapshot contains updates (either add or modify) we can completely
348-
// trust these updates as authoritative and blindly apply them to our cache (as a
349-
// defensive measure to promote self-healing in the unfortunate case that our cache
350-
// is ever somehow corrupted / out-of-sync).
351-
//
352-
// If the document is only updated while removing it from a target then watch isn't
353-
// obligated to send the absolute latest version: it can send the first version that
354-
// caused the document not to match.
355-
for (DocumentKey key : change.getAddedDocuments()) {
356-
authoritativeUpdates.add(key);
357-
}
358-
for (DocumentKey key : change.getModifiedDocuments()) {
359-
authoritativeUpdates.add(key);
360-
}
361-
362349
queryCache.removeMatchingKeys(change.getRemovedDocuments(), targetId);
363350
queryCache.addMatchingKeys(change.getAddedDocuments(), targetId);
364351

365-
// Update the resume token if the change includes one. Don't clear any preexisting
366-
// value.
367352
ByteString resumeToken = change.getResumeToken();
353+
// Update the resume token if the change includes one.
368354
if (!resumeToken.isEmpty()) {
369-
QueryData oldQueryData = queryData;
370-
queryData =
371-
queryData.copy(remoteEvent.getSnapshotVersion(), resumeToken, sequenceNumber);
372-
targetIds.put(boxedTargetId, queryData);
373-
374-
if (shouldPersistQueryData(oldQueryData, queryData, change)) {
375-
queryCache.updateQueryData(queryData);
355+
QueryData newQueryData =
356+
oldQueryData.copy(remoteVersion, resumeToken, sequenceNumber);
357+
targetIds.put(boxedTargetId, newQueryData);
358+
359+
// Update the query data if there are target changes (or if sufficient time has
360+
// passed since the last update).
361+
if (shouldPersistQueryData(oldQueryData, newQueryData, change)) {
362+
queryCache.updateQueryData(newQueryData);
376363
}
377364
}
378365
}
@@ -391,10 +378,9 @@ public ImmutableSortedMap<DocumentKey, MaybeDocument> applyRemoteEvent(RemoteEve
391378
MaybeDocument existingDoc = existingDocs.get(key);
392379

393380
if (existingDoc == null
394-
|| (authoritativeUpdates.contains(doc.getKey()) && !existingDoc.hasPendingWrites())
395-
|| doc.getVersion().compareTo(existingDoc.getVersion()) >= 0) {
396-
// If a document update isn't authoritative, make sure we don't apply an old document
397-
// version to the remote cache.
381+
|| doc.getVersion().compareTo(existingDoc.getVersion()) > 0
382+
|| (doc.getVersion().compareTo(existingDoc.getVersion()) == 0
383+
&& existingDoc.hasPendingWrites())) {
398384
remoteDocuments.add(doc);
399385
changedDocs.put(key, doc);
400386
} else if (doc instanceof NoDocument && doc.getVersion().equals(SnapshotVersion.NONE)) {
@@ -422,7 +408,6 @@ public ImmutableSortedMap<DocumentKey, MaybeDocument> applyRemoteEvent(RemoteEve
422408
// remote events when we get permission denied errors while trying to resolve the
423409
// state of a locally cached document that is in limbo.
424410
SnapshotVersion lastRemoteVersion = queryCache.getLastRemoteSnapshotVersion();
425-
SnapshotVersion remoteVersion = remoteEvent.getSnapshotVersion();
426411
if (!remoteVersion.equals(SnapshotVersion.NONE)) {
427412
hardAssert(
428413
remoteVersion.compareTo(lastRemoteVersion) >= 0,
@@ -448,10 +433,11 @@ public ImmutableSortedMap<DocumentKey, MaybeDocument> applyRemoteEvent(RemoteEve
448433
*/
449434
private static boolean shouldPersistQueryData(
450435
QueryData oldQueryData, QueryData newQueryData, TargetChange change) {
451-
// Avoid clearing any existing value
452-
if (newQueryData.getResumeToken().isEmpty()) return false;
436+
hardAssert(
437+
!newQueryData.getResumeToken().isEmpty(),
438+
"Attempted to persist query data with empty resume token");
453439

454-
// Any resume token is interesting if there isn't one already.
440+
// Always persist query data if we don't already have a resume token.
455441
if (oldQueryData.getResumeToken().isEmpty()) return true;
456442

457443
// Don't allow resume token changes to be buffered indefinitely. This allows us to be reasonably

firebase-firestore/src/test/java/com/google/firebase/firestore/local/LocalStoreTestCase.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1163,4 +1163,27 @@ public void testGetHighestUnacknowledgedBatchId() {
11631163
rejectMutation();
11641164
assertEquals(MutationBatch.UNKNOWN, localStore.getHighestUnacknowledgedBatchId());
11651165
}
1166+
1167+
@Test
1168+
public void testOnlyPersistsUpdatesForDocumentsWhenVersionChanges() {
1169+
Query query = Query.atPath(ResourcePath.fromString("foo"));
1170+
allocateQuery(query);
1171+
assertTargetId(2);
1172+
1173+
applyRemoteEvent(
1174+
addedRemoteEvent(doc("foo/bar", 1, map("val", "old")), asList(2), emptyList()));
1175+
assertChanged(doc("foo/bar", 1, map("val", "old"), Document.DocumentState.SYNCED));
1176+
assertContains(doc("foo/bar", 1, map("val", "old"), Document.DocumentState.SYNCED));
1177+
1178+
applyRemoteEvent(
1179+
addedRemoteEvent(
1180+
asList(doc("foo/bar", 1, map("val", "new")), doc("foo/baz", 2, map("val", "new"))),
1181+
asList(2),
1182+
emptyList()));
1183+
1184+
assertChanged(doc("foo/baz", 2, map("val", "new"), Document.DocumentState.SYNCED));
1185+
// The update for foo/bar is ignored.
1186+
assertContains(doc("foo/bar", 1, map("val", "old"), Document.DocumentState.SYNCED));
1187+
assertContains(doc("foo/baz", 2, map("val", "new"), Document.DocumentState.SYNCED));
1188+
}
11661189
}

firebase-firestore/src/test/resources/json/limbo_spec_test.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -611,7 +611,7 @@
611611
"docs": [
612612
{
613613
"key": "collection/a",
614-
"version": 1000,
614+
"version": 1002,
615615
"value": {
616616
"key": "b"
617617
},
@@ -948,7 +948,7 @@
948948
"docs": [
949949
{
950950
"key": "collection/a",
951-
"version": 1000,
951+
"version": 1002,
952952
"value": {
953953
"key": "b"
954954
},
@@ -1055,7 +1055,7 @@
10551055
"added": [
10561056
{
10571057
"key": "collection/a",
1058-
"version": 1000,
1058+
"version": 1002,
10591059
"value": {
10601060
"key": "b"
10611061
},

0 commit comments

Comments
 (0)