Skip to content

Commit 786082d

Browse files
authored
Fix MutationQueue issue resulting in re-sending acknowledged writes. (#559)
getNextMutationBatchAfterBatchId() was not respecting highestAcknowledgedBatchId and therefore we were resending writes if they had been acknowledged but not removed (aka the held write acks case). This showed up when a user disabled / enabled the network as reported here and I've included a spec test to emulate that case: firebase/firebase-ios-sdk#772
1 parent 69c9a97 commit 786082d

File tree

6 files changed

+91
-8
lines changed

6 files changed

+91
-8
lines changed

packages/firestore/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
neither succeeds nor fails within 10 seconds, the SDK will consider itself
44
"offline", causing get() calls to resolve with cached results, rather than
55
continuing to wait.
6+
- [fixed] Fixed a potential race condition after calling `enableNetwork()` that
7+
could result in a "Mutation batchIDs must be acknowledged in order" assertion
8+
crash.
69

710
# 0.3.2
811
- [fixed] Fixed a regression in Firebase JS release 4.9.0 that could in certain

packages/firestore/src/local/indexeddb_mutation_queue.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -255,14 +255,20 @@ export class IndexedDbMutationQueue implements MutationQueue {
255255
transaction: PersistenceTransaction,
256256
batchId: BatchId
257257
): PersistencePromise<MutationBatch | null> {
258-
const range = IDBKeyRange.lowerBound(this.keyForBatchId(batchId + 1));
258+
// All batches with batchId <= this.metadata.lastAcknowledgedBatchId have
259+
// been acknowledged so the first unacknowledged batch after batchID will
260+
// have a batchID larger than both of these values.
261+
const nextBatchId =
262+
Math.max(batchId, this.metadata.lastAcknowledgedBatchId) + 1;
263+
264+
const range = IDBKeyRange.lowerBound(this.keyForBatchId(nextBatchId));
259265
let foundBatch: MutationBatch | null = null;
260266
return mutationsStore(transaction)
261267
.iterate({ range }, (key, dbBatch, control) => {
262268
if (dbBatch.userId === this.userId) {
263269
assert(
264-
dbBatch.batchId > batchId,
265-
'Should have found mutation after ' + batchId
270+
dbBatch.batchId >= nextBatchId,
271+
'Should have found mutation after ' + nextBatchId
266272
);
267273
foundBatch = this.serializer.fromDbMutationBatch(dbBatch);
268274
}

packages/firestore/src/local/memory_mutation_queue.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,11 +182,11 @@ export class MemoryMutationQueue implements MutationQueue {
182182
// All batches with batchId <= this.highestAcknowledgedBatchId have been
183183
// acknowledged so the first unacknowledged batch after batchID will have a
184184
// batchID larger than both of these values.
185-
batchId = Math.max(batchId + 1, this.highestAcknowledgedBatchId);
185+
const nextBatchId = Math.max(batchId, this.highestAcknowledgedBatchId) + 1;
186186

187187
// The requested batchId may still be out of range so normalize it to the
188188
// start of the queue.
189-
const rawIndex = this.indexOfBatchId(batchId);
189+
const rawIndex = this.indexOfBatchId(nextBatchId);
190190
let index = rawIndex < 0 ? 0 : rawIndex;
191191

192192
// Finally return the first non-tombstone batch.

packages/firestore/test/unit/local/mutation_queue.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040

4141
import * as persistenceHelpers from './persistence_test_helpers';
4242
import { TestMutationQueue } from './test_mutation_queue';
43+
import { addEqualityMatcher } from '../../util/equality_matcher';
4344

4445
let persistence: Persistence;
4546
let mutationQueue: TestMutationQueue;
@@ -120,6 +121,8 @@ describe('IndexedDbMutationQueue', () => {
120121
* implementations.
121122
*/
122123
function genericMutationQueueTests() {
124+
addEqualityMatcher();
125+
123126
beforeEach(() => {
124127
mutationQueue = new TestMutationQueue(
125128
persistence,
@@ -324,6 +327,24 @@ function genericMutationQueueTests() {
324327
expect(notFound).to.be.null;
325328
});
326329

330+
it('getNextMutationBatchAfterBatchId() skips acknowledged batches', async () => {
331+
const batches = await createBatches(3);
332+
expect(
333+
await mutationQueue.getNextMutationBatchAfterBatchId(BATCHID_UNKNOWN)
334+
).to.deep.equal(batches[0]);
335+
336+
await mutationQueue.acknowledgeBatch(batches[0], emptyByteString());
337+
expect(
338+
await mutationQueue.getNextMutationBatchAfterBatchId(BATCHID_UNKNOWN)
339+
).to.deep.equal(batches[1]);
340+
expect(
341+
await mutationQueue.getNextMutationBatchAfterBatchId(batches[0].batchId)
342+
).to.deep.equal(batches[1]);
343+
expect(
344+
await mutationQueue.getNextMutationBatchAfterBatchId(batches[1].batchId)
345+
).to.deep.equal(batches[2]);
346+
});
347+
327348
it('can getAllMutationBatchesThroughBatchId()', async () => {
328349
const batches = await createBatches(10);
329350
await makeHolesInBatches([2, 6, 7], batches);

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -825,17 +825,17 @@ abstract class TestRunner {
825825
private validateStateExpectations(expectation: StateExpectation): void {
826826
if (expectation) {
827827
if ('numOutstandingWrites' in expectation) {
828-
expect(this.remoteStore.outstandingWrites()).to.deep.equal(
828+
expect(this.remoteStore.outstandingWrites()).to.equal(
829829
expectation.numOutstandingWrites
830830
);
831831
}
832832
if ('writeStreamRequestCount' in expectation) {
833-
expect(this.connection.writeStreamRequestCount).to.deep.equal(
833+
expect(this.connection.writeStreamRequestCount).to.equal(
834834
expectation.writeStreamRequestCount
835835
);
836836
}
837837
if ('watchStreamRequestCount' in expectation) {
838-
expect(this.connection.watchStreamRequestCount).to.deep.equal(
838+
expect(this.connection.watchStreamRequestCount).to.equal(
839839
expectation.watchStreamRequestCount
840840
);
841841
}

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

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,59 @@ describeSpec('Writes:', [], () => {
394394
);
395395
});
396396

397+
specTest(
398+
'Held writes are not re-sent after disable/enable network.',
399+
[],
400+
() => {
401+
const query = Query.atPath(path('collection'));
402+
const docALocal = doc(
403+
'collection/a',
404+
0,
405+
{ v: 1 },
406+
{ hasLocalMutations: true }
407+
);
408+
const docA = doc('collection/a', 1000, { v: 1 });
409+
410+
return (
411+
spec()
412+
.userListens(query)
413+
.watchAcksFull(query, 500)
414+
.expectEvents(query, {})
415+
.userSets('collection/a', { v: 1 })
416+
.expectEvents(query, {
417+
hasPendingWrites: true,
418+
added: [docALocal]
419+
})
420+
// ack write but without a watch event.
421+
.writeAcks(1000)
422+
423+
// handshake + write = 2 requests
424+
.expectWriteStreamRequestCount(2)
425+
426+
.disableNetwork()
427+
.expectEvents(query, {
428+
hasPendingWrites: true,
429+
fromCache: true
430+
})
431+
432+
// handshake + write + close = 3 requests
433+
.expectWriteStreamRequestCount(3)
434+
435+
.enableNetwork()
436+
.expectActiveTargets({ query, resumeToken: 'resume-token-500' })
437+
438+
// acked write should /not/ have been resent, so count should still be 3
439+
.expectWriteStreamRequestCount(3)
440+
441+
// Finally watch catches up.
442+
.watchAcksFull(query, 2000, docA)
443+
.expectEvents(query, {
444+
metadata: [docA]
445+
})
446+
);
447+
}
448+
);
449+
397450
specTest(
398451
'Held writes are released when there are no queries left.',
399452
[],

0 commit comments

Comments
 (0)