Skip to content

Commit 19dae50

Browse files
Ignore failures for notifyLocalViewChanges
1 parent 0804217 commit 19dae50

File tree

2 files changed

+159
-63
lines changed

2 files changed

+159
-63
lines changed

packages/firestore/src/local/local_store.ts

Lines changed: 57 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -694,57 +694,70 @@ export class LocalStore {
694694
/**
695695
* Notify local store of the changed views to locally pin documents.
696696
*/
697-
notifyLocalViewChanges(viewChanges: LocalViewChanges[]): Promise<void> {
698-
return this.persistence
699-
.runTransaction('notifyLocalViewChanges', 'readwrite', txn => {
700-
return PersistencePromise.forEach(
701-
viewChanges,
702-
(viewChange: LocalViewChanges) => {
703-
return PersistencePromise.forEach(
704-
viewChange.addedKeys,
705-
(key: DocumentKey) =>
706-
this.persistence.referenceDelegate.addReference(
707-
txn,
708-
viewChange.targetId,
709-
key
710-
)
711-
).next(() =>
712-
PersistencePromise.forEach(
713-
viewChange.removedKeys,
697+
async notifyLocalViewChanges(viewChanges: LocalViewChanges[]): Promise<void> {
698+
try {
699+
await this.persistence.runTransaction(
700+
'notifyLocalViewChanges',
701+
'readwrite',
702+
txn => {
703+
return PersistencePromise.forEach(
704+
viewChanges,
705+
(viewChange: LocalViewChanges) => {
706+
return PersistencePromise.forEach(
707+
viewChange.addedKeys,
714708
(key: DocumentKey) =>
715-
this.persistence.referenceDelegate.removeReference(
709+
this.persistence.referenceDelegate.addReference(
716710
txn,
717711
viewChange.targetId,
718712
key
719713
)
720-
)
721-
);
722-
}
714+
).next(() =>
715+
PersistencePromise.forEach(
716+
viewChange.removedKeys,
717+
(key: DocumentKey) =>
718+
this.persistence.referenceDelegate.removeReference(
719+
txn,
720+
viewChange.targetId,
721+
key
722+
)
723+
)
724+
);
725+
}
726+
);
727+
}
728+
);
729+
} catch (e) {
730+
if (e.name === 'IndexedDbTransactionError') {
731+
// If `notifyLocalViewChanges` fails, we did not advance the sequence
732+
// number for the documents that were included in this transaction.
733+
// This might trigger them to be deleted earlier than they otherwise
734+
// would have, but it should not invalidate the integrity of the data.
735+
logDebug(LOG_TAG, 'Failed to persist target assignments: ' + e);
736+
} else {
737+
throw e;
738+
}
739+
}
740+
741+
for (const viewChange of viewChanges) {
742+
const targetId = viewChange.targetId;
743+
744+
if (!viewChange.fromCache) {
745+
const targetData = this.targetDataByTarget.get(targetId);
746+
debugAssert(
747+
targetData !== null,
748+
`Can't set limbo-free snapshot version for unknown target: ${targetId}`
723749
);
724-
})
725-
.then(() => {
726-
for (const viewChange of viewChanges) {
727-
const targetId = viewChange.targetId;
728-
729-
if (!viewChange.fromCache) {
730-
const targetData = this.targetDataByTarget.get(targetId);
731-
debugAssert(
732-
targetData !== null,
733-
`Can't set limbo-free snapshot version for unknown target: ${targetId}`
734-
);
735750

736-
// Advance the last limbo free snapshot version
737-
const lastLimboFreeSnapshotVersion = targetData.snapshotVersion;
738-
const updatedTargetData = targetData.withLastLimboFreeSnapshotVersion(
739-
lastLimboFreeSnapshotVersion
740-
);
741-
this.targetDataByTarget = this.targetDataByTarget.insert(
742-
targetId,
743-
updatedTargetData
744-
);
745-
}
746-
}
747-
});
751+
// Advance the last limbo free snapshot version
752+
const updatedTargetData = targetData.withLastLimboFreeSnapshotVersion(
753+
targetData.snapshotVersion
754+
);
755+
this.targetDataByTarget = this.targetDataByTarget.insert(
756+
targetId,
757+
updatedTargetData
758+
);
759+
}
760+
}
748761
}
749762

750763
/**

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

Lines changed: 102 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { client, spec } from './spec_builder';
2020
import { TimerId } from '../../../src/util/async_queue';
2121
import { Query } from '../../../src/core/query';
2222
import { Code } from '../../../src/util/error';
23-
import { doc, filter, path } from '../../util/helpers';
23+
import { deletedDoc, doc, filter, path } from '../../util/helpers';
2424
import { RpcError } from './spec_rpc_error';
2525

2626
describeSpec('Persistence Recovery', ['no-ios', 'no-android'], () => {
@@ -124,24 +124,34 @@ describeSpec('Persistence Recovery', ['no-ios', 'no-android'], () => {
124124
);
125125

126126
specTest('Recovers when write cannot be persisted', [], () => {
127-
return spec()
128-
.userSets('collection/key1', { foo: 'a' })
129-
.expectNumOutstandingWrites(1)
130-
.failDatabaseTransactions({
131-
'Locally write mutations': true,
132-
notifyLocalViewChanges: true,
133-
'Get next mutation batch': true,
134-
'Get last stream token': true
135-
})
136-
.userSets('collection/key2', { bar: 'b' })
137-
.expectUserCallbacks({ rejected: ['collection/key2'] })
138-
.recoverDatabase()
139-
.expectNumOutstandingWrites(1)
140-
.userSets('collection/key3', { baz: 'c' })
141-
.expectNumOutstandingWrites(2)
142-
.writeAcks('collection/key1', 1)
143-
.writeAcks('collection/key3', 2)
144-
.expectNumOutstandingWrites(0);
127+
return (
128+
spec()
129+
.userSets('collection/key1', { foo: 'a' })
130+
.expectNumOutstandingWrites(1)
131+
// We fail the write if we cannot persist the local mutation (via
132+
// 'Locally write mutations').
133+
.failDatabaseTransactions({
134+
'Locally write mutations': true
135+
})
136+
.userSets('collection/key2', { bar: 'b' })
137+
.expectUserCallbacks({ rejected: ['collection/key2'] })
138+
// The write is considered successful if we can persist the local mutation
139+
// but fail to update view assignments (via 'notifyLocalViewChanges').
140+
.failDatabaseTransactions({
141+
'Locally write mutations': false,
142+
notifyLocalViewChanges: true,
143+
'Get next mutation batch': false
144+
})
145+
.userSets('collection/key3', { bar: 'b' })
146+
.recoverDatabase()
147+
.expectNumOutstandingWrites(2)
148+
.userSets('collection/key4', { baz: 'c' })
149+
.expectNumOutstandingWrites(3)
150+
.writeAcks('collection/key1', 1)
151+
.writeAcks('collection/key3', 2)
152+
.writeAcks('collection/key4', 3)
153+
.expectNumOutstandingWrites(0)
154+
);
145155
});
146156

147157
specTest('Does not surface non-persisted writes', [], () => {
@@ -188,6 +198,79 @@ describeSpec('Persistence Recovery', ['no-ios', 'no-android'], () => {
188198
.expectEvents(query, { metadata: [doc1, doc3] });
189199
});
190200

201+
specTest(
202+
'Surfaces local documents if notifyLocalViewChanges fails',
203+
[],
204+
() => {
205+
const query = Query.atPath(path('collection'));
206+
const doc1Local = doc(
207+
'collection/key1',
208+
0,
209+
{ foo: 'a' },
210+
{ hasLocalMutations: true }
211+
);
212+
const doc1 = doc('collection/key1', 1, { foo: 'a' });
213+
const doc2 = doc('collection/key2', 2, { foo: 'b' });
214+
return spec()
215+
.userListens(query)
216+
.failDatabaseTransactions({
217+
'Locally write mutations': false,
218+
notifyLocalViewChanges: true
219+
})
220+
.userSets('collection/key1', { foo: 'a' })
221+
.expectEvents(query, {
222+
added: [doc1Local],
223+
fromCache: true,
224+
hasPendingWrites: true
225+
})
226+
.recoverDatabase()
227+
.runTimer(TimerId.AsyncQueueRetry)
228+
.writeAcks('collection/key1', 1)
229+
.failDatabaseTransactions({
230+
'Apply remote event': false,
231+
notifyLocalViewChanges: true,
232+
'Get last remote snapshot version': false
233+
})
234+
.watchAcksFull(query, 1000, doc1, doc2)
235+
.expectEvents(query, {
236+
metadata: [doc1],
237+
added: [doc2]
238+
});
239+
}
240+
);
241+
242+
specTest(
243+
'Excludes documents from future queries even if notifyLocalViewChanges fails',
244+
[],
245+
() => {
246+
const query = Query.atPath(path('collection'));
247+
const doc1 = doc('collection/key1', 1000, { foo: 'a' });
248+
const deletedDoc1 = deletedDoc('collection/key1', 2000);
249+
return (
250+
spec()
251+
.userListens(query)
252+
.watchAcksFull(query, 1000, doc1)
253+
.expectEvents(query, {
254+
added: [doc1]
255+
})
256+
.failDatabaseTransactions({
257+
'Apply remote event': false,
258+
notifyLocalViewChanges: true,
259+
'Get last remote snapshot version': false
260+
})
261+
.watchSends({ removed: [query] }, deletedDoc1)
262+
.watchSnapshots(2000)
263+
.expectEvents(query, {
264+
removed: [doc1]
265+
})
266+
.recoverDatabase()
267+
.userUnlistens(query)
268+
// No event since the document was removed
269+
.userListens(query)
270+
);
271+
}
272+
);
273+
191274
specTest('Fails targets that cannot be allocated', [], () => {
192275
const query1 = Query.atPath(path('collection1'));
193276
const query2 = Query.atPath(path('collection2'));

0 commit comments

Comments
 (0)