Skip to content

Commit 23312dd

Browse files
Zombie all client data (#1013)
1 parent af30f6c commit 23312dd

File tree

2 files changed

+48
-38
lines changed

2 files changed

+48
-38
lines changed

packages/firestore/src/local/indexeddb_persistence.ts

Lines changed: 47 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,6 @@ const CLIENT_METADATA_MAX_AGE_MS = 5000;
6666
* if they're already performing an IndexedDB operation.
6767
*/
6868
const CLIENT_METADATA_REFRESH_INTERVAL_MS = 4000;
69-
/** LocalStorage location to indicate a zombied client id (see class comment). */
70-
const ZOMBIED_PRIMARY_LOCALSTORAGE_SUFFIX = 'zombiedClientId';
7169
/** User-facing error when the primary lease is required but not available. */
7270
const PRIMARY_LEASE_LOST_ERROR_MSG =
7371
'The current tab is not in the required state to perform this operation. ' +
@@ -81,6 +79,10 @@ const UNSUPPORTED_PLATFORM_ERROR_MSG =
8179
' IndexedDB or is known to have an incomplete implementation. Offline' +
8280
' persistence has been disabled.';
8381

82+
// The format of the LocalStorage key that stores zombied client is:
83+
// firestore_zombie_<persistence_prefix>_<instance_key>
84+
const ZOMBIED_CLIENTS_KEY_PREFIX = 'firestore_zombie';
85+
8486
/**
8587
* An IndexedDB-backed instance of Persistence. Data is stored persistently
8688
* across sessions.
@@ -127,7 +129,6 @@ export class IndexedDbPersistence implements Persistence {
127129
private isPrimary = false;
128130
private networkEnabled = true;
129131
private dbName: string;
130-
private localStoragePrefix: string;
131132

132133
/**
133134
* Set to an Error object if we encounter an unrecoverable error. All further
@@ -153,15 +154,14 @@ export class IndexedDbPersistence implements Persistence {
153154
private primaryStateListener: PrimaryStateListener = _ => Promise.resolve();
154155

155156
constructor(
156-
prefix: string,
157+
private readonly persistenceKey: string,
157158
private readonly clientId: ClientId,
158159
platform: Platform,
159160
private readonly queue: AsyncQueue,
160161
serializer: JsonProtoSerializer
161162
) {
162-
this.dbName = prefix + IndexedDbPersistence.MAIN_DATABASE;
163+
this.dbName = persistenceKey + IndexedDbPersistence.MAIN_DATABASE;
163164
this.serializer = new LocalSerializer(serializer);
164-
this.localStoragePrefix = prefix;
165165
this.document = platform.document;
166166
this.window = platform.window;
167167
}
@@ -308,7 +308,7 @@ export class IndexedDbPersistence implements Persistence {
308308
const currentLeaseIsValid =
309309
currentPrimary !== null &&
310310
this.isWithinMaxAge(currentPrimary.leaseTimestampMs) &&
311-
currentPrimary.ownerId !== this.getZombiedClientId();
311+
!this.isClientZombied(currentPrimary.ownerId);
312312

313313
// A client is eligible for the primary lease if:
314314
// - its network is enabled and the client's tab is in the foreground.
@@ -356,7 +356,8 @@ export class IndexedDbPersistence implements Persistence {
356356
.iterate((key, otherClient, control) => {
357357
if (
358358
this.clientId !== otherClient.clientId &&
359-
this.isWithinMaxAge(otherClient.updateTimeMs)
359+
this.isWithinMaxAge(otherClient.updateTimeMs) &&
360+
!this.isClientZombied(otherClient.clientId)
360361
) {
361362
const otherClientHasBetterNetworkState =
362363
!this.networkEnabled && otherClient.networkEnabled;
@@ -393,10 +394,9 @@ export class IndexedDbPersistence implements Persistence {
393394
if (!this.started) {
394395
return Promise.resolve();
395396
}
396-
// TODO(multitab): Similar to the zombied client ID, we should write an
397-
// entry to Local Storage first to indicate that we are no longer alive.
398-
// This will help us when the shutdown handler doesn't run to completion.
399397
this.started = false;
398+
399+
this.markClientZombied();
400400
if (this.clientMetadataRefresher) {
401401
this.clientMetadataRefresher.cancel();
402402
}
@@ -412,6 +412,10 @@ export class IndexedDbPersistence implements Persistence {
412412
}
413413
);
414414
this.simpleDb.close();
415+
416+
// Remove the entry marking the client as zombied from LocalStorage since
417+
// we successfully deleted its metadata from IndexedDb.
418+
this.removeClientZombiedEntry();
415419
if (deleteData) {
416420
await SimpleDb.delete(this.dbName);
417421
}
@@ -506,7 +510,7 @@ export class IndexedDbPersistence implements Persistence {
506510
const currentLeaseIsValid =
507511
currentPrimary !== null &&
508512
this.isWithinMaxAge(currentPrimary.leaseTimestampMs) &&
509-
currentPrimary.ownerId !== this.getZombiedClientId();
513+
!this.isClientZombied(currentPrimary.ownerId);
510514

511515
if (currentLeaseIsValid && !this.isLocalClient(currentPrimary)) {
512516
if (!currentPrimary.allowTabSynchronization) {
@@ -641,9 +645,7 @@ export class IndexedDbPersistence implements Persistence {
641645
// Note: In theory, this should be scheduled on the AsyncQueue since it
642646
// accesses internal state. We execute this code directly during shutdown
643647
// to make sure it gets a chance to run.
644-
if (this.isPrimary) {
645-
this.setZombiedClientId(this.clientId);
646-
}
648+
this.markClientZombied();
647649

648650
this.queue.enqueue(() => {
649651
// Attempt graceful shutdown (including releasing our owner lease), but
@@ -671,7 +673,7 @@ export class IndexedDbPersistence implements Persistence {
671673
* zombied due to their tab closing) from LocalStorage, or null if no such
672674
* record exists.
673675
*/
674-
private getZombiedClientId(): ClientId | null {
676+
private isClientZombied(clientId: ClientId): boolean {
675677
if (this.window.localStorage === undefined) {
676678
assert(
677679
process.env.USE_MOCK_PERSISTENCE === 'YES',
@@ -681,15 +683,17 @@ export class IndexedDbPersistence implements Persistence {
681683
}
682684

683685
try {
684-
const zombiedClientId = this.window.localStorage.getItem(
685-
this.zombiedClientLocalStorageKey()
686-
);
686+
const isZombied =
687+
this.window.localStorage.getItem(
688+
this.zombiedClientLocalStorageKey(clientId)
689+
) !== null;
687690
log.debug(
688691
LOG_TAG,
689-
'Zombied clientId from LocalStorage:',
690-
zombiedClientId
692+
`Client '${clientId}' ${
693+
isZombied ? 'is' : 'is not'
694+
} zombied in LocalStorage`
691695
);
692-
return zombiedClientId;
696+
return isZombied;
693697
} catch (e) {
694698
// Gracefully handle if LocalStorage isn't available / working.
695699
log.error(LOG_TAG, 'Failed to get zombied client id.', e);
@@ -698,29 +702,35 @@ export class IndexedDbPersistence implements Persistence {
698702
}
699703

700704
/**
701-
* Records a zombied primary client (a primary client that had its tab closed)
702-
* in LocalStorage or, if passed null, deletes any recorded zombied owner.
705+
* Record client as zombied (a client that had its tab closed). Zombied
706+
* clients are ignored during primary tab selection.
703707
*/
704-
private setZombiedClientId(zombiedClientId: ClientId | null): void {
708+
private markClientZombied(): void {
705709
try {
706-
if (zombiedClientId === null) {
707-
this.window.localStorage.removeItem(
708-
this.zombiedClientLocalStorageKey()
709-
);
710-
} else {
711-
this.window.localStorage.setItem(
712-
this.zombiedClientLocalStorageKey(),
713-
zombiedClientId
714-
);
715-
}
710+
// TODO(multitab): Garbage Collect Local Storage
711+
this.window.localStorage.setItem(
712+
this.zombiedClientLocalStorageKey(this.clientId),
713+
String(Date.now())
714+
);
716715
} catch (e) {
717716
// Gracefully handle if LocalStorage isn't available / working.
718717
log.error('Failed to set zombie owner id.', e);
719718
}
720719
}
721720

722-
private zombiedClientLocalStorageKey(): string {
723-
return this.localStoragePrefix + ZOMBIED_PRIMARY_LOCALSTORAGE_SUFFIX;
721+
/** Removes the zombied client entry if it exists. */
722+
private removeClientZombiedEntry(): void {
723+
try {
724+
this.window.localStorage.removeItem(
725+
this.zombiedClientLocalStorageKey(this.clientId)
726+
);
727+
} catch (e) {
728+
// Ignore
729+
}
730+
}
731+
732+
private zombiedClientLocalStorageKey(clientId: ClientId): string {
733+
return `${ZOMBIED_CLIENTS_KEY_PREFIX}_${this.persistenceKey}_${clientId}`;
724734
}
725735
}
726736

packages/firestore/test/util/test_platform.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ export class SharedFakeWebStorage {
146146
}
147147

148148
private getItem(key: string): string | null {
149-
return this.data.get(key);
149+
return this.data.has(key) ? this.data.get(key) : null;
150150
}
151151

152152
private key(index: number): string | null {

0 commit comments

Comments
 (0)