Skip to content

Commit d7c9ed4

Browse files
Only retry DOMException/DOMError (#2919)
1 parent fc3d539 commit d7c9ed4

File tree

5 files changed

+61
-13
lines changed

5 files changed

+61
-13
lines changed

packages/firestore/src/local/simple_db.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,15 @@ export interface IterateOptions {
409409
reverse?: boolean;
410410
}
411411

412+
/** An error that wraps exceptions that thrown during IndexedDB execution. */
413+
export class IndexedDbTransactionError extends FirestoreError {
414+
name = 'IndexedDbTransactionError';
415+
416+
constructor(cause: Error) {
417+
super(Code.UNAVAILABLE, 'IndexedDB transaction failed: ' + cause);
418+
}
419+
}
420+
412421
/**
413422
* Wraps an IDBTransaction and exposes a store() method to get a handle to a
414423
* specific object store.
@@ -435,7 +444,9 @@ export class SimpleDbTransaction {
435444
};
436445
this.transaction.onabort = () => {
437446
if (transaction.error) {
438-
this.completionDeferred.reject(transaction.error);
447+
this.completionDeferred.reject(
448+
new IndexedDbTransactionError(transaction.error)
449+
);
439450
} else {
440451
this.completionDeferred.resolve();
441452
}
@@ -444,7 +455,7 @@ export class SimpleDbTransaction {
444455
const error = checkForAndReportiOSError(
445456
(event.target as IDBRequest).error!
446457
);
447-
this.completionDeferred.reject(error);
458+
this.completionDeferred.reject(new IndexedDbTransactionError(error));
448459
};
449460
}
450461

packages/firestore/src/util/async_queue.ts

Lines changed: 11 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+
* IndexedDbTransactionError (the error type used by SimpleDb). 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,13 @@ 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+
if (e.name === 'IndexedDbTransactionError') {
342+
logDebug(LOG_TAG, 'Operation failed with retryable error: ' + e);
343+
this.backoff.backoffAndRun(retryingOp);
344+
} else {
345+
deferred.resolve();
346+
throw e; // Failure will be handled by AsyncQueue
347+
}
342348
}
343349
};
344350
this.enqueueAndForget(retryingOp);

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { TargetCache } from '../../../src/local/target_cache';
3535
import { RemoteDocumentCache } from '../../../src/local/remote_document_cache';
3636
import { IndexManager } from '../../../src/local/index_manager';
3737
import { PersistencePromise } from '../../../src/local/persistence_promise';
38+
import { IndexedDbTransactionError } from '../../../src/local/simple_db';
3839
import { debugAssert } from '../../../src/util/assert';
3940
import {
4041
MemoryEagerDelegate,
@@ -113,7 +114,9 @@ export class MockPersistence implements Persistence {
113114
) => PersistencePromise<T>
114115
): Promise<T> {
115116
if (this.injectFailures) {
116-
return Promise.reject(new Error('Injected Failure'));
117+
return Promise.reject(
118+
new IndexedDbTransactionError(new Error('Simulated retryable error'))
119+
);
117120
} else {
118121
return this.delegate.runTransaction(action, mode, transactionOperation);
119122
}

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

Lines changed: 32 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 { IndexedDbTransactionError } from '../../../src/local/simple_db';
27+
28+
use(chaiAsPromised);
2329

2430
describe('AsyncQueue', () => {
2531
// We reuse these TimerIds for generic testing.
@@ -212,13 +218,31 @@ describe('AsyncQueue', () => {
212218
queue.enqueueRetryable(async () => {
213219
doStep(1);
214220
if (completedSteps.length === 1) {
215-
throw new Error('Simulated retryable error');
221+
throw new IndexedDbTransactionError(
222+
new Error('Simulated retryable error')
223+
);
216224
}
217225
});
218226
await queue.runDelayedOperationsEarly(TimerId.AsyncQueueRetry);
219227
expect(completedSteps).to.deep.equal([1, 1]);
220228
});
221229

230+
it("Doesn't retry internal exceptions", async () => {
231+
const queue = new AsyncQueue();
232+
// We use a deferred Promise as retryable operations are scheduled only
233+
// when Promise chains are resolved, which can happen after the
234+
// `queue.enqueue()` call below.
235+
const deferred = new Deferred<void>();
236+
queue.enqueueRetryable(async () => {
237+
deferred.resolve();
238+
throw fail('Simulated test failure');
239+
});
240+
await deferred.promise;
241+
await expect(
242+
queue.enqueue(() => Promise.resolve())
243+
).to.eventually.be.rejectedWith('Simulated test failure');
244+
});
245+
222246
it('Schedules first retryable attempt with no delay', async () => {
223247
const queue = new AsyncQueue();
224248
const completedSteps: number[] = [];
@@ -244,7 +268,9 @@ describe('AsyncQueue', () => {
244268
queue.enqueueRetryable(async () => {
245269
doStep(1);
246270
if (completedSteps.length === 1) {
247-
throw new Error('Simulated retryable error');
271+
throw new IndexedDbTransactionError(
272+
new Error('Simulated retryable error')
273+
);
248274
}
249275
});
250276

@@ -270,7 +296,9 @@ describe('AsyncQueue', () => {
270296
doStep(1);
271297
if (completedSteps.length > 1) {
272298
} else {
273-
throw new Error('Simulated retryable error');
299+
throw new IndexedDbTransactionError(
300+
new Error('Simulated retryable error')
301+
);
274302
}
275303
});
276304
queue.enqueueRetryable(async () => {

packages/firestore/test/util/node_persistence.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* @license
3-
* Copyright 2018 Google Inc.
3+
* Copyright 2018 Google LLC
44
*
55
* Licensed under the Apache License, Version 2.0 (the "License");
66
* you may not use this file except in compliance with the License.

0 commit comments

Comments
 (0)