Skip to content

Commit 1550964

Browse files
author
Michael Lehenbauer
committed
Fix MutationQueue issue resulting in re-sending acknowledged writes.
getNextMutationBatchAfterBatchId() was not respecting highestAcknowledgedBatchId and therefore we were resending writes after the network was disabled / re-enabled as reported here: firebase/firebase-ios-sdk#772
1 parent 69c9a97 commit 1550964

File tree

5 files changed

+85
-8
lines changed

5 files changed

+85
-8
lines changed

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: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,56 @@ 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+
.disableNetwork()
424+
.expectEvents(query, {
425+
hasPendingWrites: true,
426+
fromCache: true
427+
})
428+
429+
// handshake + write + close = 3 requests
430+
.expectWriteStreamRequestCount(3)
431+
432+
.enableNetwork()
433+
.expectActiveTargets({ query, resumeToken: 'resume-token-500' })
434+
435+
// acked write should /not/ have been resent, so count should still be 3
436+
.expectWriteStreamRequestCount(3)
437+
438+
// Finally watch catches up.
439+
.watchAcksFull(query, 2000, docA)
440+
.expectEvents(query, {
441+
metadata: [docA]
442+
})
443+
);
444+
}
445+
);
446+
397447
specTest(
398448
'Held writes are released when there are no queries left.',
399449
[],

0 commit comments

Comments
 (0)