Skip to content

Commit d8fedfe

Browse files
Store per-user state for indexes (#3153)
1 parent a8f8dd9 commit d8fedfe

19 files changed

+488
-439
lines changed

firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@
4444
import com.google.firebase.firestore.model.FieldIndex;
4545
import com.google.firebase.firestore.model.FieldPath;
4646
import com.google.firebase.firestore.model.ResourcePath;
47-
import com.google.firebase.firestore.model.SnapshotVersion;
4847
import com.google.firebase.firestore.remote.FirestoreChannel;
4948
import com.google.firebase.firestore.remote.GrpcMetadataProvider;
5049
import com.google.firebase.firestore.util.AsyncQueue;
@@ -340,7 +339,9 @@ Task<Void> configureIndices(String json) {
340339
}
341340
}
342341

343-
parsedIndices.add(FieldIndex.create(-1, collectionGroup, segments, SnapshotVersion.NONE));
342+
parsedIndices.add(
343+
FieldIndex.create(
344+
FieldIndex.UNKNOWN_ID, collectionGroup, segments, FieldIndex.INITIAL_STATE));
344345
}
345346
}
346347
} catch (JSONException e) {

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

Lines changed: 55 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818

1919
import androidx.annotation.Nullable;
2020
import androidx.annotation.VisibleForTesting;
21-
import com.google.firebase.Timestamp;
2221
import com.google.firebase.database.collection.ImmutableSortedMap;
2322
import com.google.firebase.firestore.core.Query;
2423
import com.google.firebase.firestore.model.Document;
@@ -27,10 +26,13 @@
2726
import com.google.firebase.firestore.model.ResourcePath;
2827
import com.google.firebase.firestore.model.SnapshotVersion;
2928
import com.google.firebase.firestore.util.AsyncQueue;
29+
import java.util.ArrayList;
3030
import java.util.Collection;
31+
import java.util.Collections;
32+
import java.util.HashSet;
33+
import java.util.List;
3134
import java.util.Map;
32-
import java.util.PriorityQueue;
33-
import java.util.Queue;
35+
import java.util.Set;
3436
import java.util.concurrent.TimeUnit;
3537

3638
/** Implements the steps for backfilling indexes. */
@@ -44,13 +46,15 @@ public class IndexBackfiller {
4446

4547
private final Scheduler scheduler;
4648
private final Persistence persistence;
49+
private final RemoteDocumentCache remoteDocumentCache;
4750
private LocalDocumentsView localDocumentsView;
4851
private IndexManager indexManager;
4952
private int maxDocumentsToProcess = MAX_DOCUMENTS_TO_PROCESS;
5053

5154
public IndexBackfiller(Persistence persistence, AsyncQueue asyncQueue) {
5255
this.persistence = persistence;
5356
this.scheduler = new Scheduler(asyncQueue);
57+
this.remoteDocumentCache = persistence.getRemoteDocumentCache();
5458
}
5559

5660
public void setLocalDocumentsView(LocalDocumentsView localDocumentsView) {
@@ -131,71 +135,92 @@ public Results backfill() {
131135
return persistence.runTransaction(
132136
"Backfill Indexes",
133137
() -> {
134-
// TODO(indexing): Handle field indexes that are removed by the user.
135138
int documentsProcessed = writeIndexEntries(localDocumentsView);
136139
return new Results(/* hasRun= */ true, documentsProcessed);
137140
});
138141
}
139142

140143
/** Writes index entries until the cap is reached. Returns the number of documents processed. */
141144
private int writeIndexEntries(LocalDocumentsView localDocumentsView) {
142-
int documentsProcessed = 0;
143-
Timestamp startingTimestamp = Timestamp.now();
144-
145-
while (documentsProcessed < maxDocumentsToProcess) {
146-
int documentsRemaining = maxDocumentsToProcess - documentsProcessed;
147-
String collectionGroup = indexManager.getNextCollectionGroupToUpdate(startingTimestamp);
148-
if (collectionGroup == null) {
145+
Set<String> processedCollectionGroups = new HashSet<>();
146+
int documentsRemaining = maxDocumentsToProcess;
147+
while (documentsRemaining > 0) {
148+
String collectionGroup = indexManager.getNextCollectionGroupToUpdate();
149+
if (collectionGroup == null || processedCollectionGroups.contains(collectionGroup)) {
149150
break;
150151
}
151-
documentsProcessed +=
152+
documentsRemaining -=
152153
writeEntriesForCollectionGroup(localDocumentsView, collectionGroup, documentsRemaining);
154+
processedCollectionGroups.add(collectionGroup);
153155
}
154-
155-
return documentsProcessed;
156+
return maxDocumentsToProcess - documentsRemaining;
156157
}
157158

158159
/** Writes entries for the fetched field indexes. */
159160
private int writeEntriesForCollectionGroup(
160161
LocalDocumentsView localDocumentsView, String collectionGroup, int entriesRemainingUnderCap) {
161162
Query query = new Query(ResourcePath.EMPTY, collectionGroup);
162163

163-
// Use the earliest updateTime of all field indexes as the base updateTime.
164-
SnapshotVersion earliestUpdateTime =
165-
getEarliestUpdateTime(indexManager.getFieldIndexes(collectionGroup));
164+
// Use the earliest readTime of all field indexes as the base readtime.
165+
SnapshotVersion earliestReadTime =
166+
getEarliestReadTime(indexManager.getFieldIndexes(collectionGroup));
166167

167168
// TODO(indexing): Use limit queries to only fetch the required number of entries.
168169
// TODO(indexing): Support mutation batch Ids when sorting and writing indexes.
169170
ImmutableSortedMap<DocumentKey, Document> documents =
170-
localDocumentsView.getDocumentsMatchingQuery(query, earliestUpdateTime);
171+
localDocumentsView.getDocumentsMatchingQuery(query, earliestReadTime);
171172

172-
Queue<Document> oldestDocuments = getOldestDocuments(documents, entriesRemainingUnderCap);
173+
List<Document> oldestDocuments = getOldestDocuments(documents, entriesRemainingUnderCap);
173174
indexManager.updateIndexEntries(oldestDocuments);
175+
176+
SnapshotVersion latestReadTime = getPostUpdateReadTime(oldestDocuments, earliestReadTime);
177+
indexManager.updateCollectionGroup(collectionGroup, latestReadTime);
174178
return oldestDocuments.size();
175179
}
176180

181+
/**
182+
* Returns the new read time for the index.
183+
*
184+
* @param documents a list of documents sorted by read time (ascending)
185+
* @param currentReadTime the current read time of the index
186+
*/
187+
private SnapshotVersion getPostUpdateReadTime(
188+
List<Document> documents, SnapshotVersion currentReadTime) {
189+
SnapshotVersion latestReadTime =
190+
documents.isEmpty()
191+
? remoteDocumentCache.getLatestReadTime()
192+
: documents.get(documents.size() - 1).getReadTime();
193+
// Make sure the index does not go back in time
194+
latestReadTime =
195+
latestReadTime.compareTo(currentReadTime) > 0 ? latestReadTime : currentReadTime;
196+
return latestReadTime;
197+
}
198+
177199
/** Returns up to {@code count} documents sorted by read time. */
178-
private Queue<Document> getOldestDocuments(
200+
private List<Document> getOldestDocuments(
179201
ImmutableSortedMap<DocumentKey, Document> documents, int count) {
180-
Queue<Document> oldestDocuments =
181-
new PriorityQueue<>(count + 1, (l, r) -> r.getReadTime().compareTo(l.getReadTime()));
202+
List<Document> oldestDocuments = new ArrayList<>();
182203
for (Map.Entry<DocumentKey, Document> entry : documents) {
183204
oldestDocuments.add(entry.getValue());
184-
if (oldestDocuments.size() > count) {
185-
oldestDocuments.poll();
186-
}
187205
}
188-
return oldestDocuments;
206+
Collections.sort(
207+
oldestDocuments,
208+
(l, r) -> {
209+
int cmp = l.getReadTime().compareTo(r.getReadTime());
210+
if (cmp != 0) return cmp;
211+
return l.getKey().compareTo(r.getKey());
212+
});
213+
return oldestDocuments.subList(0, Math.min(count, oldestDocuments.size()));
189214
}
190215

191-
private SnapshotVersion getEarliestUpdateTime(Collection<FieldIndex> fieldIndexes) {
216+
private SnapshotVersion getEarliestReadTime(Collection<FieldIndex> fieldIndexes) {
192217
SnapshotVersion lowestVersion = null;
193218
for (FieldIndex fieldIndex : fieldIndexes) {
194219
lowestVersion =
195220
lowestVersion == null
196-
? fieldIndex.getUpdateTime()
197-
: fieldIndex.getUpdateTime().compareTo(lowestVersion) < 0
198-
? fieldIndex.getUpdateTime()
221+
? fieldIndex.getIndexState().getReadTime()
222+
: fieldIndex.getIndexState().getReadTime().compareTo(lowestVersion) < 0
223+
? fieldIndex.getIndexState().getReadTime()
199224
: lowestVersion;
200225
}
201226
return lowestVersion;

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

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@
1515
package com.google.firebase.firestore.local;
1616

1717
import androidx.annotation.Nullable;
18-
import com.google.firebase.Timestamp;
1918
import com.google.firebase.firestore.core.Target;
2019
import com.google.firebase.firestore.model.Document;
2120
import com.google.firebase.firestore.model.DocumentKey;
2221
import com.google.firebase.firestore.model.FieldIndex;
2322
import com.google.firebase.firestore.model.ResourcePath;
23+
import com.google.firebase.firestore.model.SnapshotVersion;
2424
import java.util.Collection;
2525
import java.util.List;
2626
import java.util.Set;
@@ -87,14 +87,22 @@ public interface IndexManager {
8787
/** Returns the documents that match the given target based on the provided index. */
8888
Set<DocumentKey> getDocumentsMatchingTarget(FieldIndex fieldIndex, Target target);
8989

90-
/** Returns the next collection group to update. */
90+
/** Returns the next collection group to update. Returns {@code null} if no group exists. */
9191
@Nullable
92-
String getNextCollectionGroupToUpdate(Timestamp lastUpdateTime);
92+
String getNextCollectionGroupToUpdate();
93+
94+
/**
95+
* Sets the collection group's latest read time.
96+
*
97+
* <p>This method updates the read time for all field indices for the collection group and
98+
* increments their sequence number. Subsequent calls to {@link #getNextCollectionGroupToUpdate()}
99+
* will return a different collection group (unless only one collection group is configured).
100+
*/
101+
void updateCollectionGroup(String collectionGroup, SnapshotVersion readTime);
93102

94103
/**
95104
* Updates the index entries for the provided documents and corresponding field indexes until the
96-
* cap is reached. Updates the field indexes in persistence with the latest read time that was
97-
* processed.
105+
* cap is reached.
98106
*/
99107
void updateIndexEntries(Collection<Document> documents);
100108
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ private ImmutableSortedMap<DocumentKey, Document> performCollectionQuery(Query q
8484
ImmutableSortedMap<DocumentKey, Document> indexedDocuments =
8585
localDocuments.getDocuments(keys);
8686
ImmutableSortedMap<DocumentKey, Document> additionalDocuments =
87-
localDocuments.getDocumentsMatchingQuery(query, fieldIndex.getUpdateTime());
87+
localDocuments.getDocumentsMatchingQuery(query, fieldIndex.getIndexState().getReadTime());
8888
for (Map.Entry<DocumentKey, Document> entry : additionalDocuments) {
8989
indexedDocuments = indexedDocuments.insert(entry.getKey(), entry.getValue());
9090
}

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@
1616
import static com.google.firebase.firestore.util.Assert.hardAssert;
1717

1818
import androidx.annotation.Nullable;
19-
import com.google.firebase.Timestamp;
2019
import com.google.firebase.firestore.core.Target;
2120
import com.google.firebase.firestore.model.Document;
2221
import com.google.firebase.firestore.model.DocumentKey;
2322
import com.google.firebase.firestore.model.FieldIndex;
2423
import com.google.firebase.firestore.model.ResourcePath;
24+
import com.google.firebase.firestore.model.SnapshotVersion;
2525
import java.util.ArrayList;
2626
import java.util.Collection;
2727
import java.util.Collections;
@@ -79,12 +79,18 @@ public Set<DocumentKey> getDocumentsMatchingTarget(FieldIndex fieldIndex, Target
7979
return Collections.emptySet();
8080
}
8181

82+
@Nullable
8283
@Override
83-
public String getNextCollectionGroupToUpdate(Timestamp startingTimestamp) {
84+
public String getNextCollectionGroupToUpdate() {
8485
// Field indices are not supported with memory persistence.
8586
return null;
8687
}
8788

89+
@Override
90+
public void updateCollectionGroup(String collectionGroup, SnapshotVersion readTime) {
91+
// Field indices are not supported with memory persistence.
92+
}
93+
8894
@Override
8995
public Collection<FieldIndex> getFieldIndexes(String collectionGroup) {
9096
// Field indices are not supported with memory persistence.

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,12 @@ final class MemoryRemoteDocumentCache implements RemoteDocumentCache {
3636
private ImmutableSortedMap<DocumentKey, Pair<MutableDocument, SnapshotVersion>> docs;
3737
/** Manages the collection group index. */
3838
private IndexManager indexManager;
39+
/** The latest read time of any document in the cache. */
40+
private SnapshotVersion latestReadTime;
3941

4042
MemoryRemoteDocumentCache() {
4143
docs = ImmutableSortedMap.Builder.emptyMap(DocumentKey.comparator());
44+
latestReadTime = SnapshotVersion.NONE;
4245
}
4346

4447
@Override
@@ -53,6 +56,7 @@ public void add(MutableDocument document, SnapshotVersion readTime) {
5356
!readTime.equals(SnapshotVersion.NONE),
5457
"Cannot add document to the RemoteDocumentCache with a read time of zero");
5558
docs = docs.insert(document.getKey(), new Pair<>(document.clone(), readTime));
59+
latestReadTime = readTime.compareTo(latestReadTime) > 0 ? readTime : latestReadTime;
5660

5761
indexManager.addToCollectionParentIndex(document.getKey().getPath().popLast());
5862
}
@@ -120,6 +124,11 @@ public ImmutableSortedMap<DocumentKey, MutableDocument> getAllDocumentsMatchingQ
120124
return result;
121125
}
122126

127+
@Override
128+
public SnapshotVersion getLatestReadTime() {
129+
return latestReadTime;
130+
}
131+
123132
Iterable<MutableDocument> getDocuments() {
124133
return new DocumentIterable();
125134
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,7 @@ interface RemoteDocumentCache {
7979
*/
8080
ImmutableSortedMap<DocumentKey, MutableDocument> getAllDocumentsMatchingQuery(
8181
Query query, SnapshotVersion sinceReadTime);
82+
83+
/** Returns the latest read time of any document in the cache. */
84+
SnapshotVersion getLatestReadTime();
8285
}

0 commit comments

Comments
 (0)