Skip to content

Commit eab4627

Browse files
committed
Implement limbo resolution listen throttling.
1 parent 6251d0c commit eab4627

File tree

3 files changed

+912
-20
lines changed

3 files changed

+912
-20
lines changed

firebase-firestore/src/main/java/com/google/firebase/firestore/core/SyncEngine.java

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import static com.google.firebase.firestore.util.Assert.fail;
1818
import static com.google.firebase.firestore.util.Assert.hardAssert;
1919

20+
import androidx.annotation.NonNull;
2021
import androidx.annotation.Nullable;
2122
import androidx.annotation.VisibleForTesting;
2223
import com.google.android.gms.tasks.Task;
@@ -48,11 +49,13 @@
4849
import com.google.firebase.firestore.util.Logger;
4950
import com.google.firebase.firestore.util.Util;
5051
import io.grpc.Status;
52+
import java.util.ArrayDeque;
5153
import java.util.ArrayList;
5254
import java.util.Collections;
5355
import java.util.HashMap;
5456
import java.util.List;
5557
import java.util.Map;
58+
import java.util.Queue;
5659
import java.util.Set;
5760

5861
/**
@@ -89,8 +92,28 @@ private static class LimboResolution {
8992
}
9093
}
9194

95+
/** An entry in {@link #limboListenQueue}. */
96+
private static final class LimboListenQueueEntry {
97+
98+
final DocumentKey key;
99+
final TargetData targetData;
100+
101+
LimboListenQueueEntry(DocumentKey key, TargetData targetData) {
102+
this.key = key;
103+
this.targetData = targetData;
104+
}
105+
106+
@NonNull
107+
@Override
108+
public String toString() {
109+
return "key=" + key + " targetData=" + targetData;
110+
}
111+
}
112+
92113
private static final String TAG = SyncEngine.class.getSimpleName();
93114

115+
private static final int DEFAULT_MAX_CONCURRENT_LIMBO_RESOLUTIONS = 100;
116+
94117
/** Interface implemented by EventManager to handle notifications from SyncEngine. */
95118
interface SyncEngineCallback {
96119
/** Handles new view snapshots. */
@@ -109,6 +132,8 @@ interface SyncEngineCallback {
109132
/** The remote store for sending writes, watches, etc. to the backend. */
110133
private final RemoteStore remoteStore;
111134

135+
private final int maxConcurrentLimboResolutions;
136+
112137
/** QueryViews for all active queries, indexed by query. */
113138
private final Map<Query, QueryView> queryViewsByQuery;
114139

@@ -127,6 +152,13 @@ interface SyncEngineCallback {
127152
*/
128153
private final Map<Integer, LimboResolution> limboResolutionsByTarget;
129154

155+
/**
156+
* The list of enqueued limbo resolutions. After {@link #maxConcurrentLimboResolutions} listens
157+
* have started for the purpose of limbo resolutions then additional limbo resolution listens are
158+
* enqueued in this queue. When a listen completes then a listen is dequeued and started.
159+
*/
160+
private final Queue<LimboListenQueueEntry> limboListenQueue;
161+
130162
/** Used to track any documents that are currently in limbo. */
131163
private final ReferenceSet limboDocumentRefs;
132164

@@ -144,14 +176,24 @@ interface SyncEngineCallback {
144176
private SyncEngineCallback syncEngineListener;
145177

146178
public SyncEngine(LocalStore localStore, RemoteStore remoteStore, User initialUser) {
179+
this(localStore, remoteStore, initialUser, DEFAULT_MAX_CONCURRENT_LIMBO_RESOLUTIONS);
180+
}
181+
182+
public SyncEngine(
183+
LocalStore localStore,
184+
RemoteStore remoteStore,
185+
User initialUser,
186+
int maxConcurrentLimboResolutions) {
147187
this.localStore = localStore;
148188
this.remoteStore = remoteStore;
189+
this.maxConcurrentLimboResolutions = maxConcurrentLimboResolutions;
149190

150191
queryViewsByQuery = new HashMap<>();
151192
queriesByTarget = new HashMap<>();
152193

153194
limboTargetsByKey = new HashMap<>();
154195
limboResolutionsByTarget = new HashMap<>();
196+
limboListenQueue = new ArrayDeque<>();
155197
limboDocumentRefs = new ReferenceSet();
156198

157199
mutationUserCallbacks = new HashMap<>();
@@ -380,6 +422,10 @@ public void handleRejectedListen(int targetId, Status error) {
380422
limboTargetsByKey.remove(limboKey);
381423
limboResolutionsByTarget.remove(targetId);
382424

425+
if (!limboListenQueue.isEmpty()) {
426+
remoteStore.listen(limboListenQueue.remove().targetData);
427+
}
428+
383429
// TODO: Retry on transient errors?
384430

385431
// It's a limbo doc. Create a synthetic event saying it was deleted. This is kind of a hack.
@@ -540,6 +586,9 @@ private void removeLimboTarget(DocumentKey key) {
540586
remoteStore.stopListening(targetId);
541587
limboTargetsByKey.remove(key);
542588
limboResolutionsByTarget.remove(targetId);
589+
if (!limboListenQueue.isEmpty()) {
590+
remoteStore.listen(limboListenQueue.remove().targetData);
591+
}
543592
}
544593
}
545594

@@ -616,7 +665,11 @@ private void trackLimboChange(LimboDocumentChange change) {
616665
ListenSequence.INVALID,
617666
QueryPurpose.LIMBO_RESOLUTION);
618667
limboResolutionsByTarget.put(limboTargetId, new LimboResolution(key));
619-
remoteStore.listen(targetData);
668+
if (limboResolutionsByTarget.size() <= maxConcurrentLimboResolutions) {
669+
remoteStore.listen(targetData);
670+
} else {
671+
limboListenQueue.add(new LimboListenQueueEntry(key, targetData));
672+
}
620673
limboTargetsByKey.put(key, limboTargetId);
621674
}
622675
}
@@ -627,6 +680,15 @@ public Map<DocumentKey, Integer> getCurrentLimboDocuments() {
627680
return new HashMap<>(limboTargetsByKey);
628681
}
629682

683+
@VisibleForTesting
684+
public List<DocumentKey> getEnqueuedLimboDocuments() {
685+
ArrayList<DocumentKey> list = new ArrayList<>(limboListenQueue.size());
686+
for (LimboListenQueueEntry entry : limboListenQueue) {
687+
list.add(entry.key);
688+
}
689+
return list;
690+
}
691+
630692
public void handleCredentialChange(User user) {
631693
boolean userChanged = !currentUser.equals(user);
632694
currentUser = user;

firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SpecTestCase.java

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ public abstract class SpecTestCase implements RemoteStoreCallback {
153153
: Sets.newHashSet("no-android", BENCHMARK_TAG, "multi-client");
154154

155155
private boolean garbageCollectionEnabled;
156+
private Integer maxNumConcurrentLimboResolutions;
156157
private boolean networkEnabled = true;
157158

158159
//
@@ -250,6 +251,8 @@ protected void specSetUp(JSONObject config) {
250251
outstandingWrites = new HashMap<>();
251252

252253
this.garbageCollectionEnabled = config.optBoolean("useGarbageCollection", false);
254+
this.maxNumConcurrentLimboResolutions =
255+
(Integer) config.opt("maxNumConcurrentLimboResolutions");
253256

254257
currentUser = User.UNAUTHENTICATED;
255258

@@ -293,7 +296,12 @@ private void initClient() {
293296
ConnectivityMonitor connectivityMonitor =
294297
new AndroidConnectivityMonitor(ApplicationProvider.getApplicationContext());
295298
remoteStore = new RemoteStore(this, localStore, datastore, queue, connectivityMonitor);
296-
syncEngine = new SyncEngine(localStore, remoteStore, currentUser);
299+
if (maxNumConcurrentLimboResolutions == null) {
300+
syncEngine = new SyncEngine(localStore, remoteStore, currentUser);
301+
} else {
302+
syncEngine =
303+
new SyncEngine(localStore, remoteStore, currentUser, maxNumConcurrentLimboResolutions);
304+
}
297305
eventManager = new EventManager(syncEngine);
298306
localStore.start();
299307
remoteStore.start();
@@ -988,12 +996,20 @@ private void validateLimboDocs() {
988996
@SuppressWarnings("VisibleForTests")
989997
Map<DocumentKey, Integer> actualLimboDocs =
990998
new HashMap<>(syncEngine.getCurrentLimboDocuments());
999+
@SuppressWarnings("VisibleForTests")
1000+
List<DocumentKey> enqueuedLimboDocuments = syncEngine.getEnqueuedLimboDocuments();
9911001

9921002
// Validate that each limbo doc has an expected active target
9931003
for (Map.Entry<DocumentKey, Integer> limboDoc : actualLimboDocs.entrySet()) {
994-
assertTrue(
995-
"Found limbo doc " + limboDoc.getKey() + " without an expected active target",
996-
expectedActiveTargets.containsKey(limboDoc.getValue()));
1004+
if (enqueuedLimboDocuments.contains(limboDoc.getKey())) {
1005+
assertFalse(
1006+
"Found limbo doc " + limboDoc.getKey() + " with an unexpected active target",
1007+
expectedActiveTargets.containsKey(limboDoc.getValue()));
1008+
} else {
1009+
assertTrue(
1010+
"Found limbo doc " + limboDoc.getKey() + " without an expected active target",
1011+
expectedActiveTargets.containsKey(limboDoc.getValue()));
1012+
}
9971013
}
9981014

9991015
for (DocumentKey expectedLimboDoc : expectedLimboDocs) {

0 commit comments

Comments
 (0)