Skip to content

Commit 2187e60

Browse files
Don't delay retryable operations
1 parent acd89ee commit 2187e60

File tree

4 files changed

+88
-29
lines changed

4 files changed

+88
-29
lines changed

packages/firestore/src/util/async_queue.ts

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -207,9 +207,9 @@ export class AsyncQueue {
207207
// The last promise in the queue.
208208
private tail: Promise<unknown> = Promise.resolve();
209209

210-
// The last retryable operation. Retryable operation are run in order and
210+
// A list of retryable operations Retryable operation are run in order and
211211
// retried with backoff.
212-
private retryableTail: Promise<void> = Promise.resolve();
212+
private retryableOps: Array<() => Promise<void>> = [];
213213

214214
// Is this AsyncQueue being shut down? Once it is set to true, it will not
215215
// be changed again.
@@ -323,32 +323,32 @@ export class AsyncQueue {
323323
* operations were retried successfully.
324324
*/
325325
enqueueRetryable(op: () => Promise<void>): void {
326-
this.verifyNotFailed();
327-
328-
if (this._isShuttingDown) {
329-
return;
330-
}
326+
this.retryableOps.push(op);
327+
this.enqueueAndForget(() => this.retryNextOp());
328+
}
331329

332-
this.retryableTail = this.retryableTail.then(() => {
333-
const deferred = new Deferred<void>();
334-
const retryingOp = async (): Promise<void> => {
335-
try {
336-
await op();
337-
deferred.resolve();
338-
this.backoff.reset();
339-
} catch (e) {
340-
if (isIndexedDbTransactionError(e)) {
341-
logDebug(LOG_TAG, 'Operation failed with retryable error: ' + e);
342-
this.backoff.backoffAndRun(retryingOp);
343-
} else {
344-
deferred.resolve();
345-
throw e; // Failure will be handled by AsyncQueue
346-
}
330+
/**
331+
* Runs the next operation from the retryable queue. If the operation fails,
332+
* reschedules with backoff.
333+
*/
334+
private async retryNextOp(): Promise<void> {
335+
const op = this.retryableOps.shift();
336+
337+
if (op) {
338+
try {
339+
await op();
340+
this.backoff.reset();
341+
} catch (e) {
342+
if (isIndexedDbTransactionError(e)) {
343+
logDebug(LOG_TAG, 'Operation failed with retryable error: ' + e);
344+
this.retryableOps.unshift(op);
345+
} else {
346+
throw e; // Failure will be handled by AsyncQueue
347347
}
348-
};
349-
this.enqueueAndForget(retryingOp);
350-
return deferred.promise;
351-
});
348+
}
349+
350+
this.backoff.backoffAndRun(() => this.retryNextOp());
351+
}
352352
}
353353

354354
private enqueueInternal<T extends unknown>(op: () => Promise<T>): Promise<T> {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -787,6 +787,7 @@ describeSpec('Persistence Recovery', ['no-ios', 'no-android'], () => {
787787
.expectActiveTargets({ query })
788788
// We are now user 2
789789
.expectEvents(query, { removed: [doc1], fromCache: true })
790+
.runTimer(TimerId.AsyncQueueRetry)
790791
// We are now user 1
791792
.expectEvents(query, {
792793
added: [doc1],

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ abstract class TestRunner {
277277
}
278278

279279
async shutdown(): Promise<void> {
280-
await this.queue.enqueue(async () => {
280+
await this.queue.enqueueAndInitiateShutdown(async () => {
281281
if (this.started) {
282282
await this.doShutdown();
283283
}

packages/firestore/test/unit/util/async_queue.test.ts

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -297,8 +297,7 @@ describe('AsyncQueue', () => {
297297

298298
queue.enqueueRetryable(async () => {
299299
doStep(1);
300-
if (completedSteps.length > 1) {
301-
} else {
300+
if (completedSteps.length === 1) {
302301
throw new IndexedDbTransactionError(
303302
new Error('Simulated retryable error')
304303
);
@@ -313,6 +312,65 @@ describe('AsyncQueue', () => {
313312
expect(completedSteps).to.deep.equal([1, 1, 2]);
314313
});
315314

315+
it('Doesn not delay retryable operations that succeed', async () => {
316+
const queue = new AsyncQueue();
317+
const completedSteps: number[] = [];
318+
const doStep = (n: number): void => {
319+
completedSteps.push(n);
320+
};
321+
322+
queue.enqueueRetryable(async () => {
323+
doStep(1);
324+
});
325+
queue.enqueueAndForget(async () => {
326+
doStep(2);
327+
});
328+
await queue.enqueue(async () => {
329+
doStep(3);
330+
});
331+
332+
expect(completedSteps).to.deep.equal([1, 2, 3]);
333+
});
334+
335+
it('Catches up when retryable operation fails', async () => {
336+
const queue = new AsyncQueue();
337+
const completedSteps: number[] = [];
338+
const doStep = (n: number): void => {
339+
completedSteps.push(n);
340+
};
341+
342+
const blockingPromise = new Deferred<void>();
343+
344+
queue.enqueueRetryable(async () => {
345+
doStep(1);
346+
if (completedSteps.length === 1) {
347+
throw new IndexedDbTransactionError(
348+
new Error('Simulated retryable error')
349+
);
350+
}
351+
});
352+
queue.enqueueAndForget(async () => {
353+
doStep(2);
354+
});
355+
queue.enqueueRetryable(async () => {
356+
doStep(3);
357+
blockingPromise.resolve();
358+
});
359+
await blockingPromise.promise;
360+
361+
// Once all existing retryable operations succeeded, they are scheduled
362+
// in the order they are enqueued.
363+
queue.enqueueAndForget(async () => {
364+
doStep(4);
365+
});
366+
await queue.enqueue(async () => {
367+
doStep(5);
368+
});
369+
370+
await blockingPromise.promise;
371+
expect(completedSteps).to.deep.equal([1, 2, 1, 3, 4, 5]);
372+
});
373+
316374
it('Can drain (non-delayed) operations', async () => {
317375
const queue = new AsyncQueue();
318376
const completedSteps: number[] = [];

0 commit comments

Comments
 (0)