Skip to content

Commit 74b06f5

Browse files
authored
Implement limbo resolution throttling (#2790)
1 parent 735f7c7 commit 74b06f5

File tree

5 files changed

+460
-45
lines changed

5 files changed

+460
-45
lines changed

packages/firestore/src/core/firestore_client.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import { OnlineState, OnlineStateSource } from './types';
5353
import { ViewSnapshot } from './view_snapshot';
5454

5555
const LOG_TAG = 'FirestoreClient';
56+
const MAX_CONCURRENT_LIMBO_RESOLUTIONS = 100;
5657

5758
/** DOMException error code constants. */
5859
const DOM_EXCEPTION_INVALID_STATE = 11;
@@ -369,7 +370,8 @@ export class FirestoreClient {
369370
this.localStore,
370371
this.remoteStore,
371372
this.sharedClientState,
372-
user
373+
user,
374+
MAX_CONCURRENT_LIMBO_RESOLUTIONS
373375
);
374376

375377
this.sharedClientState.onlineStateHandler = sharedClientStateOnlineStateChangedHandler;

packages/firestore/src/core/sync_engine.ts

+69-24
Original file line numberDiff line numberDiff line change
@@ -148,10 +148,23 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
148148
q.canonicalId()
149149
);
150150
private queriesByTarget: { [targetId: number]: Query[] } = {};
151-
private limboTargetsByKey = new SortedMap<DocumentKey, TargetId>(
151+
/**
152+
* The keys of documents that are in limbo for which we haven't yet started a
153+
* limbo resolution query.
154+
*/
155+
private enqueuedLimboResolutions: DocumentKey[] = [];
156+
/**
157+
* Keeps track of the target ID for each document that is in limbo with an
158+
* active target.
159+
*/
160+
private activeLimboTargetsByKey = new SortedMap<DocumentKey, TargetId>(
152161
DocumentKey.comparator
153162
);
154-
private limboResolutionsByTarget: {
163+
/**
164+
* Keeps track of the information about an active limbo resolution for each
165+
* active target ID that was started for the purpose of limbo resolution.
166+
*/
167+
private activeLimboResolutionsByTarget: {
155168
[targetId: number]: LimboResolution;
156169
} = {};
157170
private limboDocumentRefs = new ReferenceSet();
@@ -174,7 +187,8 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
174187
private remoteStore: RemoteStore,
175188
// PORTING NOTE: Manages state synchronization in multi-tab environments.
176189
private sharedClientState: SharedClientState,
177-
private currentUser: User
190+
private currentUser: User,
191+
private maxConcurrentLimboResolutions: number
178192
) {}
179193

180194
// Only used for testing.
@@ -400,7 +414,9 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
400414
const changes = await this.localStore.applyRemoteEvent(remoteEvent);
401415
// Update `receivedDocument` as appropriate for any limbo targets.
402416
objUtils.forEach(remoteEvent.targetChanges, (targetId, targetChange) => {
403-
const limboResolution = this.limboResolutionsByTarget[Number(targetId)];
417+
const limboResolution = this.activeLimboResolutionsByTarget[
418+
Number(targetId)
419+
];
404420
if (limboResolution) {
405421
// Since this is a limbo resolution lookup, it's for a single document
406422
// and it could be added, modified, or removed, but not a combination.
@@ -479,13 +495,16 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
479495
// PORTING NOTE: Multi-tab only.
480496
this.sharedClientState.updateQueryState(targetId, 'rejected', err);
481497

482-
const limboResolution = this.limboResolutionsByTarget[targetId];
498+
const limboResolution = this.activeLimboResolutionsByTarget[targetId];
483499
const limboKey = limboResolution && limboResolution.key;
484500
if (limboKey) {
485501
// Since this query failed, we won't want to manually unlisten to it.
486502
// So go ahead and remove it from bookkeeping.
487-
this.limboTargetsByKey = this.limboTargetsByKey.remove(limboKey);
488-
delete this.limboResolutionsByTarget[targetId];
503+
this.activeLimboTargetsByKey = this.activeLimboTargetsByKey.remove(
504+
limboKey
505+
);
506+
delete this.activeLimboResolutionsByTarget[targetId];
507+
this.pumpEnqueuedLimboResolutions();
489508

490509
// TODO(klimt): We really only should do the following on permission
491510
// denied errors, but we don't have the cause code here.
@@ -731,15 +750,16 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
731750
private removeLimboTarget(key: DocumentKey): void {
732751
// It's possible that the target already got removed because the query failed. In that case,
733752
// the key won't exist in `limboTargetsByKey`. Only do the cleanup if we still have the target.
734-
const limboTargetId = this.limboTargetsByKey.get(key);
753+
const limboTargetId = this.activeLimboTargetsByKey.get(key);
735754
if (limboTargetId === null) {
736755
// This target already got removed, because the query failed.
737756
return;
738757
}
739758

740759
this.remoteStore.unlisten(limboTargetId);
741-
this.limboTargetsByKey = this.limboTargetsByKey.remove(key);
742-
delete this.limboResolutionsByTarget[limboTargetId];
760+
this.activeLimboTargetsByKey = this.activeLimboTargetsByKey.remove(key);
761+
delete this.activeLimboResolutionsByTarget[limboTargetId];
762+
this.pumpEnqueuedLimboResolutions();
743763
}
744764

745765
private updateTrackedLimbos(
@@ -768,29 +788,54 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
768788

769789
private trackLimboChange(limboChange: AddedLimboDocument): void {
770790
const key = limboChange.key;
771-
if (!this.limboTargetsByKey.get(key)) {
791+
if (!this.activeLimboTargetsByKey.get(key)) {
772792
logDebug(LOG_TAG, 'New document in limbo: ' + key);
793+
this.enqueuedLimboResolutions.push(key);
794+
this.pumpEnqueuedLimboResolutions();
795+
}
796+
}
797+
798+
/**
799+
* Starts listens for documents in limbo that are enqueued for resolution,
800+
* subject to a maximum number of concurrent resolutions.
801+
*
802+
* Without bounding the number of concurrent resolutions, the server can fail
803+
* with "resource exhausted" errors which can lead to pathological client
804+
* behavior as seen in https://github.com/firebase/firebase-js-sdk/issues/2683.
805+
*/
806+
private pumpEnqueuedLimboResolutions(): void {
807+
while (
808+
this.enqueuedLimboResolutions.length > 0 &&
809+
this.activeLimboTargetsByKey.size < this.maxConcurrentLimboResolutions
810+
) {
811+
const key = this.enqueuedLimboResolutions.shift()!;
773812
const limboTargetId = this.limboTargetIdGenerator.next();
774-
const query = Query.atPath(key.path);
775-
this.limboResolutionsByTarget[limboTargetId] = new LimboResolution(key);
813+
this.activeLimboResolutionsByTarget[limboTargetId] = new LimboResolution(
814+
key
815+
);
816+
this.activeLimboTargetsByKey = this.activeLimboTargetsByKey.insert(
817+
key,
818+
limboTargetId
819+
);
776820
this.remoteStore.listen(
777821
new TargetData(
778-
query.toTarget(),
822+
Query.atPath(key.path).toTarget(),
779823
limboTargetId,
780824
TargetPurpose.LimboResolution,
781825
ListenSequence.INVALID
782826
)
783827
);
784-
this.limboTargetsByKey = this.limboTargetsByKey.insert(
785-
key,
786-
limboTargetId
787-
);
788828
}
789829
}
790830

791831
// Visible for testing
792-
currentLimboDocs(): SortedMap<DocumentKey, TargetId> {
793-
return this.limboTargetsByKey;
832+
activeLimboDocumentResolutions(): SortedMap<DocumentKey, TargetId> {
833+
return this.activeLimboTargetsByKey;
834+
}
835+
836+
// Visible for testing
837+
enqueuedLimboDocumentResolutions(): DocumentKey[] {
838+
return this.enqueuedLimboResolutions;
794839
}
795840

796841
private async emitNewSnapsAndNotifyLocalStore(
@@ -936,12 +981,12 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
936981

937982
// PORTING NOTE: Multi-tab only.
938983
private resetLimboDocuments(): void {
939-
objUtils.forEachNumber(this.limboResolutionsByTarget, targetId => {
984+
objUtils.forEachNumber(this.activeLimboResolutionsByTarget, targetId => {
940985
this.remoteStore.unlisten(targetId);
941986
});
942987
this.limboDocumentRefs.removeAllReferences();
943-
this.limboResolutionsByTarget = [];
944-
this.limboTargetsByKey = new SortedMap<DocumentKey, TargetId>(
988+
this.activeLimboResolutionsByTarget = [];
989+
this.activeLimboTargetsByKey = new SortedMap<DocumentKey, TargetId>(
945990
DocumentKey.comparator
946991
);
947992
}
@@ -1138,7 +1183,7 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
11381183
}
11391184

11401185
getRemoteKeysForTarget(targetId: TargetId): DocumentKeySet {
1141-
const limboResolution = this.limboResolutionsByTarget[targetId];
1186+
const limboResolution = this.activeLimboResolutionsByTarget[targetId];
11421187
if (limboResolution && limboResolution.receivedDocument) {
11431188
return documentKeySet().add(limboResolution.key);
11441189
} else {

0 commit comments

Comments
 (0)