Skip to content

Commit f0a2077

Browse files
committed
Implement code review feedback from @wilhuff, round 2
1 parent 5900aa8 commit f0a2077

File tree

4 files changed

+86
-80
lines changed

4 files changed

+86
-80
lines changed

packages/firestore/src/core/sync_engine.ts

Lines changed: 51 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -148,14 +148,23 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
148148
q.canonicalId()
149149
);
150150
private queriesByTarget: { [targetId: number]: Query[] } = {};
151-
/** The keys of documents that are in limbo for which we haven't yet started a limbo resolution query. */
152-
private limboListenQueue: DocumentKey[] = [];
153-
/** Keeps track of the target ID for each document that is in limbo with an active target. */
154-
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>(
155161
DocumentKey.comparator
156162
);
157-
/** Keeps track of the information about an active limbo resolution for each active target ID that was started for the purpose of limbo resolution. */
158-
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: {
159168
[targetId: number]: LimboResolution;
160169
} = {};
161170
private limboDocumentRefs = new ReferenceSet();
@@ -405,7 +414,9 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
405414
const changes = await this.localStore.applyRemoteEvent(remoteEvent);
406415
// Update `receivedDocument` as appropriate for any limbo targets.
407416
objUtils.forEach(remoteEvent.targetChanges, (targetId, targetChange) => {
408-
const limboResolution = this.limboResolutionsByTarget[Number(targetId)];
417+
const limboResolution = this.activeLimboResolutionsByTarget[
418+
Number(targetId)
419+
];
409420
if (limboResolution) {
410421
// Since this is a limbo resolution lookup, it's for a single document
411422
// and it could be added, modified, or removed, but not a combination.
@@ -484,14 +495,16 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
484495
// PORTING NOTE: Multi-tab only.
485496
this.sharedClientState.updateQueryState(targetId, 'rejected', err);
486497

487-
const limboResolution = this.limboResolutionsByTarget[targetId];
498+
const limboResolution = this.activeLimboResolutionsByTarget[targetId];
488499
const limboKey = limboResolution && limboResolution.key;
489500
if (limboKey) {
490501
// Since this query failed, we won't want to manually unlisten to it.
491502
// So go ahead and remove it from bookkeeping.
492-
this.limboTargetsByKey = this.limboTargetsByKey.remove(limboKey);
493-
delete this.limboResolutionsByTarget[targetId];
494-
this.pumpLimboResolutionListenQueue();
503+
this.activeLimboTargetsByKey = this.activeLimboTargetsByKey.remove(
504+
limboKey
505+
);
506+
delete this.activeLimboResolutionsByTarget[targetId];
507+
this.pumpEnqueuedLimboResolutions();
495508

496509
// TODO(klimt): We really only should do the following on permission
497510
// denied errors, but we don't have the cause code here.
@@ -737,16 +750,16 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
737750
private removeLimboTarget(key: DocumentKey): void {
738751
// It's possible that the target already got removed because the query failed. In that case,
739752
// the key won't exist in `limboTargetsByKey`. Only do the cleanup if we still have the target.
740-
const limboTargetId = this.limboTargetsByKey.get(key);
753+
const limboTargetId = this.activeLimboTargetsByKey.get(key);
741754
if (limboTargetId === null) {
742755
// This target already got removed, because the query failed.
743756
return;
744757
}
745758

746759
this.remoteStore.unlisten(limboTargetId);
747-
this.limboTargetsByKey = this.limboTargetsByKey.remove(key);
748-
delete this.limboResolutionsByTarget[limboTargetId];
749-
this.pumpLimboResolutionListenQueue();
760+
this.activeLimboTargetsByKey = this.activeLimboTargetsByKey.remove(key);
761+
delete this.activeLimboResolutionsByTarget[limboTargetId];
762+
this.pumpEnqueuedLimboResolutions();
750763
}
751764

752765
private updateTrackedLimbos(
@@ -775,41 +788,32 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
775788

776789
private trackLimboChange(limboChange: AddedLimboDocument): void {
777790
const key = limboChange.key;
778-
if (!this.limboTargetsByKey.get(key)) {
791+
if (!this.activeLimboTargetsByKey.get(key)) {
779792
log.debug(LOG_TAG, 'New document in limbo: ' + key);
780-
this.limboListenQueue.push(key);
781-
this.pumpLimboResolutionListenQueue();
793+
this.enqueuedLimboResolutions.push(key);
794+
this.pumpEnqueuedLimboResolutions();
782795
}
783796
}
784797

785798
/**
786-
* Starts listens for documents in limbo that are enqueued for resolution.
787-
*
788-
* When a document goes into limbo it is enqueued for resolution. This method
789-
* repeatedly removes entries from the limbo resolution queue and starts a
790-
* listen for them until either (1) the queue is empty, meaning that all
791-
* documents that were in limbo either have active listens or have been
792-
* resolved, or (2) the maximum number of concurrent limbo resolution listens
793-
* has been reached.
799+
* Starts listens for documents in limbo that are enqueued for resolution,
800+
* subject to a maximum number of concurrent resolutions.
794801
*
795-
* This method is invoked every time an entry is added to the limbo
796-
* resolution queue and every time that a limbo resolution listen completes
797-
* (either successfully or unsuccessfully). This ensures that all documents in
798-
* limbo are eventually resolved.
799-
*
800-
* A maximum number of concurrent limbo resolution listens was implemented to
801-
* prevent an unbounded number of active limbo resolution listens that can
802-
* exhaust server resources and result in "resource exhausted" errors.
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.
803805
*/
804-
private pumpLimboResolutionListenQueue(): void {
806+
private pumpEnqueuedLimboResolutions(): void {
805807
while (
806-
this.limboListenQueue.length > 0 &&
807-
this.limboTargetsByKey.size < this.maxConcurrentLimboResolutions
808+
this.enqueuedLimboResolutions.length > 0 &&
809+
this.activeLimboTargetsByKey.size < this.maxConcurrentLimboResolutions
808810
) {
809-
const key = this.limboListenQueue.shift()!;
811+
const key = this.enqueuedLimboResolutions.shift()!;
810812
const limboTargetId = this.limboTargetIdGenerator.next();
811-
this.limboResolutionsByTarget[limboTargetId] = new LimboResolution(key);
812-
this.limboTargetsByKey = this.limboTargetsByKey.insert(
813+
this.activeLimboResolutionsByTarget[limboTargetId] = new LimboResolution(
814+
key
815+
);
816+
this.activeLimboTargetsByKey = this.activeLimboTargetsByKey.insert(
813817
key,
814818
limboTargetId
815819
);
@@ -826,12 +830,12 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
826830

827831
// Visible for testing
828832
activeLimboDocumentResolutions(): SortedMap<DocumentKey, TargetId> {
829-
return this.limboTargetsByKey;
833+
return this.activeLimboTargetsByKey;
830834
}
831835

832836
// Visible for testing
833-
documentsEnqueuedForLimboResolution(): DocumentKey[] {
834-
return this.limboListenQueue;
837+
enqueuedLimboDocumentResolutions(): DocumentKey[] {
838+
return this.enqueuedLimboResolutions;
835839
}
836840

837841
private async emitNewSnapsAndNotifyLocalStore(
@@ -977,12 +981,12 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
977981

978982
// PORTING NOTE: Multi-tab only.
979983
private resetLimboDocuments(): void {
980-
objUtils.forEachNumber(this.limboResolutionsByTarget, targetId => {
984+
objUtils.forEachNumber(this.activeLimboResolutionsByTarget, targetId => {
981985
this.remoteStore.unlisten(targetId);
982986
});
983987
this.limboDocumentRefs.removeAllReferences();
984-
this.limboResolutionsByTarget = [];
985-
this.limboTargetsByKey = new SortedMap<DocumentKey, TargetId>(
988+
this.activeLimboResolutionsByTarget = [];
989+
this.activeLimboTargetsByKey = new SortedMap<DocumentKey, TargetId>(
986990
DocumentKey.comparator
987991
);
988992
}
@@ -1179,7 +1183,7 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
11791183
}
11801184

11811185
getRemoteKeysForTarget(targetId: TargetId): DocumentKeySet {
1182-
const limboResolution = this.limboResolutionsByTarget[targetId];
1186+
const limboResolution = this.activeLimboResolutionsByTarget[targetId];
11831187
if (limboResolution && limboResolution.receivedDocument) {
11841188
return documentKeySet().add(limboResolution.key);
11851189
} else {

packages/firestore/test/unit/specs/limbo_spec.test.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -674,7 +674,7 @@ describeSpec('Limbo Documents:', [], () => {
674674
.watchCurrents(query, 'resume-token-2000')
675675
.watchSnapshots(2000)
676676
.expectLimboDocs(doc1.key, doc2.key)
677-
.expectInactiveLimboDocs(doc3.key, doc4.key, doc5.key)
677+
.expectEnqueuedLimboDocs(doc3.key, doc4.key, doc5.key)
678678
// Limbo document causes query to be "inconsistent"
679679
.expectEvents(query, { fromCache: true })
680680
.watchAcks(limboQuery1)
@@ -689,7 +689,7 @@ describeSpec('Limbo Documents:', [], () => {
689689
})
690690
// Start the second round of limbo resolutions.
691691
.expectLimboDocs(doc3.key, doc4.key)
692-
.expectInactiveLimboDocs(doc5.key)
692+
.expectEnqueuedLimboDocs(doc5.key)
693693
.watchAcks(limboQuery3)
694694
.watchAcks(limboQuery4)
695695
// Resolve limbo documents doc3 and doc4 in a single snapshot.
@@ -702,7 +702,7 @@ describeSpec('Limbo Documents:', [], () => {
702702
})
703703
// Start the final round of limbo resolutions.
704704
.expectLimboDocs(doc5.key)
705-
.expectInactiveLimboDocs()
705+
.expectEnqueuedLimboDocs()
706706
.watchAcks(limboQuery5)
707707
// Resolve limbo document doc5.
708708
.watchCurrents(limboQuery5, 'resume-token-2003')
@@ -712,7 +712,7 @@ describeSpec('Limbo Documents:', [], () => {
712712
fromCache: false
713713
})
714714
.expectLimboDocs()
715-
.expectInactiveLimboDocs()
715+
.expectEnqueuedLimboDocs()
716716
);
717717
}
718718
);
@@ -751,7 +751,7 @@ describeSpec('Limbo Documents:', [], () => {
751751
.watchCurrents(query, 'resume-token-2000')
752752
.watchSnapshots(2000)
753753
.expectLimboDocs(doc1.key, doc2.key)
754-
.expectInactiveLimboDocs(doc3.key, doc4.key, doc5.key)
754+
.expectEnqueuedLimboDocs(doc3.key, doc4.key, doc5.key)
755755
// Limbo document causes query to be "inconsistent"
756756
.expectEvents(query, { fromCache: true })
757757
.watchAcks(limboQuery1)
@@ -762,37 +762,37 @@ describeSpec('Limbo Documents:', [], () => {
762762
.expectEvents(query, { removed: [doc1], fromCache: true })
763763
// Start the next limbo resolution since one has finished.
764764
.expectLimboDocs(doc2.key, doc3.key)
765-
.expectInactiveLimboDocs(doc4.key, doc5.key)
765+
.expectEnqueuedLimboDocs(doc4.key, doc5.key)
766766
.watchAcks(limboQuery3)
767767
// Resolve the limbo documents doc2 in its own snapshot.
768768
.watchCurrents(limboQuery2, 'resume-token-2002')
769769
.watchSnapshots(2002)
770770
.expectEvents(query, { removed: [doc2], fromCache: true })
771771
// Start the next limbo resolution since one has finished.
772772
.expectLimboDocs(doc3.key, doc4.key)
773-
.expectInactiveLimboDocs(doc5.key)
773+
.expectEnqueuedLimboDocs(doc5.key)
774774
.watchAcks(limboQuery4)
775775
// Resolve the limbo documents doc3 in its own snapshot.
776776
.watchCurrents(limboQuery3, 'resume-token-2003')
777777
.watchSnapshots(2003)
778778
.expectEvents(query, { removed: [doc3], fromCache: true })
779779
// Start the next limbo resolution since one has finished.
780780
.expectLimboDocs(doc4.key, doc5.key)
781-
.expectInactiveLimboDocs()
781+
.expectEnqueuedLimboDocs()
782782
.watchAcks(limboQuery5)
783783
// Resolve the limbo documents doc4 in its own snapshot.
784784
.watchCurrents(limboQuery4, 'resume-token-2004')
785785
.watchSnapshots(2004)
786786
.expectEvents(query, { removed: [doc4], fromCache: true })
787787
// The final limbo document listen is already active; resolve it.
788788
.expectLimboDocs(doc5.key)
789-
.expectInactiveLimboDocs()
789+
.expectEnqueuedLimboDocs()
790790
// Resolve the limbo documents doc5 in its own snapshot.
791791
.watchCurrents(limboQuery5, 'resume-token-2005')
792792
.watchSnapshots(2005)
793793
.expectEvents(query, { removed: [doc5], fromCache: false })
794794
.expectLimboDocs()
795-
.expectInactiveLimboDocs()
795+
.expectEnqueuedLimboDocs()
796796
);
797797
}
798798
);
@@ -823,7 +823,7 @@ describeSpec('Limbo Documents:', [], () => {
823823
.watchCurrents(query, 'resume-token-1001')
824824
.watchSnapshots(2000)
825825
.expectLimboDocs(doc1.key)
826-
.expectInactiveLimboDocs(doc2.key)
826+
.expectEnqueuedLimboDocs(doc2.key)
827827
// Limbo document causes query to be "inconsistent"
828828
.expectEvents(query, { fromCache: true })
829829
.watchRemoves(
@@ -835,7 +835,7 @@ describeSpec('Limbo Documents:', [], () => {
835835
// start.
836836
.expectEvents(query, { removed: [doc1], fromCache: true })
837837
.expectLimboDocs(doc2.key)
838-
.expectInactiveLimboDocs()
838+
.expectEnqueuedLimboDocs()
839839
// Reject the listen for the second limbo resolution as well, in order
840840
// to exercise the code path of a rejected limbo resolution without
841841
// any enqueued limbo resolutions.
@@ -845,7 +845,7 @@ describeSpec('Limbo Documents:', [], () => {
845845
)
846846
.expectEvents(query, { removed: [doc2] })
847847
.expectLimboDocs()
848-
.expectInactiveLimboDocs()
848+
.expectEnqueuedLimboDocs()
849849
);
850850
}
851851
);
@@ -910,21 +910,21 @@ describeSpec('Limbo Documents:', [], () => {
910910
.watchAcksFull(query, 1002, docB1, docB2, docB3)
911911
// The docAs are now in limbo; the client begins limbo resolution.
912912
.expectLimboDocs(docA1.key, docA2.key)
913-
.expectInactiveLimboDocs(docA3.key)
913+
.expectEnqueuedLimboDocs(docA3.key)
914914
.watchAcks(docA1Query)
915915
.watchAcks(docA2Query)
916916
.watchCurrents(docA1Query, 'resume-token-1003')
917917
.watchCurrents(docA2Query, 'resume-token-1003')
918918
.watchSnapshots(1003)
919919
.expectEvents(query, { removed: [docA1, docA2], fromCache: true })
920920
.expectLimboDocs(docA3.key)
921-
.expectInactiveLimboDocs()
921+
.expectEnqueuedLimboDocs()
922922
.watchAcks(docA3Query)
923923
.watchCurrents(docA3Query, 'resume-token-1004')
924924
.watchSnapshots(1004)
925925
.expectEvents(query, { removed: [docA3] })
926926
.expectLimboDocs()
927-
.expectInactiveLimboDocs()
927+
.expectEnqueuedLimboDocs()
928928
);
929929
}
930930
);

packages/firestore/test/unit/specs/spec_builder.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ export class SpecBuilder {
359359
expectedState: {
360360
activeTargets: {},
361361
activeLimboDocs: [],
362-
inactiveLimboDocs: []
362+
enqueuedLimboDocs: []
363363
}
364364
};
365365
return this;
@@ -388,7 +388,7 @@ export class SpecBuilder {
388388
expectedState: {
389389
activeTargets: {},
390390
activeLimboDocs: [],
391-
inactiveLimboDocs: []
391+
enqueuedLimboDocs: []
392392
}
393393
};
394394
// Reset our mappings / target ids since all existing listens will be
@@ -404,7 +404,7 @@ export class SpecBuilder {
404404
expectedState: {
405405
activeTargets: {},
406406
activeLimboDocs: [],
407-
inactiveLimboDocs: []
407+
enqueuedLimboDocs: []
408408
}
409409
};
410410
// Reset our mappings / target ids since all existing listens will be
@@ -477,14 +477,15 @@ export class SpecBuilder {
477477
}
478478

479479
/**
480-
* Expects a document to be in limbo, but *without* a targetId.
480+
* Expects a document to be in limbo, enqueued for limbo resolution, and
481+
* therefore *without* an active targetId.
481482
*/
482-
expectInactiveLimboDocs(...keys: DocumentKey[]): this {
483+
expectEnqueuedLimboDocs(...keys: DocumentKey[]): this {
483484
this.assertStep('Limbo expectation requires previous step');
484485
const currentStep = this.currentStep!;
485486

486487
currentStep.expectedState = currentStep.expectedState || {};
487-
currentStep.expectedState.inactiveLimboDocs = keys.map(k =>
488+
currentStep.expectedState.enqueuedLimboDocs = keys.map(k =>
488489
SpecBuilder.keyToSpec(k)
489490
);
490491

0 commit comments

Comments
 (0)