Skip to content

Commit cb49563

Browse files
Don't release primary lease
1 parent 692b789 commit cb49563

File tree

12 files changed

+269
-175
lines changed

12 files changed

+269
-175
lines changed

packages/firestore/src/core/sync_engine.ts

Lines changed: 102 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import { RemoteEvent, TargetChange } from '../remote/remote_event';
3333
import { RemoteStore } from '../remote/remote_store';
3434
import { RemoteSyncer } from '../remote/remote_syncer';
3535
import { assert, fail } from '../util/assert';
36-
import { Code, FirestoreError } from '../util/error';
36+
import { FirestoreError } from '../util/error';
3737
import * as log from '../util/log';
3838
import { AnyJs, primitiveComparator } from '../util/misc';
3939
import { ObjectMap } from '../util/obj_map';
@@ -57,7 +57,8 @@ import {
5757
AddedLimboDocument,
5858
LimboDocumentChange,
5959
RemovedLimboDocument,
60-
View, ViewChange,
60+
View,
61+
ViewChange,
6162
ViewDocumentChanges
6263
} from './view';
6364
import { ViewSnapshot } from './view_snapshot';
@@ -68,6 +69,7 @@ import {
6869
import { ClientId, SharedClientState } from '../local/shared_client_state';
6970
import { SortedSet } from '../util/sorted_set';
7071
import * as objUtils from '../util/obj';
72+
import { isPrimaryLeaseLostError } from '../local/indexeddb_persistence';
7173

7274
const LOG_TAG = 'SyncEngine';
7375

@@ -208,7 +210,7 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
208210
queryData.targetId
209211
);
210212
targetId = queryData.targetId;
211-
viewSnapshot = await this.initializeViewAndComputeInitialSnapshot(
213+
viewSnapshot = await this.initializeViewAndComputeSnapshot(
212214
queryData,
213215
status === 'current'
214216
);
@@ -221,7 +223,11 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
221223
return targetId;
222224
}
223225

224-
private initializeViewAndComputeInitialSnapshot(
226+
/**
227+
* Registers a view for a previously unknown query and computes its initial
228+
* snapshot.
229+
*/
230+
private initializeViewAndComputeSnapshot(
225231
queryData: QueryData,
226232
current: boolean
227233
): Promise<ViewSnapshot> {
@@ -265,15 +271,32 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
265271
}
266272

267273
/**
268-
* Reconcile the list of synced documents in the local views with those from
269-
* persistence.
274+
* Reconcile the list of synced documents in an existing view with those
275+
* from persistence.
270276
*/
271277
// PORTING NOTE: Multi-tab only.
272-
private async synchronizeLocalView(query:Query, targetId: TargetId): Promise<ViewChange> {
273-
return this.localStore.executeQuery(query).then(docs => {
274-
const queryView = this.queryViewsByTarget[targetId];
275-
assert(!!queryView, 'Expected queryView to be defined');
276-
return queryView.view.synchronizeWithRemoteDocuments(docs);
278+
private synchronizeViewAndComputeSnapshot(
279+
query: Query,
280+
targetId: TargetId
281+
): Promise<ViewChange> {
282+
return this.localStore.executeQuery(query).then(async docs => {
283+
return this.localStore
284+
.remoteDocumentKeys(targetId)
285+
.then(async remoteKeys => {
286+
const queryView = this.queryViewsByTarget[targetId];
287+
assert(!!queryView, 'Cannot reconcile missing view');
288+
const viewChange = queryView.view.synchronizeWithRemoteDocuments(
289+
docs,
290+
remoteKeys,
291+
/* resetLimboDocuments= */ this.isPrimary,
292+
/* resetCurrent= */ this.isPrimary
293+
);
294+
await this.updateTrackedLimbos(
295+
queryView.targetId,
296+
viewChange.limboChanges
297+
);
298+
return viewChange;
299+
});
277300
});
278301
}
279302

@@ -763,7 +786,7 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
763786
private async tryRecoverFromPrimaryLeaseLoss(
764787
err: FirestoreError
765788
): Promise<void> {
766-
if (err.code === Code.FAILED_PRECONDITION) {
789+
if (isPrimaryLeaseLostError(err)) {
767790
log.debug(
768791
LOG_TAG,
769792
'Unexpectedly lost primary lease, reverting to secondary'
@@ -802,7 +825,6 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
802825
async applyPrimaryState(isPrimary: boolean): Promise<void> {
803826
if (isPrimary === true && this.isPrimary !== true) {
804827
this.isPrimary = true;
805-
806828
await this.remoteStore.applyPrimaryState(true);
807829

808830
// Secondary tabs only maintain Views for their local listeners and the
@@ -811,52 +833,78 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
811833
// server considers to be in the target). So when a secondary becomes
812834
// primary, we need to need to make sure that all views for all targets
813835
// match the state on disk.
814-
let p = Promise.resolve();
815836
const activeTargets = this.sharedClientState.getAllActiveQueryTargets();
816-
activeTargets.forEach(targetId => {
817-
p = p.then(async () => {
818-
let queryData;
819-
const query = await this.localStore.getQueryForTarget(targetId);
820-
if (this.queryViewsByTarget[targetId] === undefined) {
821-
// For queries that never executed on this client, we need to
822-
// allocate the query in LocalStore and initialize a new View.
823-
queryData = await this.localStore.allocateQuery(query);
824-
await this.initializeViewAndComputeInitialSnapshot(
825-
queryData,
826-
false
827-
);
828-
} else {
829-
// For queries that have a local View, we need to update their state
830-
// in LocalStore (as the resume token and the snapshot version
831-
// might have changed) and reconcile their views with the persisted
832-
// state (the list of syncedDocuments may have gotten out of sync).
833-
await this.localStore.releaseQuery(query, true);
834-
queryData = await this.localStore.allocateQuery(query);
835-
await this.synchronizeLocalView(query, targetId);
836-
}
837-
this.remoteStore.listen(queryData);
838-
});
839-
});
840-
await p;
837+
const activeQueries = await this.synchronizeLocalViews(
838+
activeTargets.toArray()
839+
);
840+
for (const queryData of activeQueries) {
841+
this.remoteStore.listen(queryData);
842+
}
841843
} else if (isPrimary === false && this.isPrimary !== false) {
842844
this.isPrimary = false;
845+
const activeQueries = await this.synchronizeLocalViews(
846+
objUtils.indices(this.queryViewsByTarget)
847+
);
848+
this.resetLimboDocuments();
849+
for (const queryData of activeQueries) {
850+
// TODO(multitab): Remove query views for non-local queries.
851+
this.remoteStore.unlisten(queryData.targetId);
852+
}
853+
await this.remoteStore.applyPrimaryState(false);
854+
}
855+
}
856+
857+
// PORTING NOTE: Multi-tab only.
858+
private resetLimboDocuments() : void {
859+
objUtils.forEachNumber(this.limboKeysByTarget, targetId => {
860+
this.remoteStore.unlisten(targetId);
861+
});
862+
this.limboKeysByTarget = [];
863+
this.limboTargetsByKey = new SortedMap<DocumentKey, TargetId>(
864+
DocumentKey.comparator
865+
);
866+
}
843867

844-
let p = Promise.resolve();
845-
objUtils.forEachNumber(this.queryViewsByTarget, targetId => {
846-
p = p.then(async () => {
847-
let queryView = this.queryViewsByTarget[targetId];
848-
// TODO(multitab): Remove query views for non-local queries.
849-
const viewChange = await this.synchronizeLocalView(queryView.query, targetId);
850-
// const viewChange = queryView.view.clearLimboDocuments();
868+
/**
869+
* Reconcile the list of synced documents in the local views with those from
870+
* persistence.
871+
*/
872+
// PORTING NOTE: Multi-tab only.
873+
private synchronizeLocalViews(targets: TargetId[]): Promise<QueryData[]> {
874+
let p = Promise.resolve();
875+
const activeQueries: QueryData[] = [];
876+
for (const targetId of targets) {
877+
p = p.then(async () => {
878+
let queryData;
879+
const query = await this.localStore.getQueryForTarget(targetId);
880+
if (this.queryViewsByTarget[targetId] === undefined) {
881+
assert(
882+
this.isPrimary,
883+
'A secondary tab should never have an active query without an active view.'
884+
);
885+
// For queries that never executed on this client, we need to
886+
// allocate the query in LocalStore and initialize a new View.
887+
queryData = await this.localStore.allocateQuery(query);
888+
await this.initializeViewAndComputeSnapshot(queryData, false);
889+
} else {
890+
// For queries that have a local View, we need to update their state
891+
// in LocalStore (as the resume token and the snapshot version
892+
// might have changed) and reconcile their views with the persisted
893+
// state (the list of syncedDocuments may have gotten out of sync).
894+
await this.localStore.releaseQuery(query, true);
895+
queryData = await this.localStore.allocateQuery(query);
896+
const viewChange = await this.synchronizeViewAndComputeSnapshot(
897+
query,
898+
queryData.targetId
899+
);
851900
if (viewChange.snapshot) {
852901
this.syncEngineListener!.onWatchChange([viewChange.snapshot]);
853902
}
854-
});
855-
this.remoteStore.unlisten(targetId);
903+
}
904+
activeQueries.push(queryData);
856905
});
857-
858-
await this.remoteStore.applyPrimaryState(false);
859906
}
907+
return p.then(() => activeQueries);
860908
}
861909

862910
// PORTING NOTE: Multi-tab only
@@ -871,13 +919,13 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
871919
error?: FirestoreError
872920
): Promise<void> {
873921
if (this.isPrimary) {
874-
// If we receive a target state notification via Web Storage, we are
922+
// If we receive a target state notification via WebStorage, we are
875923
// either already secondary or another tab has taken the primary lease.
876924
log.debug(
877925
LOG_TAG,
878-
'Unexpectedly received query state notification when already primary. Ignoring.'
926+
'Ignoring unexpected query state notification as primary.'
879927
);
880-
return;
928+
return;
881929
}
882930

883931
if (this.queryViewsByTarget[targetId]) {
@@ -927,7 +975,7 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
927975
const query = await this.localStore.getQueryForTarget(targetId);
928976
assert(!!query, `Query data for active target ${targetId} not found`);
929977
const queryData = await this.localStore.allocateQuery(query);
930-
await this.initializeViewAndComputeInitialSnapshot(
978+
await this.initializeViewAndComputeSnapshot(
931979
queryData,
932980
/*current=*/ false
933981
);

packages/firestore/src/core/view.ts

Lines changed: 15 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -384,19 +384,23 @@ export class View {
384384
}
385385

386386
// PORTING NOTE: Multi-tab only.
387-
synchronizeWithRemoteDocuments(remoteDocs: MaybeDocumentMap): ViewChange {
388-
389-
this.limboDocuments = documentKeySet();
390-
const docChanges = this.computeDocChanges(remoteDocs);
391-
const viewChange = this.applyChanges(docChanges, false);
387+
synchronizeWithRemoteDocuments(
388+
localDocs: MaybeDocumentMap,
389+
remoteKeys: DocumentKeySet,
390+
resetLimboDocuments: boolean,
391+
resetCurrent: boolean
392+
): ViewChange {
393+
if (resetLimboDocuments) {
394+
this.limboDocuments = documentKeySet();
395+
}
392396

393-
let keys = documentKeySet();
397+
if (resetCurrent) {
398+
this.current = false;
399+
}
394400

395-
remoteDocs.forEach(key => {
396-
keys = keys.add(key);
397-
});
398-
this._syncedDocuments = keys;
399-
return viewChange;
401+
this._syncedDocuments = remoteKeys;
402+
const docChanges = this.computeDocChanges(localDocs);
403+
return this.applyChanges(docChanges, resetLimboDocuments);
400404
}
401405

402406
/**
@@ -413,31 +417,6 @@ export class View {
413417
!this.mutatedKeys.isEmpty()
414418
);
415419
}
416-
417-
clearLimboDocuments(): ViewChange {
418-
this.limboDocuments = documentKeySet();
419-
const synced = this.current;
420-
const newSyncState = synced ? SyncState.Synced : SyncState.Local;
421-
const syncStateChanged = newSyncState !== this.syncState;
422-
423-
if (syncStateChanged) {
424-
this.syncState = newSyncState;
425-
426-
const snap: ViewSnapshot = new ViewSnapshot(
427-
this.query,
428-
this.documentSet,
429-
this.documentSet,
430-
[],
431-
this.syncState === SyncState.Local,
432-
!this.mutatedKeys.isEmpty(),
433-
syncStateChanged,
434-
/* excludesMetadataChanges= */ false
435-
);
436-
return { snapshot: snap, limboChanges: [] };
437-
} else {
438-
return { limboChanges: [] };
439-
}
440-
}
441420
}
442421

443422
function compareChangeType(c1: ChangeType, c2: ChangeType): number {

packages/firestore/src/local/indexeddb_persistence.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -244,15 +244,15 @@ export class IndexedDbPersistence implements Persistence {
244244
const wasPrimary = this.isPrimary;
245245
this.isPrimary = canActAsPrimary;
246246

247-
// Always call the primary state listener, since SyncEngine may have
248-
// changed the primary state to 'false'.
249-
this.queue.enqueue(async () => {
250-
// Verify that `shutdown()` hasn't been called yet by the time
251-
// we invoke the `primaryStateListener`.
252-
if (this.started) {
253-
return this.primaryStateListener(this.isPrimary);
254-
}
255-
});
247+
if (wasPrimary !== this.isPrimary) {
248+
this.queue.enqueue(async () => {
249+
// Verify that `shutdown()` hasn't been called yet by the time
250+
// we invoke the `primaryStateListener`.
251+
if (this.started) {
252+
return this.primaryStateListener(this.isPrimary);
253+
}
254+
});
255+
}
256256

257257
if (wasPrimary && !this.isPrimary) {
258258
return this.releasePrimaryLeaseIfHeld(txn);
@@ -734,6 +734,12 @@ export class IndexedDbPersistence implements Persistence {
734734
}
735735
}
736736

737+
export function isPrimaryLeaseLostError(err: FirestoreError) : boolean {
738+
return (
739+
err.code === Code.FAILED_PRECONDITION &&
740+
err.message === PRIMARY_LEASE_LOST_ERROR_MSG
741+
);
742+
}
737743
/**
738744
* Helper to get a typed SimpleDbStore for the owner object store.
739745
*/

packages/firestore/src/local/persistence.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,9 @@ export interface Persistence {
100100
shutdown(deleteData?: boolean): Promise<void>;
101101

102102
/**
103-
* Registers a listener that gets called immediately wih the current primary
104-
* state and then periodically as the lease is verified (likely with the same
105-
* state).
103+
* Registers a listener that gets called when the primary state of the
104+
* instance changes. Upon registering, this listener is invoked immediately
105+
* with the current primary state.
106106
*
107107
* PORTING NOTE: This is only used for Web multi-tab.
108108
*/

0 commit comments

Comments
 (0)