Skip to content

Commit 759b877

Browse files
Ignore failures for notifyLocalViewChanges (#3062)
1 parent f3f1b17 commit 759b877

File tree

2 files changed

+163
-63
lines changed

2 files changed

+163
-63
lines changed

packages/firestore/src/local/local_store.ts

Lines changed: 58 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -694,57 +694,71 @@ 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 update sequence numbers: ' + 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 lastLimboFreeSnapshotVersion = targetData.snapshotVersion;
753+
const updatedTargetData = targetData.withLastLimboFreeSnapshotVersion(
754+
lastLimboFreeSnapshotVersion
755+
);
756+
this.targetDataByTarget = this.targetDataByTarget.insert(
757+
targetId,
758+
updatedTargetData
759+
);
760+
}
761+
}
748762
}
749763

750764
/**

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

Lines changed: 105 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,82 @@ 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+
'Get next mutation batch': false,
220+
'Set last stream token': false
221+
})
222+
.userSets('collection/key1', { foo: 'a' })
223+
.expectEvents(query, {
224+
added: [doc1Local],
225+
fromCache: true,
226+
hasPendingWrites: true
227+
})
228+
.recoverDatabase()
229+
.runTimer(TimerId.AsyncQueueRetry)
230+
.writeAcks('collection/key1', 1)
231+
.failDatabaseTransactions({
232+
'Apply remote event': false,
233+
notifyLocalViewChanges: true,
234+
'Get last remote snapshot version': false
235+
})
236+
.watchAcksFull(query, 1000, doc1, doc2)
237+
.expectEvents(query, {
238+
metadata: [doc1],
239+
added: [doc2]
240+
});
241+
}
242+
);
243+
244+
specTest(
245+
'Excludes documents from future queries even if notifyLocalViewChanges fails',
246+
[],
247+
() => {
248+
const query = Query.atPath(path('collection'));
249+
const doc1 = doc('collection/key1', 1000, { foo: 'a' });
250+
const deletedDoc1 = deletedDoc('collection/key1', 2000);
251+
return (
252+
spec()
253+
.withGCEnabled(false)
254+
.userListens(query)
255+
.watchAcksFull(query, 1000, doc1)
256+
.expectEvents(query, {
257+
added: [doc1]
258+
})
259+
.failDatabaseTransactions({
260+
'Apply remote event': false,
261+
notifyLocalViewChanges: true,
262+
'Get last remote snapshot version': false
263+
})
264+
.watchSends({ removed: [query] }, deletedDoc1)
265+
.watchSnapshots(2000)
266+
.expectEvents(query, {
267+
removed: [doc1]
268+
})
269+
.recoverDatabase()
270+
.userUnlistens(query)
271+
// No event since the document was removed
272+
.userListens(query, 'resume-token-1000')
273+
);
274+
}
275+
);
276+
191277
specTest('Fails targets that cannot be allocated', [], () => {
192278
const query1 = Query.atPath(path('collection1'));
193279
const query2 = Query.atPath(path('collection2'));

0 commit comments

Comments
 (0)