Bug 1377533 - Remove scattered references to Kinto and Sqlite in blocklist clients r=glasserc,mgoodwin
☠☠ backed out by 19b12e888f71 ☠ ☠
authorMathieu Leplatre <mathieu@mozilla.com>
Fri, 30 Jun 2017 12:07:28 -0700
changeset 377989 e6189da47385a9a7286259d013103634432722ba
parent 377988 15b957e02bbf59a2eaf5b4fcf17dff260bef819d
child 377990 d2ad47692c8763a1365a9eda357730052df6fc40
push id50135
push userryanvm@gmail.com
push dateThu, 31 Aug 2017 15:36:30 +0000
treeherderautoland@e6189da47385 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersglasserc, mgoodwin
bugs1377533
milestone57.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1377533 - Remove scattered references to Kinto and Sqlite in blocklist clients r=glasserc,mgoodwin 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);
@@ -332,37 +331,27 @@ global.KeyRingEncryptionRemoteTransforme
  * 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.
  */
 const storageSyncInit = (async function() {
   const path = "storage-sync.sqlite";
-  const opts = {path, sharedMemoryCache: false};
-  const connection = await Sqlite.openConnection(opts);
-  await FirefoxAdapter._init(connection);
+  const connection = await FirefoxAdapter.openConnection({path});
   return {
     connection,
     kinto: new Kinto({
       adapter: FirefoxAdapter,
       adapterOptions: {sqliteHandle: connection},
       timeout: KINTO_REQUEST_TIMEOUT,
     }),
   };
 })();
 
-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
@@ -1225,17 +1214,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);
     }