Skip to content

Commit 00b50e1

Browse files
Keep Query state consistent when primary lease is lost (#1162)
1 parent f15f1f0 commit 00b50e1

File tree

4 files changed

+73
-19
lines changed

4 files changed

+73
-19
lines changed

packages/firestore/src/core/sync_engine.ts

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -315,11 +315,13 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
315315
);
316316

317317
if (!targetRemainsActive) {
318-
this.sharedClientState.clearQueryState(queryView.targetId);
319-
this.remoteStore.unlisten(queryView.targetId);
320318
await this.localStore
321319
.releaseQuery(query, /*keepPersistedQueryData=*/ false)
322-
.then(() => this.removeAndCleanupQuery(queryView))
320+
.then(() => {
321+
this.sharedClientState.clearQueryState(queryView.targetId);
322+
this.remoteStore.unlisten(queryView.targetId);
323+
return this.removeAndCleanupQuery(queryView);
324+
})
323325
.then(() => this.localStore.collectGarbage())
324326
.catch(err => this.ignoreIfPrimaryLeaseLoss(err));
325327
}
@@ -882,14 +884,22 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
882884
}
883885
} else if (isPrimary === false && this.isPrimary !== false) {
884886
this.isPrimary = false;
885-
const activeQueries = await this.synchronizeQueryViewsAndRaiseSnapshots(
886-
objUtils.indices(this.queryViewsByTarget)
887-
);
887+
888+
const activeTargets: TargetId[] = [];
889+
890+
let p = Promise.resolve();
891+
objUtils.forEachNumber(this.queryViewsByTarget, (targetId, queryView) => {
892+
if (this.sharedClientState.isLocalQueryTarget(targetId)) {
893+
activeTargets.push(targetId);
894+
} else {
895+
p = p.then(() => this.unlisten(queryView.query));
896+
}
897+
this.remoteStore.unlisten(queryView.targetId);
898+
});
899+
await p;
900+
901+
await this.synchronizeQueryViewsAndRaiseSnapshots(activeTargets);
888902
this.resetLimboDocuments();
889-
for (const queryData of activeQueries) {
890-
// TODO(multitab): Remove query views for non-local queries.
891-
this.remoteStore.unlisten(queryData.targetId);
892-
}
893903
await this.remoteStore.applyPrimaryState(false);
894904
}
895905
}
@@ -1038,10 +1048,12 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
10381048
// Check that the query is still active since the query might have been
10391049
// removed if it has been rejected by the backend.
10401050
if (queryView) {
1041-
this.remoteStore.unlisten(targetId);
10421051
await this.localStore
10431052
.releaseQuery(queryView.query, /*keepPersistedQueryData=*/ false)
1044-
.then(() => this.removeAndCleanupQuery(queryView))
1053+
.then(() => {
1054+
this.remoteStore.unlisten(targetId);
1055+
return this.removeAndCleanupQuery(queryView);
1056+
})
10451057
.catch(err => this.ignoreIfPrimaryLeaseLoss(err));
10461058
}
10471059
}

packages/firestore/src/local/shared_client_state.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,9 @@ export interface SharedClientState {
105105
/** Removes the Query Target ID association from the local client. */
106106
removeLocalQueryTarget(targetId: TargetId): void;
107107

108+
/** Checks whether the target is associated with the local client. */
109+
isLocalQueryTarget(targetId: TargetId): boolean;
110+
108111
/**
109112
* Processes an update to a query target.
110113
*
@@ -700,6 +703,10 @@ export class WebStorageSharedClientState implements SharedClientState {
700703
this.persistClientState();
701704
}
702705

706+
isLocalQueryTarget(targetId: TargetId): boolean {
707+
return this.localClientState.activeTargetIds.has(targetId);
708+
}
709+
703710
clearQueryState(targetId: TargetId): void {
704711
this.removeItem(this.toLocalStorageQueryTargetMetadataKey(targetId));
705712
}
@@ -1086,6 +1093,10 @@ export class MemorySharedClientState implements SharedClientState {
10861093
this.localState.removeQueryTarget(targetId);
10871094
}
10881095

1096+
isLocalQueryTarget(targetId: TargetId): boolean {
1097+
return this.localState.activeTargetIds.has(targetId);
1098+
}
1099+
10891100
clearQueryState(targetId: TargetId): void {
10901101
delete this.queryState[targetId];
10911102
}

packages/firestore/src/util/obj.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,6 @@ export function size<V>(obj: Dict<V>): number {
3939
return count;
4040
}
4141

42-
/** Extracts the numeric indices from a dictionary. */
43-
export function indices<V>(obj: { [numberKey: number]: V }): number[] {
44-
return Object.keys(obj).map(key => {
45-
return Number(key);
46-
});
47-
}
48-
4942
/** Returns the given value if it's defined or the defaultValue otherwise. */
5043
export function defaulted<V>(value: V | undefined, defaultValue: V): V {
5144
return value !== undefined ? value : defaultValue;

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1054,11 +1054,49 @@ describeSpec('Listens:', [], () => {
10541054
.expectEvents(query, { added: [docC] })
10551055
.client(0)
10561056
.runTimer(TimerId.ClientMetadataRefresh)
1057+
// Client 0 recovers from its lease loss and applies the updates from
1058+
// client 1
10571059
.expectPrimaryState(false)
10581060
.expectEvents(query, { added: [docB, docC] })
10591061
);
10601062
});
10611063

1064+
specTest('Query bounces between primaries', ['multi-client'], () => {
1065+
const query = Query.atPath(path('collection'));
1066+
const docA = doc('collection/a', 1000, { key: 'a' });
1067+
const docB = doc('collection/b', 2000, { key: 'b' });
1068+
const docC = doc('collection/c', 3000, { key: 'c' });
1069+
1070+
// Client 0 listens to a query. Client 1 is the primary when the query is
1071+
// first listened to, then the query switches to client 0 and back to client
1072+
// 1.
1073+
return client(1)
1074+
.expectPrimaryState(true)
1075+
.client(0)
1076+
.userListens(query)
1077+
.client(1)
1078+
.expectListen(query)
1079+
.watchAcksFull(query, 1000, docA)
1080+
.client(0)
1081+
.expectEvents(query, { added: [docA] })
1082+
.client(2)
1083+
.stealPrimaryLease()
1084+
.expectListen(query, 'resume-token-1000')
1085+
.client(1)
1086+
.runTimer(TimerId.ClientMetadataRefresh)
1087+
.expectPrimaryState(false)
1088+
.client(2)
1089+
.watchAcksFull(query, 2000, docB)
1090+
.client(0)
1091+
.expectEvents(query, { added: [docB] })
1092+
.client(1)
1093+
.stealPrimaryLease()
1094+
.expectListen(query, 'resume-token-2000')
1095+
.watchAcksFull(query, 2000, docC)
1096+
.client(0)
1097+
.expectEvents(query, { added: [docC] });
1098+
});
1099+
10621100
specTest(
10631101
'Unresponsive primary ignores watch update',
10641102
['multi-client'],

0 commit comments

Comments
 (0)