Skip to content

[Multi-Tab] Detect and recover from GCed Remote Document Changelog #1272

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Oct 3, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/firestore/src/core/sync_engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import { SortedMap } from '../util/sorted_map';
import { isNullOrUndefined } from '../util/types';

import { isPrimaryLeaseLostError } from '../local/indexeddb_persistence';
import { isRemoteDocumentChangesGarbageCollectedError } from '../local/indexeddb_remote_document_cache';
import { isDocumentChangeMissingError } from '../local/indexeddb_remote_document_cache';
import { ClientId, SharedClientState } from '../local/shared_client_state';
import {
QueryTargetState,
Expand Down Expand Up @@ -1016,7 +1016,7 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
);
},
async err => {
if (isRemoteDocumentChangesGarbageCollectedError(err)) {
if (isDocumentChangeMissingError(err)) {
const activeTargets: TargetId[] = [];
objUtils.forEachNumber(this.queryViewsByTarget, target =>
activeTargets.push(target)
Expand Down
31 changes: 14 additions & 17 deletions packages/firestore/src/local/indexeddb_remote_document_cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import { PersistencePromise } from './persistence_promise';
import { RemoteDocumentCache } from './remote_document_cache';
import { SimpleDb, SimpleDbStore, SimpleDbTransaction } from './simple_db';

const REMOTE_DOCUMENT_CHANGELOG_GARBAGE_COLLECTED_ERR_MSG =
const REMOTE_DOCUMENT_CHANGE_MISSING_ERR_MSG =
'The remote document changelog no longer contains all changes for all ' +
'local query views. It may be necessary to rebuild these views.';

Expand Down Expand Up @@ -171,16 +171,22 @@ export class IndexedDbRemoteDocumentCache implements RemoteDocumentCache {
);
let firstIteration = true;

return documentChangesStore(transaction)
const changesStore = documentChangesStore(transaction);
return changesStore
.iterate({ range }, (_, documentChange) => {
if (firstIteration) {
// If our client was throttled for more than 30 minutes, another
// client may have garbage collected the remote document changelog.
if (this._lastProcessedDocumentChangeId + 1 !== documentChange.id) {
return PersistencePromise.reject(
new FirestoreError(
Code.DATA_LOSS,
REMOTE_DOCUMENT_CHANGELOG_GARBAGE_COLLECTED_ERR_MSG
// Reset the `lastProcessedDocumentChangeId` to allow further
// invocations to successfully return the changes after this
// rejection.
return this.synchronizeLastDocumentChangeId(changesStore).next(() =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to verify, did you consider just letting this iteration continue so that line 199 can correctly update _lastProcessedDocumentChangeId, and then we could (I think?) get rid of the synchronizeLastDocumentChangeId? I think the reduced code complexity would outweigh any potential performance saving of getting to skip the changedKeys accounting...

PersistencePromise.reject(
new FirestoreError(
Code.DATA_LOSS,
REMOTE_DOCUMENT_CHANGE_MISSING_ERR_MSG
)
)
);
}
Expand Down Expand Up @@ -221,13 +227,6 @@ export class IndexedDbRemoteDocumentCache implements RemoteDocumentCache {
return documentChangesStore(transaction).delete(range);
}

resetLastProcessedDocumentChange(
transaction: PersistenceTransaction
): PersistencePromise<void> {
const store = documentChangesStore(transaction);
return this.synchronizeLastDocumentChangeId(store);
}

private synchronizeLastDocumentChangeId(
documentChangesStore: SimpleDbStore<
DbRemoteDocumentChangesKey,
Expand All @@ -247,12 +246,10 @@ export class IndexedDbRemoteDocumentCache implements RemoteDocumentCache {
}
}

export function isRemoteDocumentChangesGarbageCollectedError(
err: FirestoreError
): boolean {
export function isDocumentChangeMissingError(err: FirestoreError): boolean {
return (
err.code === Code.DATA_LOSS &&
err.message === REMOTE_DOCUMENT_CHANGELOG_GARBAGE_COLLECTED_ERR_MSG
err.message === REMOTE_DOCUMENT_CHANGE_MISSING_ERR_MSG
);
}

Expand Down
10 changes: 0 additions & 10 deletions packages/firestore/src/local/local_store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -869,14 +869,4 @@ export class LocalStore {
}
);
}

resetLastDocumentChangeId(): Promise<void> {
return this.persistence.runTransaction(
'Reset last document change ID',
'readonly',
txn => {
return this.remoteDocuments.resetLastProcessedDocumentChange(txn);
}
);
}
}
7 changes: 0 additions & 7 deletions packages/firestore/src/local/memory_remote_document_cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,4 @@ export class MemoryRemoteDocumentCache implements RemoteDocumentCache {

return PersistencePromise.resolve(changedDocs);
}

resetLastProcessedDocumentChange(
transaction: PersistenceTransaction
): PersistencePromise<void> {
this.newDocumentChanges = documentKeySet();
return PersistencePromise.resolve();
}
}
15 changes: 4 additions & 11 deletions packages/firestore/src/local/remote_document_cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,20 +89,13 @@ export interface RemoteDocumentCache {
* Returns the set of documents that have been updated since the last call.
* If this is the first call, returns the set of changes since client
* initialization.
*
* If the changelog was garbage collected and can no longer be replayed,
* `getNewDocumentChanges` will reject the returned Promise. Further
* invocations will return document changes since the point of rejection.
*/
// PORTING NOTE: This is only used for multi-tab synchronization.
getNewDocumentChanges(
transaction: PersistenceTransaction
): PersistencePromise<MaybeDocumentMap>;

/**
* Skips all existing change log entries in IndexedDb and moves the change log
* cursor past the last existing change. This method should only be called if
* `getNewDocumentChanges()`can no longer replay all changes and all views
* have already been manually synchronized.
*/
// PORTING NOTE: This is only used for multi-tab synchronization.
resetLastProcessedDocumentChange(
transaction: PersistenceTransaction
): PersistencePromise<void>;
}
11 changes: 4 additions & 7 deletions packages/firestore/test/unit/local/remote_document_cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import {

import {
IndexedDbRemoteDocumentCache,
isRemoteDocumentChangesGarbageCollectedError
isDocumentChangeMissingError
} from '../../../src/local/indexeddb_remote_document_cache';
import {
DbRemoteDocumentChanges,
Expand Down Expand Up @@ -155,14 +155,11 @@ describe('IndexedDbRemoteDocumentCache', () => {
.getNewDocumentChanges()
.then(
() => fail('Missing expected error'),
err =>
expect(isRemoteDocumentChangesGarbageCollectedError(err)).to.be.ok
err => expect(isDocumentChangeMissingError(err)).to.be.ok
);

// Recover by moving moving the change log cursor to the last existing
// entry. We assume that as part of the recovery, all relevant changes have
// been processed manually (see `SyncEngine.applyTargetState`).
await readerCache.resetLastProcessDocumentChange();
// Ensure that we can retrieve future changes after the we processed the
// error
await writerCache.addEntries([doc('a/4', 4, DOC_DATA)]);
changedDocs = await readerCache.getNewDocumentChanges();
assertMatches([doc('a/4', 4, DOC_DATA)], changedDocs);
Expand Down
26 changes: 6 additions & 20 deletions packages/firestore/test/unit/local/test_remote_document_cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,31 +78,17 @@ export class TestRemoteDocumentCache {
);
}

resetLastProcessDocumentChange(): Promise<void> {
return this.persistence.runTransaction(
'resetLastProcessDocumentChange',
'readonly',
txn => {
return this.cache.resetLastProcessedDocumentChange(txn);
}
);
}

removeDocumentChangesThroughChangeId(changeId: number): Promise<void> {
if (!(this.cache instanceof IndexedDbRemoteDocumentCache)) {
throw new Error(
'Can only removeDocumentChangesThroughChangeId() in IndexedDb'
);
}
return this.persistence.runTransaction(
'removeDocumentChangesThroughChangeId',
'readwrite-primary',
txn => {
return (this
.cache as IndexedDbRemoteDocumentCache).removeDocumentChangesThroughChangeId(
txn,
changeId
);
if (!(this.cache instanceof IndexedDbRemoteDocumentCache)) {
throw new Error(
'Can only removeDocumentChangesThroughChangeId() in IndexedDb'
);
}
return this.cache.removeDocumentChangesThroughChangeId(txn, changeId);
}
);
}
Expand Down