From d4d37407c87b7c032a16679e96b318146bbdee22 Mon Sep 17 00:00:00 2001 From: theodab Date: Thu, 13 Oct 2022 15:35:55 -0700 Subject: [PATCH] fix(offline): Add storage muxer init timeout (#4566) In some cases, indexedDB.open() can end up calling neither callback. When this does happen, according to my initial testing, it happens consistently when reloading the page, so it's not a one-off fluke but presumably some sort of implementation or browser install problem. If that does happen, the init promise of the storage muxer hangs forever, potentially blocking other operations from happening. This adds a timeout to the invocation of indexedDB.open(), after which the operation fails with a new error. --- lib/offline/indexeddb/storage_mechanism.js | 24 +++++++++++++++++ lib/util/error.js | 7 +++++ test/offline/storage_integration.js | 30 ++++++++++++++++++++++ 3 files changed, 61 insertions(+) diff --git a/lib/offline/indexeddb/storage_mechanism.js b/lib/offline/indexeddb/storage_mechanism.js index 9019e7d4ca..c44be375d7 100644 --- a/lib/offline/indexeddb/storage_mechanism.js +++ b/lib/offline/indexeddb/storage_mechanism.js @@ -16,6 +16,7 @@ goog.require('shaka.offline.indexeddb.V5StorageCell'); goog.require('shaka.util.Error'); goog.require('shaka.util.PublicPromise'); goog.require('shaka.util.Platform'); +goog.require('shaka.util.Timer'); /** @@ -55,8 +56,25 @@ shaka.offline.indexeddb.StorageMechanism = class { const version = shaka.offline.indexeddb.StorageMechanism.VERSION; const p = new shaka.util.PublicPromise(); + + // Add a timeout mechanism, for the (rare?) case where no callbacks are + // called at all, so that this method doesn't hang forever. + let timedOut = false; + const timeOutTimer = new shaka.util.Timer(() => { + timedOut = true; + p.reject(new shaka.util.Error( + shaka.util.Error.Severity.CRITICAL, + shaka.util.Error.Category.STORAGE, + shaka.util.Error.Code.INDEXED_DB_INIT_TIMED_OUT)); + }); + timeOutTimer.tickAfter(2); + const open = window.indexedDB.open(name, version); open.onsuccess = (event) => { + if (timedOut) { + // Too late, we have already given up on opening the storage mechanism. + return; + } const db = open.result; this.db_ = db; this.v1_ = shaka.offline.indexeddb.StorageMechanism.createV1_(db); @@ -68,6 +86,7 @@ shaka.offline.indexeddb.StorageMechanism = class { this.v5_ = shaka.offline.indexeddb.StorageMechanism.createV5_(db); this.sessions_ = shaka.offline.indexeddb.StorageMechanism.createEmeSessionCell_(db); + timeOutTimer.stop(); p.resolve(); }; open.onupgradeneeded = (event) => { @@ -75,11 +94,16 @@ shaka.offline.indexeddb.StorageMechanism = class { this.createStores_(open.result); }; open.onerror = (event) => { + if (timedOut) { + // Too late, we have already given up on opening the storage mechanism. + return; + } p.reject(new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.STORAGE, shaka.util.Error.Code.INDEXED_DB_ERROR, open.error)); + timeOutTimer.stop(); // Firefox will raise an error on the main thread unless we stop it here. event.preventDefault(); diff --git a/lib/util/error.js b/lib/util/error.js index 9cf5305cc7..19152361b5 100644 --- a/lib/util/error.js +++ b/lib/util/error.js @@ -1030,6 +1030,13 @@ shaka.util.Error.Code = { */ 'MODIFY_OPERATION_NOT_SUPPORTED': 9016, + /** + * When attempting to open an indexedDB instance, nothing happened for long + * enough for us to time out. This keeps the storage mechanism from hanging + * indefinitely, if neither the success nor error callbacks are called. + */ + 'INDEXED_DB_INIT_TIMED_OUT': 9017, + /** * CS IMA SDK, required for ad insertion, has not been included on the page. */ diff --git a/test/offline/storage_integration.js b/test/offline/storage_integration.js index 6b7ee341f8..a8009407f9 100644 --- a/test/offline/storage_integration.js +++ b/test/offline/storage_integration.js @@ -1034,6 +1034,36 @@ filterDescribe('Storage', storageSupport, () => { } }); + /** + * In some situations, indexedDB.open() can just hang, and call neither the + * 'success' nor the 'error' callbacks. + * I'm not sure what causes it, but it seems to happen consistently between + * reloads when it does so it might be a browser-based issue. + * In that case, we should time out with an error, instead of also hanging. + */ + it('throws an error if indexedDB open times out', async () => { + const oldOpen = window.indexedDB.open; + window.indexedDB.open = () => { + // Just return a dummy object. + return /** @type {!IDBOpenDBRequest} */ ({ + onsuccess: (event) => {}, + onerror: (error) => {}, + }); + }; + + /** @type {!shaka.offline.StorageMuxer} */ + const muxer = new shaka.offline.StorageMuxer(); + const expectedError = shaka.test.Util.jasmineError(new shaka.util.Error( + shaka.util.Error.Severity.CRITICAL, + shaka.util.Error.Category.STORAGE, + shaka.util.Error.Code.INDEXED_DB_INIT_TIMED_OUT)); + + await expectAsync(muxer.init()) + .toBeRejectedWith(expectedError); + + window.indexedDB.open = oldOpen; + }); + it('throws an error if the content is a live stream', async () => { const expected = Util.jasmineError(new shaka.util.Error( shaka.util.Error.Severity.CRITICAL,