Skip to content

Fix overlay bug leas to patch mutation optimization not being applied #4442

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Dec 14, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions firebase-firestore/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Unreleased
* [fixed] Fix an issue that stops some performance optimization being applied.

# 24.4.1
* [fixed] Fix `FAILED_PRECONDITION` when writing to a deleted document in a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ private Map<DocumentKey, OverlayedDocument> computeViews(
Map<DocumentKey, FieldMask> mutatedFields = new HashMap<>();
for (MutableDocument doc : docs.values()) {
Overlay overlay = overlays.get(doc.getKey());

// Recalculate an overlay if the document's existence state is changed due to a remote
// event *and* the overlay is a PatchMutation. This is because document existence state
// can change if some patch mutation's preconditions are met.
Expand All @@ -174,6 +175,9 @@ private Map<DocumentKey, OverlayedDocument> computeViews(
overlay
.getMutation()
.applyToLocalView(doc, overlay.getMutation().getFieldMask(), Timestamp.now());
} else { // overlay == null
// Using EMPTY to indicate there is no overlay for the document.
mutatedFields.put(doc.getKey(), FieldMask.EMPTY);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
/** Represents a local view (overlay) of a document, and the fields that are locally mutated. */
public class OverlayedDocument {
private Document overlayedDocument;
private FieldMask mutatedFields;
@Nullable private FieldMask mutatedFields;

OverlayedDocument(Document overlayedDocument, FieldMask mutatedFields) {
this.overlayedDocument = overlayedDocument;
Expand All @@ -33,9 +33,14 @@ public Document getDocument() {
}

/**
* The fields that are locally mutated by patch mutations. If the overlayed document is from set
* or delete mutations, this returns null.
* The fields that are locally mutated by patch mutations.
*
* <p>If the overlayed document is from set or delete mutations, returns null.
*
* <p>If there is no overlay (mutation) for the document, returns FieldMask.EMPTY.
*/
// TODO(b/262245989): This screams for a proper sum type (Tagged Union) which does not exist in
// Java (yet).
public @Nullable FieldMask getMutatedFields() {
return mutatedFields;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,8 @@ public Map<DocumentKey, Mutation> applyToLocalDocumentSet(
for (DocumentKey key : getKeys()) {
// TODO(mutabledocuments): This method should take a map of MutableDocuments and we should
// remove this cast.
MutableDocument document = (MutableDocument) documentMap.get(key).getDocument();
OverlayedDocument overlayedDocument = documentMap.get(key);
MutableDocument document = (MutableDocument) overlayedDocument.getDocument();
FieldMask mutatedFields = applyToLocalView(document, documentMap.get(key).getMutatedFields());
// Set mutationFields to null if the document is only from local mutations, this creates
// a Set(or Delete) mutation, instead of trying to create a patch mutation as the overlay.
Expand All @@ -151,6 +152,7 @@ public Map<DocumentKey, Mutation> applyToLocalDocumentSet(
if (overlay != null) {
overlays.put(key, overlay);
}

if (!document.isValidDocument()) {
document.convertToNoDocument(SnapshotVersion.NONE);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,14 @@
import com.google.firebase.firestore.model.MutableDocument;
import com.google.firebase.firestore.model.ResourcePath;
import com.google.firebase.firestore.model.SnapshotVersion;
import com.google.firebase.firestore.model.mutation.DeleteMutation;
import com.google.firebase.firestore.model.mutation.Mutation;
import com.google.firebase.firestore.model.mutation.Overlay;
import com.google.firebase.firestore.model.mutation.PatchMutation;
import com.google.firebase.firestore.model.mutation.SetMutation;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.SortedSet;

Expand All @@ -35,10 +40,17 @@
* mutations read.
*/
class CountingQueryEngine extends QueryEngine {
enum OverlayType {
Patch,
Set,
Delete
}

private final QueryEngine queryEngine;

private final int[] overlaysReadByCollection = new int[] {0};
private final int[] overlaysReadByKey = new int[] {0};
private final Map<DocumentKey, OverlayType> overlayTypes = new HashMap();
private final int[] documentsReadByCollection = new int[] {0};
private final int[] documentsReadByKey = new int[] {0};

Expand All @@ -49,6 +61,7 @@ class CountingQueryEngine extends QueryEngine {
void resetCounts() {
overlaysReadByCollection[0] = 0;
overlaysReadByKey[0] = 0;
overlayTypes.clear();
documentsReadByCollection[0] = 0;
documentsReadByKey[0] = 0;
}
Expand Down Expand Up @@ -104,6 +117,14 @@ int getOverlaysReadByKey() {
return overlaysReadByKey[0];
}

/**
* Returns the types of overlay returned by the OverlayCahce's `getOverlays()` API (since the last
* call to `resetCounts()`)
*/
Map<DocumentKey, OverlayType> getOverlayTypes() {
return Collections.unmodifiableMap(overlayTypes);
}

private RemoteDocumentCache wrapRemoteDocumentCache(RemoteDocumentCache subject) {
return new RemoteDocumentCache() {
@Override
Expand Down Expand Up @@ -160,12 +181,19 @@ private DocumentOverlayCache wrapOverlayCache(DocumentOverlayCache subject) {
@Override
public Overlay getOverlay(DocumentKey key) {
++overlaysReadByKey[0];
return subject.getOverlay(key);
Overlay overlay = subject.getOverlay(key);
overlayTypes.put(key, getOverlayType(overlay));
return overlay;
}

public Map<DocumentKey, Overlay> getOverlays(SortedSet<DocumentKey> keys) {
overlaysReadByKey[0] += keys.size();
return subject.getOverlays(keys);
Map<DocumentKey, Overlay> overlays = subject.getOverlays(keys);
for (Map.Entry<DocumentKey, Overlay> entry : overlays.entrySet()) {
overlayTypes.put(entry.getKey(), getOverlayType(entry.getValue()));
}

return overlays;
}

@Override
Expand All @@ -182,6 +210,9 @@ public void removeOverlaysForBatchId(int batchId) {
public Map<DocumentKey, Overlay> getOverlays(ResourcePath collection, int sinceBatchId) {
Map<DocumentKey, Overlay> result = subject.getOverlays(collection, sinceBatchId);
overlaysReadByCollection[0] += result.size();
for (Map.Entry<DocumentKey, Overlay> entry : result.entrySet()) {
overlayTypes.put(entry.getKey(), getOverlayType(entry.getValue()));
}
return result;
}

Expand All @@ -191,8 +222,23 @@ public Map<DocumentKey, Overlay> getOverlays(
Map<DocumentKey, Overlay> result =
subject.getOverlays(collectionGroup, sinceBatchId, count);
overlaysReadByCollection[0] += result.size();
for (Map.Entry<DocumentKey, Overlay> entry : result.entrySet()) {
overlayTypes.put(entry.getKey(), getOverlayType(entry.getValue()));
}
return result;
}

private OverlayType getOverlayType(Overlay overlay) {
if (overlay.getMutation() instanceof SetMutation) {
return OverlayType.Set;
} else if (overlay.getMutation() instanceof PatchMutation) {
return OverlayType.Patch;
} else if (overlay.getMutation() instanceof DeleteMutation) {
return OverlayType.Delete;
} else {
throw new IllegalStateException("Overlay is a unrecognizable mutation.");
}
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import static com.google.firebase.firestore.testutil.TestUtil.existenceFilterEvent;
import static com.google.firebase.firestore.testutil.TestUtil.filter;
import static com.google.firebase.firestore.testutil.TestUtil.key;
import static com.google.firebase.firestore.testutil.TestUtil.keyMap;
import static com.google.firebase.firestore.testutil.TestUtil.keySet;
import static com.google.firebase.firestore.testutil.TestUtil.keys;
import static com.google.firebase.firestore.testutil.TestUtil.map;
Expand Down Expand Up @@ -82,6 +83,7 @@
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -332,6 +334,11 @@ protected void assertOverlaysRead(int byKey, int byCollection) {
"Overlays read (by collection)", byCollection, queryEngine.getOverlaysReadByCollection());
}

/** Asserts the expected overlay types. */
protected void assertOverlayTypes(Map<DocumentKey, CountingQueryEngine.OverlayType> expected) {
assertEquals("Overlays types", expected, queryEngine.getOverlayTypes());
}

/**
* Asserts the expected numbers of documents read by the RemoteDocumentCache since the last call
* to `resetPersistenceStats()`.
Expand Down Expand Up @@ -975,6 +982,7 @@ public void testReadsAllDocumentsForInitialCollectionQueries() {
localStore.executeQuery(query, /* usePreviousResults= */ true);
assertRemoteDocumentsRead(/* byKey= */ 0, /* byCollection= */ 2);
assertOverlaysRead(/* byKey= */ 0, /* byCollection= */ 1);
assertOverlayTypes(keyMap("foo/bonk", CountingQueryEngine.OverlayType.Set));
}

@Test
Expand Down Expand Up @@ -1689,4 +1697,21 @@ public void testMultipleFieldPatchesOnLocalDocs() {
assertChanged(doc("foo/bar", 0, map("likes", 1, "stars", 2)).setHasLocalMutations());
assertContains(doc("foo/bar", 0, map("likes", 1, "stars", 2)).setHasLocalMutations());
}

@Test
public void testUpdateOnRemoteDocLeadsToUpdateOverlay() {
Query query = query("foo");
allocateQuery(query);

applyRemoteEvent(updateRemoteEvent(doc("foo/baz", 10, map("a", 1)), asList(2), emptyList()));
applyRemoteEvent(updateRemoteEvent(doc("foo/bar", 20, map()), asList(2), emptyList()));
writeMutation(patchMutation("foo/baz", map("b", 2)));

resetPersistenceStats();

localStore.executeQuery(query, /* usePreviousResults= */ true);
assertRemoteDocumentsRead(/* byKey= */ 0, /* byCollection= */ 2);
assertOverlaysRead(/* byKey= */ 0, /* byCollection= */ 1);
assertOverlayTypes(keyMap("foo/baz", CountingQueryEngine.OverlayType.Patch));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import static com.google.firebase.firestore.testutil.TestUtil.fieldIndex;
import static com.google.firebase.firestore.testutil.TestUtil.filter;
import static com.google.firebase.firestore.testutil.TestUtil.key;
import static com.google.firebase.firestore.testutil.TestUtil.keyMap;
import static com.google.firebase.firestore.testutil.TestUtil.map;
import static com.google.firebase.firestore.testutil.TestUtil.orderBy;
import static com.google.firebase.firestore.testutil.TestUtil.query;
Expand Down Expand Up @@ -209,6 +210,12 @@ public void testUsesPartiallyIndexedOverlaysWhenAvailable() {
Query query = query("coll").filter(filter("matches", "==", true));
executeQuery(query);
assertOverlaysRead(/* byKey= */ 1, /* byCollection= */ 1);
assertOverlayTypes(
keyMap(
"coll/a",
CountingQueryEngine.OverlayType.Set,
"coll/b",
CountingQueryEngine.OverlayType.Set));
assertQueryReturned("coll/a", "coll/b");
}

Expand Down Expand Up @@ -238,6 +245,7 @@ public void testDoesNotUseLimitWhenIndexIsOutdated() {
// The query engine first reads the documents by key and then re-runs the query without limit.
assertRemoteDocumentsRead(/* byKey= */ 5, /* byCollection= */ 0);
assertOverlaysRead(/* byKey= */ 5, /* byCollection= */ 1);
assertOverlayTypes(keyMap("coll/b", CountingQueryEngine.OverlayType.Delete));
assertQueryReturned("coll/a", "coll/c");
}

Expand Down Expand Up @@ -279,6 +287,7 @@ public void testIndexesServerTimestamps() {
Query query = query("coll").orderBy(orderBy("time", "asc"));
executeQuery(query);
assertOverlaysRead(/* byKey= */ 1, /* byCollection= */ 0);
assertOverlayTypes(keyMap("coll/a", CountingQueryEngine.OverlayType.Set));
assertQueryReturned("coll/a");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,14 @@ public static ImmutableSortedSet<DocumentKey> keySet(DocumentKey... keys) {
return keySet;
}

public static <T> Map<DocumentKey, T> keyMap(Object... entries) {
Map<DocumentKey, T> res = new LinkedHashMap<>();
for (int i = 0; i < entries.length; i += 2) {
res.put(DocumentKey.fromPathString((String) entries[i]), (T) entries[i + 1]);
}
return res;
}

public static FieldFilter filter(String key, String operator, Object value) {
return FieldFilter.create(field(key), operatorFromString(operator), wrap(value));
}
Expand Down