diff --git a/packages/firestore/src/local/simple_db.ts b/packages/firestore/src/local/simple_db.ts index 77d6b6ae971..1adbdf9f081 100644 --- a/packages/firestore/src/local/simple_db.ts +++ b/packages/firestore/src/local/simple_db.ts @@ -406,6 +406,15 @@ export interface IterateOptions { reverse?: boolean; } +/** An error that wraps exceptions that thrown during IndexedDB execution. */ +export class IndexedDbTransactionError extends FirestoreError { + name = 'IndexedDbTransactionError'; + + constructor(cause: Error) { + super(Code.UNAVAILABLE, 'IndexedDB transaction failed: ' + cause); + } +} + /** * Wraps an IDBTransaction and exposes a store() method to get a handle to a * specific object store. @@ -432,7 +441,9 @@ export class SimpleDbTransaction { }; this.transaction.onabort = () => { if (transaction.error) { - this.completionDeferred.reject(transaction.error); + this.completionDeferred.reject( + new IndexedDbTransactionError(transaction.error) + ); } else { this.completionDeferred.resolve(); } @@ -441,7 +452,7 @@ export class SimpleDbTransaction { const error = checkForAndReportiOSError( (event.target as IDBRequest).error! ); - this.completionDeferred.reject(error); + this.completionDeferred.reject(new IndexedDbTransactionError(error)); }; } diff --git a/packages/firestore/src/util/async_queue.ts b/packages/firestore/src/util/async_queue.ts index 9293634e20d..7b4bde18262 100644 --- a/packages/firestore/src/util/async_queue.ts +++ b/packages/firestore/src/util/async_queue.ts @@ -318,9 +318,10 @@ export class AsyncQueue { /** * Enqueue a retryable operation. * - * A retryable operation is rescheduled with backoff if it fails with any - * exception. All retryable operations are executed in order and only run - * if all prior operations were retried successfully. + * A retryable operation is rescheduled with backoff if it fails with a + * IndexedDbTransactionError (the error type used by SimpleDb). All + * retryable operations are executed in order and only run if all prior + * operations were retried successfully. */ enqueueRetryable(op: () => Promise): void { this.verifyNotFailed(); @@ -337,8 +338,13 @@ export class AsyncQueue { deferred.resolve(); this.backoff.reset(); } catch (e) { - logDebug(LOG_TAG, 'Retryable operation failed: ' + e.message); - this.backoff.backoffAndRun(retryingOp); + if (e.name === 'IndexedDbTransactionError') { + logDebug(LOG_TAG, 'Operation failed with retryable error: ' + e); + this.backoff.backoffAndRun(retryingOp); + } else { + deferred.resolve(); + throw e; // Failure will be handled by AsyncQueue + } } }; this.enqueueAndForget(retryingOp); diff --git a/packages/firestore/test/unit/specs/recovery_spec.test.ts b/packages/firestore/test/unit/specs/recovery_spec.test.ts index b5dcf024479..8abb2bb2867 100644 --- a/packages/firestore/test/unit/specs/recovery_spec.test.ts +++ b/packages/firestore/test/unit/specs/recovery_spec.test.ts @@ -42,7 +42,7 @@ describeSpec( // Client 1 has received the WebStorage notification that the write // has been acknowledged, but failed to process the change. Hence, // we did not get a user callback. We schedule the first retry and - // make sure that it also does not get processed until + // make sure that it also does not get processed until // `recoverDatabase` is called. .runTimer(TimerId.AsyncQueueRetry) .recoverDatabase() diff --git a/packages/firestore/test/unit/specs/spec_test_components.ts b/packages/firestore/test/unit/specs/spec_test_components.ts index b5371b0e5e1..fa27ef9d06a 100644 --- a/packages/firestore/test/unit/specs/spec_test_components.ts +++ b/packages/firestore/test/unit/specs/spec_test_components.ts @@ -35,6 +35,7 @@ import { TargetCache } from '../../../src/local/target_cache'; import { RemoteDocumentCache } from '../../../src/local/remote_document_cache'; import { IndexManager } from '../../../src/local/index_manager'; import { PersistencePromise } from '../../../src/local/persistence_promise'; +import { IndexedDbTransactionError } from '../../../src/local/simple_db'; import { debugAssert } from '../../../src/util/assert'; import { MemoryEagerDelegate, @@ -113,7 +114,9 @@ export class MockPersistence implements Persistence { ) => PersistencePromise ): Promise { if (this.injectFailures) { - return Promise.reject(new Error('Injected Failure')); + return Promise.reject( + new IndexedDbTransactionError(new Error('Simulated retryable error')) + ); } else { return this.delegate.runTransaction(action, mode, transactionOperation); } diff --git a/packages/firestore/test/unit/util/async_queue.test.ts b/packages/firestore/test/unit/util/async_queue.test.ts index b4f3b829916..e667462d47f 100644 --- a/packages/firestore/test/unit/util/async_queue.test.ts +++ b/packages/firestore/test/unit/util/async_queue.test.ts @@ -15,11 +15,17 @@ * limitations under the License. */ -import { expect } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; + +import { expect, use } from 'chai'; import { AsyncQueue, TimerId } from '../../../src/util/async_queue'; import { Code } from '../../../src/util/error'; import { getLogLevel, setLogLevel, LogLevel } from '../../../src/util/log'; import { Deferred, Rejecter, Resolver } from '../../../src/util/promise'; +import { fail } from '../../../src/util/assert'; +import { IndexedDbTransactionError } from '../../../src/local/simple_db'; + +use(chaiAsPromised); describe('AsyncQueue', () => { // We reuse these TimerIds for generic testing. @@ -212,13 +218,31 @@ describe('AsyncQueue', () => { queue.enqueueRetryable(async () => { doStep(1); if (completedSteps.length === 1) { - throw new Error('Simulated retryable error'); + throw new IndexedDbTransactionError( + new Error('Simulated retryable error') + ); } }); await queue.runDelayedOperationsEarly(TimerId.AsyncQueueRetry); expect(completedSteps).to.deep.equal([1, 1]); }); + it("Doesn't retry internal exceptions", async () => { + const queue = new AsyncQueue(); + // We use a deferred Promise as retryable operations are scheduled only + // when Promise chains are resolved, which can happen after the + // `queue.enqueue()` call below. + const deferred = new Deferred(); + queue.enqueueRetryable(async () => { + deferred.resolve(); + throw fail('Simulated test failure'); + }); + await deferred.promise; + await expect( + queue.enqueue(() => Promise.resolve()) + ).to.eventually.be.rejectedWith('Simulated test failure'); + }); + it('Schedules first retryable attempt with no delay', async () => { const queue = new AsyncQueue(); const completedSteps: number[] = []; @@ -244,7 +268,9 @@ describe('AsyncQueue', () => { queue.enqueueRetryable(async () => { doStep(1); if (completedSteps.length === 1) { - throw new Error('Simulated retryable error'); + throw new IndexedDbTransactionError( + new Error('Simulated retryable error') + ); } }); @@ -270,7 +296,9 @@ describe('AsyncQueue', () => { doStep(1); if (completedSteps.length > 1) { } else { - throw new Error('Simulated retryable error'); + throw new IndexedDbTransactionError( + new Error('Simulated retryable error') + ); } }); queue.enqueueRetryable(async () => { diff --git a/packages/firestore/test/util/node_persistence.ts b/packages/firestore/test/util/node_persistence.ts index 90977b0295f..aa95831da50 100644 --- a/packages/firestore/test/util/node_persistence.ts +++ b/packages/firestore/test/util/node_persistence.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2018 Google Inc. + * Copyright 2018 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License.