Bug 1377533 - Remove scattered references to Kinto and Sqlite in blocklist clients r=mgoodwin,glasserc draft
authorMathieu Leplatre <mathieu@mozilla.com>
Fri, 30 Jun 2017 12:07:28 -0700
changeset 657498 cdc361bf3732bbadd4ed729b90679da2c7e005f8
parent 649871 c7c96eebbcb91e5e0c8ef0dbbb5324812fa1e476
child 729443 1684b94a4306d194fd570cb94439f98b7de7e267
push id77537
push usermleplatre@mozilla.com
push dateFri, 01 Sep 2017 15:25:56 +0000
reviewersmgoodwin, glasserc
bugs1377533
milestone57.0a1
Bug 1377533 - Remove scattered references to Kinto and Sqlite in blocklist clients r=mgoodwin,glasserc MozReview-Commit-ID: FExozSDHgNN
services/common/blocklist-clients.js
services/common/tests/unit/test_blocklist_certificates.js
services/common/tests/unit/test_blocklist_clients.js
services/common/tests/unit/test_blocklist_pinning.js
services/common/tests/unit/test_blocklist_signatures.js
toolkit/components/extensions/ExtensionStorageSync.jsm
--- a/services/common/blocklist-clients.js
+++ b/services/common/blocklist-clients.js
@@ -44,20 +44,20 @@ const PREF_BLOCKLIST_PINNING_BUCKET     
 const PREF_BLOCKLIST_PINNING_COLLECTION      = "services.blocklist.pinning.collection";
 const PREF_BLOCKLIST_PINNING_CHECKED_SECONDS = "services.blocklist.pinning.checked";
 const PREF_BLOCKLIST_GFX_COLLECTION          = "services.blocklist.gfx.collection";
 const PREF_BLOCKLIST_GFX_CHECKED_SECONDS     = "services.blocklist.gfx.checked";
 const PREF_BLOCKLIST_ENFORCE_SIGNING         = "services.blocklist.signing.enforced";
 
 const INVALID_SIGNATURE = "Invalid content/signature";
 
-// FIXME: this was the default path in earlier versions of
+// This was the default path in earlier versions of
 // FirefoxAdapter, so for backwards compatibility we maintain this
 // filename, even though it isn't descriptive of who is using it.
-this.KINTO_STORAGE_PATH    = "kinto.sqlite";
+const KINTO_STORAGE_PATH = "kinto.sqlite";
 
 
 
 function mergeChanges(collection, localRecords, changes) {
   const records = {};
   // Local records by id.
   localRecords.forEach((record) => records[record.id] = collection.cleanLocalFields(record));
   // All existing records are replaced by the version from the server.
@@ -110,16 +110,47 @@ class BlocklistClient {
 
   get filename() {
     // Replace slash by OS specific path separator (eg. Windows)
     const identifier = OS.Path.join(...this.identifier.split("/"));
     return `${identifier}.json`;
   }
 
   /**
+   * Open the underlying Kinto collection, using the appropriate adapter and
+   * options. This acts as a context manager where the connection is closed
+   * once the specified `callback` has finished.
+   *
+   * @param {callback} function           the async function to execute with the open SQlite connection.
+   * @param {Object}   options            additional advanced options.
+   * @param {string}   options.bucket     override bucket name of client (default: this.bucketName)
+   * @param {string}   options.collection override collection name of client (default: this.collectionName)
+   * @param {string}   options.path       override default Sqlite path (default: kinto.sqlite)
+   * @param {string}   options.hooks      hooks to execute on synchronization (see Kinto.js docs)
+   */
+  async openCollection(callback, options = {}) {
+    const { bucket = this.bucketName, path = KINTO_STORAGE_PATH } = options;
+    if (!this._kinto) {
+      this._kinto = new Kinto({bucket, adapter: FirefoxAdapter});
+    }
+    let sqliteHandle;
+    try {
+      sqliteHandle = await FirefoxAdapter.openConnection({path});
+      const colOptions = Object.assign({adapterOptions: {sqliteHandle}}, options);
+      const {collection: collectionName = this.collectionName} = options;
+      const collection = this._kinto.collection(collectionName, colOptions);
+      return await callback(collection);
+    } finally {
+      if (sqliteHandle) {
+        await sqliteHandle.close();
+      }
+    }
+  }
+
+  /**
    * Load the the JSON file distributed with the release for this blocklist.
    *
    * For Bug 1257565 this method will have to try to load the file from the profile,
    * in order to leverage the updateJSONBlocklist() below, which writes a new
    * dump each time the collection changes.
    */
   async loadDumpFile() {
     // Replace OS specific path separator by / for URI.
@@ -181,138 +212,123 @@ class BlocklistClient {
    * @return {Promise}              which rejects on sync or process failure.
    */
   async maybeSync(lastModified, serverTime, options = {loadDump: true}) {
     const {loadDump} = options;
     const remote = Services.prefs.getCharPref(PREF_SETTINGS_SERVER);
     const enforceCollectionSigning =
       Services.prefs.getBoolPref(PREF_BLOCKLIST_ENFORCE_SIGNING);
 
-    if (!this._kinto) {
-      this._kinto = new Kinto({
-        bucket: this.bucketName,
-        adapter: FirefoxAdapter,
-      });
-    }
-
     // if there is a signerName and collection signing is enforced, add a
     // hook for incoming changes that validates the signature
-    let hooks;
+    const colOptions = {};
     if (this.signerName && enforceCollectionSigning) {
-      hooks = {
+      colOptions.hooks = {
         "incoming-changes": [(payload, collection) => {
           return this.validateCollectionSignature(remote, payload, collection);
         }]
       }
     }
 
-    let sqliteHandle;
     let reportStatus = null;
     try {
-      // Synchronize remote data into a local Sqlite DB.
-      sqliteHandle = await FirefoxAdapter.openConnection({path: KINTO_STORAGE_PATH});
-      const options = {
-        hooks,
-        adapterOptions: {sqliteHandle},
-      };
-      const collection = this._kinto.collection(this.collectionName, options);
-
-      let collectionLastModified = await collection.db.getLastModified();
+      return await this.openCollection(async (collection) => {
+        // Synchronize remote data into a local Sqlite DB.
+        let collectionLastModified = await collection.db.getLastModified();
 
-      // If there is no data currently in the collection, attempt to import
-      // initial data from the application defaults.
-      // This allows to avoid synchronizing the whole collection content on
-      // cold start.
-      if (!collectionLastModified && loadDump) {
-        try {
-          const initialData = await this.loadDumpFile();
-          await collection.loadDump(initialData.data);
-          collectionLastModified = await collection.db.getLastModified();
-        } catch (e) {
-          // Report but go-on.
-          Cu.reportError(e);
+        // If there is no data currently in the collection, attempt to import
+        // initial data from the application defaults.
+        // This allows to avoid synchronizing the whole collection content on
+        // cold start.
+        if (!collectionLastModified && loadDump) {
+          try {
+            const initialData = await this.loadDumpFile();
+            await collection.loadDump(initialData.data);
+            collectionLastModified = await collection.db.getLastModified();
+          } catch (e) {
+            // Report but go-on.
+            Cu.reportError(e);
+          }
         }
-      }
+
+        // If the data is up to date, there's no need to sync. We still need
+        // to record the fact that a check happened.
+        if (lastModified <= collectionLastModified) {
+          this.updateLastCheck(serverTime);
+          reportStatus = UptakeTelemetry.STATUS.UP_TO_DATE;
+          return;
+        }
 
-      // If the data is up to date, there's no need to sync. We still need
-      // to record the fact that a check happened.
-      if (lastModified <= collectionLastModified) {
-        this.updateLastCheck(serverTime);
-        reportStatus = UptakeTelemetry.STATUS.UP_TO_DATE;
-        return;
-      }
-
-      // Fetch changes from server.
-      try {
-        // Server changes have priority during synchronization.
-        const strategy = Kinto.syncStrategy.SERVER_WINS;
-        const {ok} = await collection.sync({remote, strategy});
-        if (!ok) {
-          // Some synchronization conflicts occured.
-          reportStatus = UptakeTelemetry.STATUS.CONFLICT_ERROR;
-          throw new Error("Sync failed");
-        }
-      } catch (e) {
-        if (e.message == INVALID_SIGNATURE) {
-          // Signature verification failed during synchronzation.
-          reportStatus = UptakeTelemetry.STATUS.SIGNATURE_ERROR;
-          // if sync fails with a signature error, it's likely that our
-          // local data has been modified in some way.
-          // We will attempt to fix this by retrieving the whole
-          // remote collection.
-          const payload = await fetchRemoteCollection(remote, collection);
-          try {
-            await this.validateCollectionSignature(remote, payload, collection, {ignoreLocal: true});
-          } catch (e) {
-            reportStatus = UptakeTelemetry.STATUS.SIGNATURE_RETRY_ERROR;
+        // Fetch changes from server.
+        try {
+          // Server changes have priority during synchronization.
+          const strategy = Kinto.syncStrategy.SERVER_WINS;
+          const {ok} = await collection.sync({remote, strategy});
+          if (!ok) {
+            // Some synchronization conflicts occured.
+            reportStatus = UptakeTelemetry.STATUS.CONFLICT_ERROR;
+            throw new Error("Sync failed");
+          }
+        } catch (e) {
+          if (e.message == INVALID_SIGNATURE) {
+            // Signature verification failed during synchronzation.
+            reportStatus = UptakeTelemetry.STATUS.SIGNATURE_ERROR;
+            // if sync fails with a signature error, it's likely that our
+            // local data has been modified in some way.
+            // We will attempt to fix this by retrieving the whole
+            // remote collection.
+            const payload = await fetchRemoteCollection(remote, collection);
+            try {
+              await this.validateCollectionSignature(remote, payload, collection, {ignoreLocal: true});
+            } catch (e) {
+              reportStatus = UptakeTelemetry.STATUS.SIGNATURE_RETRY_ERROR;
+              throw e;
+            }
+            // if the signature is good (we haven't thrown), and the remote
+            // last_modified is newer than the local last_modified, replace the
+            // local data
+            const localLastModified = await collection.db.getLastModified();
+            if (payload.last_modified >= localLastModified) {
+              await collection.clear();
+              await collection.loadDump(payload.data);
+            }
+          } else {
+            // The sync has thrown, it can be a network or a general error.
+            if (/NetworkError/.test(e.message)) {
+              reportStatus = UptakeTelemetry.STATUS.NETWORK_ERROR;
+            } else if (/Backoff/.test(e.message)) {
+              reportStatus = UptakeTelemetry.STATUS.BACKOFF;
+            } else {
+              reportStatus = UptakeTelemetry.STATUS.SYNC_ERROR;
+            }
             throw e;
           }
-          // if the signature is good (we haven't thrown), and the remote
-          // last_modified is newer than the local last_modified, replace the
-          // local data
-          const localLastModified = await collection.db.getLastModified();
-          if (payload.last_modified >= localLastModified) {
-            await collection.clear();
-            await collection.loadDump(payload.data);
-          }
-        } else {
-          // The sync has thrown, it can be a network or a general error.
-          if (/NetworkError/.test(e.message)) {
-            reportStatus = UptakeTelemetry.STATUS.NETWORK_ERROR;
-          } else if (/Backoff/.test(e.message)) {
-            reportStatus = UptakeTelemetry.STATUS.BACKOFF;
-          } else {
-            reportStatus = UptakeTelemetry.STATUS.SYNC_ERROR;
-          }
+        }
+        // Read local collection of records.
+        const {data} = await collection.list();
+
+        // Handle the obtained records (ie. apply locally).
+        try {
+          await this.processCallback(data);
+        } catch (e) {
+          reportStatus = UptakeTelemetry.STATUS.APPLY_ERROR;
           throw e;
         }
-      }
-      // Read local collection of records.
-      const {data} = await collection.list();
 
-      // Handle the obtained records (ie. apply locally).
-      try {
-        await this.processCallback(data);
-      } catch (e) {
-        reportStatus = UptakeTelemetry.STATUS.APPLY_ERROR;
-        throw e;
-      }
+        // Track last update.
+        this.updateLastCheck(serverTime);
 
-      // Track last update.
-      this.updateLastCheck(serverTime);
+      }, colOptions);
     } catch (e) {
       // No specific error was tracked, mark it as unknown.
       if (reportStatus === null) {
         reportStatus = UptakeTelemetry.STATUS.UNKNOWN_ERROR;
       }
       throw e;
     } finally {
-      if (sqliteHandle) {
-        await sqliteHandle.close();
-      }
       // No error was reported, this is a success!
       if (reportStatus === null) {
         reportStatus = UptakeTelemetry.STATUS.SUCCESS;
       }
       // Report success/error status to Telemetry.
       UptakeTelemetry.report(this.identifier, reportStatus);
     }
   }
--- a/services/common/tests/unit/test_blocklist_certificates.js
+++ b/services/common/tests/unit/test_blocklist_certificates.js
@@ -1,38 +1,19 @@
 const { Constructor: CC } = Components;
 
 Cu.import("resource://testing-common/httpd.js");
 
 const { OneCRLBlocklistClient } = Cu.import("resource://services-common/blocklist-clients.js", {});
-const { Kinto } = Cu.import("resource://services-common/kinto-offline-client.js", {});
-const { FirefoxAdapter } = Cu.import("resource://services-common/kinto-storage-adapter.js", {});
 
 const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
   "nsIBinaryInputStream", "setInputStream");
 
 let server;
 
-// set up what we need to make storage adapters
-let sqliteHandle;
-const KINTO_FILENAME = "kinto.sqlite";
-
-function do_get_kinto_collection(collectionName) {
-  let config = {
-    // Set the remote to be some server that will cause test failure when
-    // hit since we should never hit the server directly, only via maybeSync()
-    remote: "https://firefox.settings.services.mozilla.com/v1/",
-    // Set up the adapter and bucket as normal
-    adapter: FirefoxAdapter,
-    adapterOptions: {sqliteHandle},
-    bucket: "blocklists"
-  };
-  return new Kinto(config).collection(collectionName);
-}
-
 // Some simple tests to demonstrate that the logic inside maybeSync works
 // correctly and that simple kinto operations are working as expected. There
 // are more tests for core Kinto.js (and its storage adapter) in the
 // xpcshell tests under /services/common
 add_task(async function test_something() {
   const configPath = "/v1/";
   const recordsPath = "/v1/buckets/blocklists/collections/certificates/records";
 
@@ -62,55 +43,62 @@ add_task(async function test_something()
     }
   }
   server.registerPathHandler(configPath, handleResponse);
   server.registerPathHandler(recordsPath, handleResponse);
 
   // Test an empty db populates
   await OneCRLBlocklistClient.maybeSync(2000, Date.now());
 
-  sqliteHandle = await FirefoxAdapter.openConnection({path: KINTO_FILENAME});
-  const collection = do_get_kinto_collection("certificates");
-
-  // Open the collection, verify it's been populated:
-  let list = await collection.list();
-  // We know there will be initial values from the JSON dump.
-  // (at least as many as in the dump shipped when this test was written).
-  do_check_true(list.data.length >= 363);
+  await OneCRLBlocklistClient.openCollection(async (collection) => {
+    // Open the collection, verify it's been populated:
+    const list = await collection.list();
+    // We know there will be initial values from the JSON dump.
+    // (at least as many as in the dump shipped when this test was written).
+    do_check_true(list.data.length >= 363);
+  });
 
   // No sync will be intented if maybeSync() is up-to-date.
   Services.prefs.clearUserPref("services.settings.server");
   Services.prefs.setIntPref("services.blocklist.onecrl.checked", 0);
   // Use any last_modified older than highest shipped in JSON dump.
   await OneCRLBlocklistClient.maybeSync(123456, Date.now());
   // Last check value was updated.
   do_check_neq(0, Services.prefs.getIntPref("services.blocklist.onecrl.checked"));
 
   // Restore server pref.
   Services.prefs.setCharPref("services.settings.server", dummyServerURL);
-  // clear the collection, save a non-zero lastModified so we don't do
-  // import of initial data when we sync again.
-  await collection.clear();
-  // a lastModified value of 1000 means we get a remote collection with a
-  // single record
-  await collection.db.saveLastModified(1000);
+
+  await OneCRLBlocklistClient.openCollection(async (collection) => {
+    // clear the collection, save a non-zero lastModified so we don't do
+    // import of initial data when we sync again.
+    await collection.clear();
+    // a lastModified value of 1000 means we get a remote collection with a
+    // single record
+    await collection.db.saveLastModified(1000);
+  });
+
   await OneCRLBlocklistClient.maybeSync(2000, Date.now());
 
-  // Open the collection, verify it's been updated:
-  // Our test data now has two records; both should be in the local collection
-  list = await collection.list();
-  do_check_eq(list.data.length, 1);
+  await OneCRLBlocklistClient.openCollection(async (collection) => {
+    // Open the collection, verify it's been updated:
+    // Our test data now has two records; both should be in the local collection
+    const list = await collection.list();
+    do_check_eq(list.data.length, 1);
+  });
 
   // Test the db is updated when we call again with a later lastModified value
   await OneCRLBlocklistClient.maybeSync(4000, Date.now());
 
-  // Open the collection, verify it's been updated:
-  // Our test data now has two records; both should be in the local collection
-  list = await collection.list();
-  do_check_eq(list.data.length, 3);
+  await OneCRLBlocklistClient.openCollection(async (collection) => {
+    // Open the collection, verify it's been updated:
+    // Our test data now has two records; both should be in the local collection
+    const list = await collection.list();
+    do_check_eq(list.data.length, 3);
+  });
 
   // Try to maybeSync with the current lastModified value - no connection
   // should be attempted.
   // Clear the kinto base pref so any connections will cause a test failure
   Services.prefs.clearUserPref("services.settings.server");
   await OneCRLBlocklistClient.maybeSync(4000, Date.now());
 
   // Try again with a lastModified value at some point in the past
@@ -138,17 +126,16 @@ function run_test() {
   // Set up an HTTP Server
   server = new HttpServer();
   server.start(-1);
 
   run_next_test();
 
   do_register_cleanup(function() {
     server.stop(() => { });
-    return sqliteHandle.close();
   });
 }
 
 // get a response for a given request from sample data
 function getSampleResponse(req, port) {
   const responses = {
     "OPTIONS": {
       "sampleHeaders": [
--- a/services/common/tests/unit/test_blocklist_clients.js
+++ b/services/common/tests/unit/test_blocklist_clients.js
@@ -1,66 +1,46 @@
 const { Constructor: CC } = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://testing-common/httpd.js");
 Cu.import("resource://gre/modules/Timer.jsm");
 const { FileUtils } = Cu.import("resource://gre/modules/FileUtils.jsm", {});
 const { OS } = Cu.import("resource://gre/modules/osfile.jsm", {});
 
-const { Kinto } = Cu.import("resource://services-common/kinto-offline-client.js", {});
-const { FirefoxAdapter } = Cu.import("resource://services-common/kinto-storage-adapter.js", {});
 const BlocklistClients = Cu.import("resource://services-common/blocklist-clients.js", {});
 const { UptakeTelemetry } = Cu.import("resource://services-common/uptake-telemetry.js", {});
 
 const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
   "nsIBinaryInputStream", "setInputStream");
-const kintoFilename = "kinto.sqlite";
 
 const gBlocklistClients = [
   {client: BlocklistClients.AddonBlocklistClient, testData: ["i808", "i720", "i539"]},
   {client: BlocklistClients.PluginBlocklistClient, testData: ["p1044", "p32", "p28"]},
   {client: BlocklistClients.GfxBlocklistClient, testData: ["g204", "g200", "g36"]},
 ];
 
 
 let server;
 
-function kintoCollection(collectionName, sqliteHandle) {
-  const config = {
-    // Set the remote to be some server that will cause test failure when
-    // hit since we should never hit the server directly, only via maybeSync()
-    remote: "https://firefox.settings.services.mozilla.com/v1/",
-    adapter: FirefoxAdapter,
-    adapterOptions: {sqliteHandle},
-    bucket: "blocklists"
-  };
-  return new Kinto(config).collection(collectionName);
-}
-
 async function readJSON(filepath) {
   const binaryData = await OS.File.read(filepath);
   const textData = (new TextDecoder()).decode(binaryData);
   return Promise.resolve(JSON.parse(textData));
 }
 
 async function clear_state() {
   for (let {client} of gBlocklistClients) {
     // Remove last server times.
     Services.prefs.clearUserPref(client.lastCheckTimePref);
 
     // Clear local DB.
-    let sqliteHandle;
-    try {
-      sqliteHandle = await FirefoxAdapter.openConnection({path: kintoFilename});
-      const collection = kintoCollection(client.collectionName, sqliteHandle);
+    await client.openCollection(async (collection) => {
       await collection.clear();
-    } finally {
-      await sqliteHandle.close();
-    }
+    });
 
     // Remove JSON dumps folders in profile dir.
     const dumpFile = OS.Path.join(OS.Constants.Path.profileDir, client.filename);
     const folder = OS.Path.dirname(dumpFile);
     await OS.File.removeDir(folder, { ignoreAbsent: true });
   }
 }
 
@@ -115,53 +95,49 @@ function run_test() {
 }
 
 add_task(async function test_initial_dump_is_loaded_as_synced_when_collection_is_empty() {
   for (let {client} of gBlocklistClients) {
     // Test an empty db populates, but don't reach server (specified timestamp <= dump).
     await client.maybeSync(1, Date.now());
 
     // Open the collection, verify the loaded data has status to synced:
-    const sqliteHandle = await FirefoxAdapter.openConnection({path: kintoFilename});
-    const collection = kintoCollection(client.collectionName, sqliteHandle);
-    const list = await collection.list();
-    equal(list.data[0]._status, "synced");
-    await sqliteHandle.close();
+    await client.openCollection(async (collection) => {
+      const list = await collection.list();
+      equal(list.data[0]._status, "synced");
+    });
   }
 });
 add_task(clear_state);
 
 add_task(async function test_records_obtained_from_server_are_stored_in_db() {
   for (let {client} of gBlocklistClients) {
     // Test an empty db populates
     await client.maybeSync(2000, Date.now(), {loadDump: false});
 
     // Open the collection, verify it's been populated:
     // Our test data has a single record; it should be in the local collection
-    const sqliteHandle = await FirefoxAdapter.openConnection({path: kintoFilename});
-    let collection = kintoCollection(client.collectionName, sqliteHandle);
-    let list = await collection.list();
-    equal(list.data.length, 1);
-    await sqliteHandle.close();
+    await client.openCollection(async (collection) => {
+      const list = await collection.list();
+      equal(list.data.length, 1);
+    });
   }
 });
 add_task(clear_state);
 
 add_task(async function test_records_changes_are_overwritten_by_server_changes() {
   const {client} = gBlocklistClients[0];
 
   // Create some local conflicting data, and make sure it syncs without error.
-  const sqliteHandle = await FirefoxAdapter.openConnection({path: kintoFilename});
-  const collection = kintoCollection(client.collectionName, sqliteHandle);
-  await collection.create({
-    "versionRange": [],
-    "id": "9d500963-d80e-3a91-6e74-66f3811b99cc"
-  }, { useRecordId: true });
-  await sqliteHandle.close();
-
+  await client.openCollection(async (collection) => {
+    await collection.create({
+      "versionRange": [],
+      "id": "9d500963-d80e-3a91-6e74-66f3811b99cc"
+    }, { useRecordId: true });
+  });
   await client.maybeSync(2000, Date.now(), {loadDump: false});
 });
 add_task(clear_state);
 
 add_task(async function test_list_is_written_to_file_in_profile() {
   for (let {client, testData} of gBlocklistClients) {
     const filePath = OS.Path.join(OS.Constants.Path.profileDir, client.filename);
     const profFile = new FileUtils.File(filePath);
@@ -279,45 +255,44 @@ add_task(async function test_telemetry_r
   checkUptakeTelemetry(startHistogram, endHistogram, expectedIncrements);
 });
 add_task(clear_state);
 
 add_task(async function test_telemetry_reports_if_sync_fails() {
   const {client} = gBlocklistClients[0];
   const serverTime = Date.now();
 
-  const sqliteHandle = await FirefoxAdapter.openConnection({path: kintoFilename});
-  const collection = kintoCollection(client.collectionName, sqliteHandle);
-  await collection.db.saveLastModified(9999);
-  await sqliteHandle.close();
+  await client.openCollection(async (collection) => {
+    await collection.db.saveLastModified(9999);
+  });
 
   const startHistogram = getUptakeTelemetrySnapshot(client.identifier);
 
   try {
     await client.maybeSync(10000, serverTime);
   } catch (e) {}
 
   const endHistogram = getUptakeTelemetrySnapshot(client.identifier);
   const expectedIncrements = {[UptakeTelemetry.STATUS.SYNC_ERROR]: 1};
   checkUptakeTelemetry(startHistogram, endHistogram, expectedIncrements);
 });
 add_task(clear_state);
 
 add_task(async function test_telemetry_reports_unknown_errors() {
   const {client} = gBlocklistClients[0];
   const serverTime = Date.now();
-  const backup = FirefoxAdapter.openConnection;
-  FirefoxAdapter.openConnection = () => { throw new Error("Internal"); };
+  const backup = client.openCollection;
+  client.openCollection = () => { throw new Error("Internal"); };
   const startHistogram = getUptakeTelemetrySnapshot(client.identifier);
 
   try {
     await client.maybeSync(2000, serverTime);
   } catch (e) {}
 
-  FirefoxAdapter.openConnection = backup;
+  client.openCollection = backup;
   const endHistogram = getUptakeTelemetrySnapshot(client.identifier);
   const expectedIncrements = {[UptakeTelemetry.STATUS.UNKNOWN_ERROR]: 1};
   checkUptakeTelemetry(startHistogram, endHistogram, expectedIncrements);
 });
 add_task(clear_state);
 
 // get a response for a given request from sample data
 function getSampleResponse(req, port) {
--- a/services/common/tests/unit/test_blocklist_pinning.js
+++ b/services/common/tests/unit/test_blocklist_pinning.js
@@ -1,24 +1,17 @@
 "use strict"
 
 const { Constructor: CC } = Components;
 
 Cu.import("resource://testing-common/httpd.js");
 
-const { Kinto } = Cu.import("resource://services-common/kinto-offline-client.js", {});
-const { FirefoxAdapter } = Cu.import("resource://services-common/kinto-storage-adapter.js", {});
-
 const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
   "nsIBinaryInputStream", "setInputStream");
 
-const PREF_BLOCKLIST_PINNING_COLLECTION = "services.blocklist.pinning.collection";
-const COLLECTION_NAME = "pins";
-const KINTO_STORAGE_PATH    = "kinto.sqlite";
-
 // First, we need to setup appInfo or we can't do version checks
 var id = "xpcshell@tests.mozilla.org";
 var appName = "XPCShell";
 var version = "1";
 var platformVersion = "1.9.2";
 Cu.import("resource://testing-common/AppInfo.jsm", this);
 
 updateAppInfo({
@@ -26,40 +19,19 @@ updateAppInfo({
   ID: id,
   version,
   platformVersion: platformVersion ? platformVersion : "1.0",
   crashReporter: true,
 });
 
 let server;
 
-
-function do_get_kinto_collection(connection, collectionName) {
-  let config = {
-    // Set the remote to be some server that will cause test failure when
-    // hit since we should never hit the server directly (any non-local
-    // request causes failure in tests), only via maybeSync()
-    remote: "https://firefox.settings.services.mozilla.com/v1/",
-    // Set up the adapter and bucket as normal
-    adapter: FirefoxAdapter,
-    adapterOptions: {sqliteHandle: connection},
-    bucket: "pinning"
-  };
-  let kintoClient = new Kinto(config);
-  return kintoClient.collection(collectionName);
-}
-
 // Some simple tests to demonstrate that the core preload sync operations work
 // correctly and that simple kinto operations are working as expected.
 add_task(async function test_something() {
-  // set the collection name explicitly - since there will be version
-  // specific collection names in prefs
-  Services.prefs.setCharPref(PREF_BLOCKLIST_PINNING_COLLECTION,
-                             COLLECTION_NAME);
-
   const { PinningPreloadClient } = Cu.import("resource://services-common/blocklist-clients.js", {});
 
   const configPath = "/v1/";
   const recordsPath = "/v1/buckets/pinning/collections/pins/records";
 
   Services.prefs.setCharPref("services.settings.server",
                              `http://localhost:${server.identity.primaryPort}/v1`);
 
@@ -101,37 +73,36 @@ add_task(async function test_something()
   ok(!sss.isSecureURI(sss.HEADER_HSTS,
                       Services.io.newURI("https://four.example.com"), 0));
   ok(!sss.isSecureURI(sss.HEADER_HSTS,
                       Services.io.newURI("https://five.example.com"), 0));
 
   // Test an empty db populates
   await PinningPreloadClient.maybeSync(2000, Date.now());
 
-  let connection = await FirefoxAdapter.openConnection({path: KINTO_STORAGE_PATH});
-
   // Open the collection, verify it's been populated:
   // Our test data has a single record; it should be in the local collection
-  let collection = do_get_kinto_collection(connection, COLLECTION_NAME);
-  let list = await collection.list();
-  do_check_eq(list.data.length, 1);
+  await PinningPreloadClient.openCollection(async (collection) => {
+    const list = await collection.list();
+    do_check_eq(list.data.length, 1);
+  });
 
   // check that a pin exists for one.example.com
   ok(sss.isSecureURI(sss.HEADER_HPKP,
                      Services.io.newURI("https://one.example.com"), 0));
 
   // Test the db is updated when we call again with a later lastModified value
   await PinningPreloadClient.maybeSync(4000, Date.now());
 
   // Open the collection, verify it's been updated:
   // Our data now has four new records; all should be in the local collection
-  collection = do_get_kinto_collection(connection, COLLECTION_NAME);
-  list = await collection.list();
-  do_check_eq(list.data.length, 5);
-  await connection.close();
+  await PinningPreloadClient.openCollection(async (collection) => {
+    const list = await collection.list();
+    do_check_eq(list.data.length, 5);
+  });
 
   // check that a pin exists for two.example.com and three.example.com
   ok(sss.isSecureURI(sss.HEADER_HPKP,
                      Services.io.newURI("https://two.example.com"), 0));
   ok(sss.isSecureURI(sss.HEADER_HPKP,
                      Services.io.newURI("https://three.example.com"), 0));
 
   // check that a pin does not exist for four.example.com - it's in the
--- a/services/common/tests/unit/test_blocklist_signatures.js
+++ b/services/common/tests/unit/test_blocklist_signatures.js
@@ -1,32 +1,26 @@
 "use strict";
 
 Cu.import("resource://services-common/blocklist-updater.js");
 Cu.import("resource://testing-common/httpd.js");
 
-const { Kinto } = Cu.import("resource://services-common/kinto-offline-client.js", {});
-const { FirefoxAdapter } = Cu.import("resource://services-common/kinto-storage-adapter.js", {});
 const { NetUtil } = Cu.import("resource://gre/modules/NetUtil.jsm", {});
 const { OneCRLBlocklistClient } = Cu.import("resource://services-common/blocklist-clients.js", {});
 const { UptakeTelemetry } = Cu.import("resource://services-common/uptake-telemetry.js", {});
 
 let server;
 
-const PREF_BLOCKLIST_BUCKET            = "services.blocklist.bucket";
 const PREF_BLOCKLIST_ENFORCE_SIGNING   = "services.blocklist.signing.enforced";
-const PREF_BLOCKLIST_ONECRL_COLLECTION = "services.blocklist.onecrl.collection";
 const PREF_SETTINGS_SERVER             = "services.settings.server";
 const PREF_SIGNATURE_ROOT              = "security.content.signature.root_hash";
 
 // Telemetry reports.
 const TELEMETRY_HISTOGRAM_KEY = OneCRLBlocklistClient.identifier;
 
-const kintoFilename = "kinto.sqlite";
-
 const CERT_DIR = "test_blocklist_signatures/";
 const CHAIN_FILES =
     ["collection_signing_ee.pem",
      "collection_signing_int.pem",
      "collection_signing_root.pem"];
 
 function getFileData(file) {
   const stream = Cc["@mozilla.org/network/file-input-stream;1"]
@@ -55,39 +49,21 @@ function getCertChain() {
   const chain = [];
   for (let file of CHAIN_FILES) {
     chain.push(getFileData(do_get_file(CERT_DIR + file)));
   }
   return chain.join("\n");
 }
 
 async function checkRecordCount(count) {
-  // open the collection manually
-  const base = Services.prefs.getCharPref(PREF_SETTINGS_SERVER);
-  const bucket = Services.prefs.getCharPref(PREF_BLOCKLIST_BUCKET);
-  const collectionName =
-      Services.prefs.getCharPref(PREF_BLOCKLIST_ONECRL_COLLECTION);
-
-  const sqliteHandle = await FirefoxAdapter.openConnection({path: kintoFilename});
-  const config = {
-    remote: base,
-    bucket,
-    adapter: FirefoxAdapter,
-    adapterOptions: {sqliteHandle},
-  };
-
-  const db = new Kinto(config);
-  const collection = db.collection(collectionName);
-
-  // Check we have the expected number of records
-  let records = await collection.list();
-  do_check_eq(count, records.data.length);
-
-  // Close the collection so the test can exit cleanly
-  await sqliteHandle.close();
+  await OneCRLBlocklistClient.openCollection(async (collection) => {
+    // Check we have the expected number of records
+    const records = await collection.list();
+    do_check_eq(count, records.data.length);
+  });
 }
 
 // Check to ensure maybeSync is called with correct values when a changes
 // document contains information on when a collection was last modified
 add_task(async function test_check_signatures() {
   const port = server.identity.primaryPort;
 
   // a response to give the client when the cert chain is expected
--- a/toolkit/components/extensions/ExtensionStorageSync.jsm
+++ b/toolkit/components/extensions/ExtensionStorageSync.jsm
@@ -52,17 +52,16 @@ XPCOMUtils.defineLazyModuleGetters(this,
   CollectionKeyManager: "resource://services-sync/record.js",
   CommonUtils: "resource://services-common/utils.js",
   CryptoUtils: "resource://services-crypto/utils.js",
   fxAccounts: "resource://gre/modules/FxAccounts.jsm",
   KintoHttpClient: "resource://services-common/kinto-http-client.js",
   Kinto: "resource://services-common/kinto-offline-client.js",
   FirefoxAdapter: "resource://services-common/kinto-storage-adapter.js",
   Observers: "resource://services-common/observers.js",
-  Sqlite: "resource://gre/modules/Sqlite.jsm",
   Utils: "resource://services-sync/util.js",
 });
 
 XPCOMUtils.defineLazyPreferenceGetter(this, "prefPermitsStorageSync",
                                       STORAGE_SYNC_ENABLED_PREF, true);
 XPCOMUtils.defineLazyPreferenceGetter(this, "prefStorageSyncServerURL",
                                       STORAGE_SYNC_SERVER_URL_PREF,
                                       KINTO_DEFAULT_SERVER_URL);
@@ -329,40 +328,35 @@ global.KeyRingEncryptionRemoteTransforme
  * This centralizes the use of the Sqlite database, to which there is
  * only one connection which is shared by all threads.
  *
  * Fields in the object returned by this Promise:
  *
  * - connection: a Sqlite connection. Meant for internal use only.
  * - kinto: a KintoBase object, suitable for using in Firefox. All
  *   collections in this database will use the same Sqlite connection.
+ * @returns {Promise<Object>}
  */
-const storageSyncInit = (async function() {
-  const path = "storage-sync.sqlite";
-  const opts = {path, sharedMemoryCache: false};
-  const connection = await Sqlite.openConnection(opts);
-  await FirefoxAdapter._init(connection);
-  return {
-    connection,
-    kinto: new Kinto({
-      adapter: FirefoxAdapter,
-      adapterOptions: {sqliteHandle: connection},
-      timeout: KINTO_REQUEST_TIMEOUT,
-    }),
-  };
-})();
+async function storageSyncInit() {
+  // Memoize the result to share the connection.
+  if (storageSyncInit.result === undefined) {
+    const path = "storage-sync.sqlite";
+    const connection = await FirefoxAdapter.openConnection({path});
+    storageSyncInit.result = {
+      connection,
+      kinto: new Kinto({
+        adapter: FirefoxAdapter,
+        adapterOptions: {sqliteHandle: connection},
+        timeout: KINTO_REQUEST_TIMEOUT,
+      }),
+    };
+  }
+  return storageSyncInit.result;
+}
 
-AsyncShutdown.profileBeforeChange.addBlocker(
-  "ExtensionStorageSync: close Sqlite handle",
-  async function() {
-    const ret = await storageSyncInit;
-    const {connection} = ret;
-    await connection.close();
-  }
-);
 // Kinto record IDs have two condtions:
 //
 // - They must contain only ASCII alphanumerics plus - and _. To fix
 // this, we encode all non-letters using _C_, where C is the
 // percent-encoded character, so space becomes _20_
 // and underscore becomes _5F_.
 //
 // - They must start with an ASCII letter. To ensure this, we prefix
@@ -427,17 +421,17 @@ const cryptoCollectionIdSchema = {
  */
 class CryptoCollection {
   constructor(fxaService) {
     this._fxaService = fxaService;
   }
 
   async getCollection() {
     throwIfNoFxA(this._fxaService, "tried to access cryptoCollection");
-    const {kinto} = await storageSyncInit;
+    const {kinto} = await storageSyncInit();
     return kinto.collection(STORAGE_SYNC_CRYPTO_COLLECTION_NAME, {
       idSchema: cryptoCollectionIdSchema,
       remoteTransformers: [new KeyRingEncryptionRemoteTransformer(this._fxaService)],
     });
   }
 
   /**
    * Generate a new salt for use in hashing extension and record
@@ -690,17 +684,17 @@ function cleanUpForContext(extension, co
  * @param {Context} context
  *                  The context for this extension. The Collection
  *                  will shut down automatically when all contexts
  *                  close.
  * @returns {Promise<Collection>}
  */
 const openCollection = async function(cryptoCollection, extension, context) {
   let collectionId = extension.id;
-  const {kinto} = await storageSyncInit;
+  const {kinto} = await storageSyncInit();
   const remoteTransformers = [new CollectionKeyEncryptionRemoteTransformer(cryptoCollection, extension.id)];
   const coll = kinto.collection(collectionId, {
     idSchema: storageSyncIdSchema,
     remoteTransformers,
   });
   return coll;
 };
 
@@ -1225,17 +1219,27 @@ class ExtensionStorageSync {
   }
 
   addOnChangedListener(extension, listener, context) {
     let listeners = this.listeners.get(extension) || new Set();
     listeners.add(listener);
     this.listeners.set(extension, listeners);
 
     // Force opening the collection so that we will sync for this extension.
-    return this.getCollection(extension, context);
+    // This happens asynchronously, even though the surface API is synchronous.
+    return this.getCollection(extension, context)
+      .catch((e) => {
+        // We can ignore failures that happen during shutdown here. First, we
+        // can't report in any way. And second, a failure to open the collection
+        // does not matter, because there won't be any message to listen to.
+        // See Bug 1395215.
+        if (!(/Kinto storage adapter connection closing/.test(e.message))) {
+          throw e;
+        }
+      });
   }
 
   removeOnChangedListener(extension, listener) {
     let listeners = this.listeners.get(extension);
     listeners.delete(listener);
     if (listeners.size == 0) {
       this.listeners.delete(extension);
     }