Skip to content

Commit bd7f13c

Browse files
Only retry DOMException/DOMError
1 parent 77cb183 commit bd7f13c

File tree

5 files changed

+58
-12
lines changed

5 files changed

+58
-12
lines changed

packages/firestore/src/util/async_queue.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -318,9 +318,10 @@ export class AsyncQueue {
318318
/**
319319
* Enqueue a retryable operation.
320320
*
321-
* A retryable operation is rescheduled with backoff if it fails with any
322-
* exception. All retryable operations are executed in order and only run
323-
* if all prior operations were retried successfully.
321+
* A retryable operation is rescheduled with backoff if it fails with a
322+
* DOMException or a DOMError (the error types used by IndexedDB). All
323+
* retryable operations are executed in order and only run if all prior
324+
* operations were retried successfully.
324325
*/
325326
enqueueRetryable(op: () => Promise<void>): void {
326327
this.verifyNotFailed();
@@ -337,8 +338,20 @@ export class AsyncQueue {
337338
deferred.resolve();
338339
this.backoff.reset();
339340
} catch (e) {
340-
logDebug(LOG_TAG, 'Retryable operation failed: ' + e.message);
341-
this.backoff.backoffAndRun(retryingOp);
341+
// We only retry error that match the ones thrown by IndexedDB (see
342+
// https://developer.mozilla.org/en-US/docs/Web/API/IDBTransaction/error)
343+
// We don't use `instanceof` as these types are not available in all
344+
// environments.
345+
if (e.name === 'DOMException' || e.name === 'DOMError') {
346+
logDebug(
347+
LOG_TAG,
348+
'Operation failed with retryable error: ' + e.message
349+
);
350+
this.backoff.backoffAndRun(retryingOp);
351+
} else {
352+
deferred.resolve();
353+
throw e; // Failure will be handled by AsyncQueue
354+
}
342355
}
343356
};
344357
this.enqueueAndForget(retryingOp);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ describeSpec(
4242
// Client 1 has received the WebStorage notification that the write
4343
// has been acknowledged, but failed to process the change. Hence,
4444
// we did not get a user callback. We schedule the first retry and
45-
// make sure that it also does not get processed until
45+
// make sure that it also does not get processed until
4646
// `recoverDatabase` is called.
4747
.runTimer(TimerId.AsyncQueueRetry)
4848
.recoverDatabase()

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import {
4242
MemoryPersistence
4343
} from '../../../src/local/memory_persistence';
4444
import { LruParams } from '../../../src/local/lru_garbage_collector';
45+
import { throwFakeDomException } from '../../util/helpers';
4546

4647
/**
4748
* A test-only persistence implementation that delegates all calls to the
@@ -105,15 +106,15 @@ export class MockPersistence implements Persistence {
105106
return this.delegate.getIndexManager();
106107
}
107108

108-
runTransaction<T>(
109+
async runTransaction<T>(
109110
action: string,
110111
mode: PersistenceTransactionMode,
111112
transactionOperation: (
112113
transaction: PersistenceTransaction
113114
) => PersistencePromise<T>
114115
): Promise<T> {
115116
if (this.injectFailures) {
116-
return Promise.reject(new Error('Injected Failure'));
117+
throwFakeDomException('Simulated retryable error');
117118
} else {
118119
return this.delegate.runTransaction(action, mode, transactionOperation);
119120
}

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

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,17 @@
1515
* limitations under the License.
1616
*/
1717

18-
import { expect } from 'chai';
18+
import * as chaiAsPromised from 'chai-as-promised';
19+
20+
import { expect, use } from 'chai';
1921
import { AsyncQueue, TimerId } from '../../../src/util/async_queue';
2022
import { Code } from '../../../src/util/error';
2123
import { getLogLevel, setLogLevel, LogLevel } from '../../../src/util/log';
2224
import { Deferred, Rejecter, Resolver } from '../../../src/util/promise';
25+
import { fail } from '../../../src/util/assert';
26+
import { throwFakeDomException } from '../../util/helpers';
27+
28+
use(chaiAsPromised);
2329

2430
describe('AsyncQueue', () => {
2531
// We reuse these TimerIds for generic testing.
@@ -212,13 +218,29 @@ describe('AsyncQueue', () => {
212218
queue.enqueueRetryable(async () => {
213219
doStep(1);
214220
if (completedSteps.length === 1) {
215-
throw new Error('Simulated retryable error');
221+
throwFakeDomException('Simulated retryable error');
216222
}
217223
});
218224
await queue.runDelayedOperationsEarly(TimerId.AsyncQueueRetry);
219225
expect(completedSteps).to.deep.equal([1, 1]);
220226
});
221227

228+
it("Doesn't retry internal exceptions", async () => {
229+
const queue = new AsyncQueue();
230+
// We use a Deferred Promise as retryable operations are scheduled only
231+
// when Promise chains are resolved, which can happen after the
232+
// `queue.enqueue` call below.
233+
const deferred = new Deferred<void>();
234+
queue.enqueueRetryable(async () => {
235+
deferred.resolve();
236+
throw fail('Simulated test failure');
237+
});
238+
await deferred.promise;
239+
await expect(
240+
queue.enqueue(() => Promise.resolve())
241+
).to.eventually.be.rejectedWith('Simulated test failure');
242+
});
243+
222244
it('Schedules first retryable attempt with no delay', async () => {
223245
const queue = new AsyncQueue();
224246
const completedSteps: number[] = [];
@@ -244,7 +266,7 @@ describe('AsyncQueue', () => {
244266
queue.enqueueRetryable(async () => {
245267
doStep(1);
246268
if (completedSteps.length === 1) {
247-
throw new Error('Simulated retryable error');
269+
throwFakeDomException('Simulated retryable error');
248270
}
249271
});
250272

@@ -270,7 +292,7 @@ describe('AsyncQueue', () => {
270292
doStep(1);
271293
if (completedSteps.length > 1) {
272294
} else {
273-
throw new Error('Simulated retryable error');
295+
throwFakeDomException('Simulated retryable error');
274296
}
275297
});
276298
queue.enqueueRetryable(async () => {

packages/firestore/test/util/helpers.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -815,6 +815,16 @@ export function expectEqualitySets<T>(
815815
}
816816
}
817817

818+
/**
819+
* Throws an exception that passes `error.name === DOMException` checks even in
820+
* environments without DOMExceptions (such as Node).
821+
*/
822+
export function throwFakeDomException(message: string): never {
823+
const error = new Error(message);
824+
error.name = 'DOMException';
825+
throw error;
826+
}
827+
818828
export function expectFirestoreError(err: Error): void {
819829
expect(err.name).to.equal('FirebaseError');
820830
}

0 commit comments

Comments
 (0)