Bug 1319884 - Address FirefoxAdapter feedback from kinto.js#589, r?mgoodwin draft
authorEthan Glasser-Camp <eglassercamp@mozilla.com>
Wed, 23 Nov 2016 14:18:53 -0500
changeset 443048 48b3d2ae829abcd2238597fad6f29d0adcf582e0
parent 442499 1a3194836cb4c3da6ba3a9742a2d25cf26669b55
child 443049 aa7ada5f7daa469b21eb545e0b55b61a400cb8b0
push id36891
push usereglassercamp@mozilla.com
push dateWed, 23 Nov 2016 19:24:32 +0000
reviewersmgoodwin
bugs1319884
milestone53.0a1
Bug 1319884 - Address FirefoxAdapter feedback from kinto.js#589, r?mgoodwin Change FirefoxAdapter definitively to require an externally-managed Sqlite connection in order to function. This connection must be produced by calling an openConnection() static method, which does the work of initializing the tables and schema. Passing any other connection is wrong, but won't be detected at runtime, and might even work depending on the previous state of the database. Future work might define a new KintoSqliteConnection type that can only be produced by this method, so that it's impossible to create an uninitialized Kinto database. This change, since it moves Sqlite connections out of the FirefoxAdapter, also means that the path option is no longer handled or provided with a default. This means that the previous default, "kinto.sqlite", is now preserved in a bunch of places all over the codebase. This is unfortunate, but a migration is outside the scope of this patch. MozReview-Commit-ID: BKJqPR3jOTq
services/common/blocklist-clients.js
services/common/kinto-storage-adapter.js
services/common/tests/unit/test_blocklist_certificates.js
services/common/tests/unit/test_blocklist_clients.js
services/common/tests/unit/test_blocklist_signatures.js
services/common/tests/unit/test_kinto.js
services/common/tests/unit/test_storage_adapter.js
--- a/services/common/blocklist-clients.js
+++ b/services/common/blocklist-clients.js
@@ -33,16 +33,21 @@ const PREF_BLOCKLIST_ADDONS_CHECKED_SECO
 const PREF_BLOCKLIST_PLUGINS_COLLECTION      = "services.blocklist.plugins.collection";
 const PREF_BLOCKLIST_PLUGINS_CHECKED_SECONDS = "services.blocklist.plugins.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
+// 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";
+
 this.FILENAME_ADDONS_JSON  = "blocklist-addons.json";
 this.FILENAME_GFX_JSON     = "blocklist-gfx.json";
 this.FILENAME_PLUGINS_JSON = "blocklist-plugins.json";
 
 function mergeChanges(collection, localRecords, changes) {
   const records = {};
   // Local records by id.
   localRecords.forEach((record) => records[record.id] = collection.cleanLocalFields(record));
@@ -72,24 +77,25 @@ function fetchRemoteCollection(collectio
            .listRecords({sort: "id"});
 }
 
 /**
  * Helper to instantiate a Kinto client based on preferences for remote server
  * URL and bucket name. It uses the `FirefoxAdapter` which relies on SQLite to
  * persist the local DB.
  */
-function kintoClient() {
+function kintoClient(connection) {
   let base = Services.prefs.getCharPref(PREF_SETTINGS_SERVER);
   let bucket = Services.prefs.getCharPref(PREF_BLOCKLIST_BUCKET);
 
   let config = {
     remote: base,
     bucket: bucket,
     adapter: FirefoxAdapter,
+    adapterOptions: {sqliteHandle: connection},
   };
 
   return new Kinto(config);
 }
 
 
 class BlocklistClient {
 
@@ -140,34 +146,35 @@ class BlocklistClient {
    * Synchronize from Kinto server, if necessary.
    *
    * @param {int}  lastModified the lastModified date (on the server) for
                                 the remote collection.
    * @param {Date} serverTime   the current date return by the server.
    * @return {Promise}          which rejects on sync or process failure.
    */
   maybeSync(lastModified, serverTime) {
-    let db = kintoClient();
     let opts = {};
     let enforceCollectionSigning =
       Services.prefs.getBoolPref(PREF_BLOCKLIST_ENFORCE_SIGNING);
 
     // if there is a signerName and collection signing is enforced, add a
     // hook for incoming changes that validates the signature
     if (this.signerName && enforceCollectionSigning) {
       opts.hooks = {
         "incoming-changes": [this.validateCollectionSignature.bind(this)]
       }
     }
 
-    let collection = db.collection(this.collectionName, opts);
 
     return Task.spawn((function* syncCollection() {
+      let connection;
       try {
-        yield collection.db.open();
+        connection = yield FirefoxAdapter.openConnection({path: KINTO_STORAGE_PATH});
+        let db = kintoClient(connection);
+        let collection = db.collection(this.collectionName, opts);
 
         let collectionLastModified = yield collection.db.getLastModified();
         // 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);
           return;
         }
@@ -200,17 +207,17 @@ class BlocklistClient {
         // Read local collection of records.
         let list = yield collection.list();
 
         yield this.processCallback(list.data);
 
         // Track last update.
         this.updateLastCheck(serverTime);
       } finally {
-        collection.db.close();
+        yield connection.close();
       }
     }).bind(this));
   }
 
   /**
    * Save last time server was checked in users prefs.
    *
    * @param {Date} serverTime   the current date return by server.
--- a/services/common/kinto-storage-adapter.js
+++ b/services/common/kinto-storage-adapter.js
@@ -184,30 +184,34 @@ const createStatements = [
 const currentSchemaVersion = 1;
 
 /**
  * Firefox adapter.
  *
  * Uses Sqlite as a backing store.
  *
  * Options:
- *  - path: the filename/path for the Sqlite database. If absent, use SQLITE_PATH.
+ *  - sqliteHandle: a handle to the Sqlite database this adapter will
+ *    use as its backing store. To open such a handle, use the
+ *    static openConnection() method.
  */
 class FirefoxAdapter extends Kinto.adapters.BaseAdapter {
   constructor(collection, options = {}) {
     super();
     const { sqliteHandle = null } = options;
     this.collection = collection;
     this._connection = sqliteHandle;
     this._options = options;
   }
 
-  // We need to be capable of calling this from "outside" the adapter
-  // so that someone can initialize a connection and pass it to us in
-  // adapterOptions.
+  /**
+   * Initialize a Sqlite connection to be suitable for use with Kinto.
+   *
+   * This will be called automatically by open().
+   */
   static _init(connection) {
     return Task.spawn(function* () {
       yield connection.executeTransaction(function* doSetup() {
         const schema = yield connection.getSchemaVersion();
 
         if (schema == 0) {
 
           for (let statementName of createStatements) {
@@ -219,52 +223,40 @@ class FirefoxAdapter extends Kinto.adapt
           throw new Error("Unknown database schema: " + schema);
         }
       });
       return connection;
     });
   }
 
   _executeStatement(statement, params) {
-    if (!this._connection) {
-      throw new Error("The storage adapter is not open");
-    }
     return this._connection.executeCached(statement, params);
   }
 
-  open() {
-    const self = this;
-    return Task.spawn(function* () {
-      if (!self._connection) {
-        const path = self._options.path || SQLITE_PATH;
-        const opts = { path, sharedMemoryCache: false };
-        self._connection = yield Sqlite.openConnection(opts).then(FirefoxAdapter._init);
-      }
-    });
-  }
-
-  close() {
-    if (this._connection) {
-      const promise = this._connection.close();
-      this._connection = null;
-      return promise;
-    }
-    return Promise.resolve();
+  /**
+   * Open and initialize a Sqlite connection to a database that Kinto
+   * can use. When you are done with this connection, close it by
+   * calling close().
+   *
+   * Options:
+   *   - path: The path for the Sqlite database
+   *
+   * @returns SqliteConnection
+   */
+  static async openConnection(options) {
+    const opts = Object.assign({}, { sharedMemoryCache: false }, options);
+    return await Sqlite.openConnection(opts).then(this._init);
   }
 
   clear() {
     const params = { collection_name: this.collection };
     return this._executeStatement(statements.clearData, params);
   }
 
   execute(callback, options = { preload: [] }) {
-    if (!this._connection) {
-      throw new Error("The storage adapter is not open");
-    }
-
     let result;
     const conn = this._connection;
     const collection = this.collection;
 
     return conn.executeTransaction(function* doExecuteTransaction() {
       // Preload specified records from DB, within transaction.
       const parameters = [
         collection,
--- a/services/common/tests/unit/test_blocklist_certificates.js
+++ b/services/common/tests/unit/test_blocklist_certificates.js
@@ -9,31 +9,27 @@ const { FirefoxAdapter } = Cu.import("re
 const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
   "nsIBinaryInputStream", "setInputStream");
 
 let server;
 
 // set up what we need to make storage adapters
 const kintoFilename = "kinto.sqlite";
 
-let kintoClient;
-
-function do_get_kinto_collection(collectionName) {
-  if (!kintoClient) {
-    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,
-      bucket: "blocklists"
-    };
-    kintoClient = new Kinto(config);
-  }
-  return kintoClient.collection(collectionName);
+function do_get_kinto_collection(collectionName, sqliteHandle) {
+  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(function* test_something(){
   const configPath = "/v1/";
@@ -67,32 +63,32 @@ add_task(function* test_something(){
   server.registerPathHandler(configPath, handleResponse);
   server.registerPathHandler(recordsPath, handleResponse);
 
   // Test an empty db populates
   let result = yield OneCRLBlocklistClient.maybeSync(2000, Date.now());
 
   // 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("certificates");
-  yield collection.db.open();
+  let sqliteHandle = yield FirefoxAdapter.openConnection({path: kintoFilename});
+  let collection = do_get_kinto_collection("certificates", sqliteHandle);
   let list = yield collection.list();
   do_check_eq(list.data.length, 1);
-  yield collection.db.close();
+  yield sqliteHandle.close();
 
   // Test the db is updated when we call again with a later lastModified value
   result = yield 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
-  collection = do_get_kinto_collection("certificates");
-  yield collection.db.open();
+  sqliteHandle = yield FirefoxAdapter.openConnection({path: kintoFilename});
+  collection = do_get_kinto_collection("certificates", sqliteHandle);
   list = yield collection.list();
   do_check_eq(list.data.length, 3);
-  yield collection.db.close();
+  yield sqliteHandle.close();
 
   // 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");
   yield OneCRLBlocklistClient.maybeSync(4000, Date.now());
 
   // Try again with a lastModified value at some point in the past
--- a/services/common/tests/unit/test_blocklist_clients.js
+++ b/services/common/tests/unit/test_blocklist_clients.js
@@ -9,59 +9,58 @@ const { FileUtils } = Cu.import("resourc
 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 BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
   "nsIBinaryInputStream", "setInputStream");
+const kintoFilename = "kinto.sqlite";
 
 const gBlocklistClients = [
   {client: BlocklistClients.AddonBlocklistClient, filename: BlocklistClients.FILENAME_ADDONS_JSON, testData: ["i808","i720", "i539"]},
   {client: BlocklistClients.PluginBlocklistClient, filename: BlocklistClients.FILENAME_PLUGINS_JSON, testData: ["p1044","p32","p28"]},
   {client: BlocklistClients.GfxBlocklistClient, filename: BlocklistClients.FILENAME_GFX_JSON, testData: ["g204","g200","g36"]},
 ];
 
 
 let server;
-let kintoClient;
 
-function kintoCollection(collectionName) {
-  if (!kintoClient) {
-    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,
-      bucket: "blocklists"
-    };
-    kintoClient = new Kinto(config);
-  }
-  return kintoClient.collection(collectionName);
+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);
 }
 
 function* readJSON(filepath) {
   const binaryData = yield OS.File.read(filepath);
   const textData = (new TextDecoder()).decode(binaryData);
   return Promise.resolve(JSON.parse(textData));
 }
 
 function* clear_state() {
   for (let {client} of gBlocklistClients) {
     // Remove last server times.
     Services.prefs.clearUserPref(client.lastCheckTimePref);
 
     // Clear local DB.
-    const collection = kintoCollection(client.collectionName);
+    let sqliteHandle;
     try {
-      yield collection.db.open();
+      sqliteHandle = yield FirefoxAdapter.openConnection({path: kintoFilename});
+      const collection = kintoCollection(client.collectionName, sqliteHandle);
       yield collection.clear();
     } finally {
-      yield collection.db.close();
+      yield sqliteHandle.close();
     }
   }
 
   // Remove profile data.
   for (let {filename} of gBlocklistClients) {
     const blocklist = FileUtils.getFile(KEY_PROFILEDIR, [filename]);
     if (blocklist.exists()) {
       blocklist.remove(true);
@@ -121,21 +120,21 @@ function run_test() {
 
 add_task(function* test_records_obtained_from_server_are_stored_in_db(){
   for (let {client} of gBlocklistClients) {
     // Test an empty db populates
     let result = yield client.maybeSync(2000, Date.now());
 
     // Open the collection, verify it's been populated:
     // Our test data has a single record; it should be in the local collection
-    let collection = kintoCollection(client.collectionName);
-    yield collection.db.open();
+    const sqliteHandle = yield FirefoxAdapter.openConnection({path: kintoFilename});
+    let collection = kintoCollection(client.collectionName, sqliteHandle);
     let list = yield collection.list();
     equal(list.data.length, 1);
-    yield collection.db.close();
+    yield sqliteHandle.close();
   }
 });
 add_task(clear_state);
 
 add_task(function* test_list_is_written_to_file_in_profile(){
   for (let {client, filename, testData} of gBlocklistClients) {
     const profFile = FileUtils.getFile(KEY_PROFILEDIR, [filename]);
     strictEqual(profFile.exists(), false);
--- a/services/common/tests/unit/test_blocklist_signatures.js
+++ b/services/common/tests/unit/test_blocklist_signatures.js
@@ -11,16 +11,17 @@ const { OneCRLBlocklistClient } = Cu.imp
 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";
 
+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) {
@@ -56,33 +57,33 @@ function getCertChain() {
 
 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 = yield FirefoxAdapter.openConnection({path: kintoFilename});
   const config = {
     remote: base,
     bucket: bucket,
     adapter: FirefoxAdapter,
+    adapterOptions: {sqliteHandle},
   };
 
   const db = new Kinto(config);
   const collection = db.collection(collectionName);
 
-  yield collection.db.open();
-
   // Check we have the expected number of records
   let records = yield collection.list();
   do_check_eq(count, records.data.length);
 
   // Close the collection so the test can exit cleanly
-  yield collection.db.close();
+  yield sqliteHandle.close();
 }
 
 // Check to ensure maybeSync is called with correct values when a changes
 // document contains information on when a collection was last modified
 add_task(function* test_check_signatures(){
   const port = server.identity.primaryPort;
 
   // a response to give the client when the cert chain is expected
--- a/services/common/tests/unit/test_kinto.js
+++ b/services/common/tests/unit/test_kinto.js
@@ -8,45 +8,47 @@ Cu.import("resource://testing-common/htt
 const BinaryInputStream = Components.Constructor("@mozilla.org/binaryinputstream;1",
   "nsIBinaryInputStream", "setInputStream");
 
 var server;
 
 // set up what we need to make storage adapters
 const kintoFilename = "kinto.sqlite";
 
-let kintoClient;
+function do_get_kinto_sqliteHandle() {
+  return FirefoxAdapter.openConnection({path: kintoFilename});
+}
 
-function do_get_kinto_collection() {
-  if (!kintoClient) {
-    let config = {
-      remote:`http://localhost:${server.identity.primaryPort}/v1/`,
-      headers: {Authorization: "Basic " + btoa("user:pass")},
-      adapter: FirefoxAdapter
-    };
-    kintoClient = new Kinto(config);
-  }
-  return kintoClient.collection("test_collection");
+function do_get_kinto_collection(sqliteHandle, collection="test_collection") {
+  let config = {
+    remote:`http://localhost:${server.identity.primaryPort}/v1/`,
+    headers: {Authorization: "Basic " + btoa("user:pass")},
+    adapter: FirefoxAdapter,
+    adapterOptions: {sqliteHandle},
+  };
+  return new Kinto(config).collection(collection);
 }
 
 function* clear_collection() {
-  const collection = do_get_kinto_collection();
+  let sqliteHandle;
   try {
-    yield collection.db.open();
+    sqliteHandle = yield do_get_kinto_sqliteHandle();
+    const collection = do_get_kinto_collection(sqliteHandle);
     yield collection.clear();
   } finally {
-    yield collection.db.close();
+    yield sqliteHandle.close();
   }
 }
 
 // test some operations on a local collection
 add_task(function* test_kinto_add_get() {
-  const collection = do_get_kinto_collection();
+  let sqliteHandle;
   try {
-    yield collection.db.open();
+    sqliteHandle = yield do_get_kinto_sqliteHandle();
+    const collection = do_get_kinto_collection(sqliteHandle);
 
     let newRecord = { foo: "bar" };
     // check a record is created
     let createResult = yield collection.create(newRecord);
     do_check_eq(createResult.data.foo, newRecord.foo);
     // check getting the record gets the same info
     let getResult = yield collection.get(createResult.data.id);
     deepEqual(createResult.data, getResult.data);
@@ -59,108 +61,109 @@ add_task(function* test_kinto_add_get() 
     // try a few creates without waiting for the first few to resolve
     let promises = [];
     promises.push(collection.create(newRecord));
     promises.push(collection.create(newRecord));
     promises.push(collection.create(newRecord));
     yield collection.create(newRecord);
     yield Promise.all(promises);
   } finally {
-    yield collection.db.close();
+    yield sqliteHandle.close();
   }
 });
 
 add_task(clear_collection);
 
 // test some operations on multiple connections
 add_task(function* test_kinto_add_get() {
-  const collection1 = do_get_kinto_collection();
-  const collection2 = kintoClient.collection("test_collection_2");
-
+  let sqliteHandle;
   try {
-    yield collection1.db.open();
-    yield collection2.db.open();
+    sqliteHandle = yield do_get_kinto_sqliteHandle();
+    const collection1 = do_get_kinto_collection(sqliteHandle);
+    const collection2 = do_get_kinto_collection(sqliteHandle, "test_collection_2");
 
     let newRecord = { foo: "bar" };
 
     // perform several write operations alternately without waiting for promises
     // to resolve
     let promises = [];
     for (let i = 0; i < 10; i++) {
       promises.push(collection1.create(newRecord));
       promises.push(collection2.create(newRecord));
     }
 
     // ensure subsequent operations still work
     yield Promise.all([collection1.create(newRecord),
                        collection2.create(newRecord)]);
     yield Promise.all(promises);
   } finally {
-    yield collection1.db.close();
-    yield collection2.db.close();
+    yield sqliteHandle.close();
   }
 });
 
 add_task(clear_collection);
 
 add_task(function* test_kinto_update() {
-  const collection = do_get_kinto_collection();
+  let sqliteHandle;
   try {
-    yield collection.db.open();
+    sqliteHandle = yield do_get_kinto_sqliteHandle();
+    const collection = do_get_kinto_collection(sqliteHandle);
     const newRecord = { foo: "bar" };
     // check a record is created
     let createResult = yield collection.create(newRecord);
     do_check_eq(createResult.data.foo, newRecord.foo);
     do_check_eq(createResult.data._status, "created");
     // check we can update this OK
     let copiedRecord = Object.assign(createResult.data, {});
     deepEqual(createResult.data, copiedRecord);
     copiedRecord.foo = "wibble";
     let updateResult = yield collection.update(copiedRecord);
     // check the field was updated
     do_check_eq(updateResult.data.foo, copiedRecord.foo);
     // check the status is still "created", since we haven't synced
     // the record
     do_check_eq(updateResult.data._status, "created");
   } finally {
-    yield collection.db.close();
+    yield sqliteHandle.close();
   }
 });
 
 add_task(clear_collection);
 
 add_task(function* test_kinto_clear() {
-  const collection = do_get_kinto_collection();
+  let sqliteHandle;
   try {
-    yield collection.db.open();
+    sqliteHandle = yield do_get_kinto_sqliteHandle();
+    const collection = do_get_kinto_collection(sqliteHandle);
 
     // create an expected number of records
     const expected = 10;
     const newRecord = { foo: "bar" };
     for (let i = 0; i < expected; i++) {
       yield collection.create(newRecord);
     }
     // check the collection contains the correct number
     let list = yield collection.list();
     do_check_eq(list.data.length, expected);
     // clear the collection and check again - should be 0
     yield collection.clear();
     list = yield collection.list();
     do_check_eq(list.data.length, 0);
   } finally {
-    yield collection.db.close();
+    yield sqliteHandle.close();
   }
 });
 
 add_task(clear_collection);
 
 add_task(function* test_kinto_delete(){
-  const collection = do_get_kinto_collection();
+  let sqliteHandle;
   try {
-    yield collection.db.open();
+    sqliteHandle = yield do_get_kinto_sqliteHandle();
+    const collection = do_get_kinto_collection(sqliteHandle);
     const newRecord = { foo: "bar" };
     // check a record is created
     let createResult = yield collection.create(newRecord);
     do_check_eq(createResult.data.foo, newRecord.foo);
     // check getting the record gets the same info
     let getResult = yield collection.get(createResult.data.id);
     deepEqual(createResult.data, getResult.data);
     // delete that record
@@ -168,24 +171,25 @@ add_task(function* test_kinto_delete(){
     // check the ID is set on the result
     do_check_eq(getResult.data.id, deleteResult.data.id);
     // and check that get no longer returns the record
     try {
       getResult = yield collection.get(createResult.data.id);
       do_throw("there should not be a result");
     } catch (e) { }
   } finally {
-    yield collection.db.close();
+    yield sqliteHandle.close();
   }
 });
 
 add_task(function* test_kinto_list(){
-  const collection = do_get_kinto_collection();
+  let sqliteHandle;
   try {
-    yield collection.db.open();
+    sqliteHandle = yield do_get_kinto_sqliteHandle();
+    const collection = do_get_kinto_collection(sqliteHandle);
     const expected = 10;
     const created = [];
     for (let i = 0; i < expected; i++) {
       let newRecord = { foo: "test " + i };
       let createResult = yield collection.create(newRecord);
       created.push(createResult.data);
     }
     // check the collection contains the correct number
@@ -199,80 +203,84 @@ add_task(function* test_kinto_list(){
         if (createdRecord.id == retrievedRecord.id) {
           deepEqual(createdRecord, retrievedRecord);
           found = true;
         }
       }
       do_check_true(found);
     }
   } finally {
-    yield collection.db.close();
+    yield sqliteHandle.close();
   }
 });
 
 add_task(clear_collection);
 
 add_task(function* test_loadDump_ignores_already_imported_records(){
-  const collection = do_get_kinto_collection();
+  let sqliteHandle;
   try {
-    yield collection.db.open();
+    sqliteHandle = yield do_get_kinto_sqliteHandle();
+    const collection = do_get_kinto_collection(sqliteHandle);
     const record = {id: "41b71c13-17e9-4ee3-9268-6a41abf9730f", title: "foo", last_modified: 1457896541};
     yield collection.loadDump([record]);
     let impactedRecords = yield collection.loadDump([record]);
     do_check_eq(impactedRecords.length, 0);
   } finally {
-    yield collection.db.close();
+    yield sqliteHandle.close();
   }
 });
 
 add_task(clear_collection);
 
 add_task(function* test_loadDump_should_overwrite_old_records(){
-  const collection = do_get_kinto_collection();
+  let sqliteHandle;
   try {
-    yield collection.db.open();
+    sqliteHandle = yield do_get_kinto_sqliteHandle();
+    const collection = do_get_kinto_collection(sqliteHandle);
     const record = {id: "41b71c13-17e9-4ee3-9268-6a41abf9730f", title: "foo", last_modified: 1457896541};
     yield collection.loadDump([record]);
     const updated = Object.assign({}, record, {last_modified: 1457896543});
     let impactedRecords = yield collection.loadDump([updated]);
     do_check_eq(impactedRecords.length, 1);
   } finally {
-    yield collection.db.close();
+    yield sqliteHandle.close();
   }
 });
 
 add_task(clear_collection);
 
 add_task(function* test_loadDump_should_not_overwrite_unsynced_records(){
-  const collection = do_get_kinto_collection();
+  let sqliteHandle;
   try {
-    yield collection.db.open();
+    sqliteHandle = yield do_get_kinto_sqliteHandle();
+    const collection = do_get_kinto_collection(sqliteHandle);
     const recordId = "41b71c13-17e9-4ee3-9268-6a41abf9730f";
     yield collection.create({id: recordId, title: "foo"}, {useRecordId: true});
     const record = {id: recordId, title: "bar", last_modified: 1457896541};
     let impactedRecords = yield collection.loadDump([record]);
     do_check_eq(impactedRecords.length, 0);
   } finally {
-    yield collection.db.close();
+    yield sqliteHandle.close();
   }
 });
 
 add_task(clear_collection);
 
 add_task(function* test_loadDump_should_not_overwrite_records_without_last_modified(){
-  const collection = do_get_kinto_collection();
+  let sqliteHandle;
   try {
-    yield collection.db.open();
+    sqliteHandle = yield do_get_kinto_sqliteHandle();
+    const collection = do_get_kinto_collection(sqliteHandle);
     const recordId = "41b71c13-17e9-4ee3-9268-6a41abf9730f";
     yield collection.create({id: recordId, title: "foo"}, {synced: true});
     const record = {id: recordId, title: "bar", last_modified: 1457896541};
     let impactedRecords = yield collection.loadDump([record]);
     do_check_eq(impactedRecords.length, 0);
   } finally {
-    yield collection.db.close();
+    yield sqliteHandle.close();
   }
 });
 
 add_task(clear_collection);
 
 // Now do some sanity checks against a server - we're not looking to test
 // core kinto.js functionality here (there is excellent test coverage in
 // kinto.js), more making sure things are basically working as expected.
@@ -300,21 +308,22 @@ add_task(function* test_kinto_sync(){
     } catch (e) {
       dump(`${e}\n`);
     }
   }
   server.registerPathHandler(configPath, handleResponse);
   server.registerPathHandler(recordsPath, handleResponse);
 
   // create an empty collection, sync to populate
-  const collection = do_get_kinto_collection();
+  let sqliteHandle;
   try {
     let result;
+    sqliteHandle = yield do_get_kinto_sqliteHandle();
+    const collection = do_get_kinto_collection(sqliteHandle);
 
-    yield collection.db.open();
     result = yield collection.sync();
     do_check_true(result.ok);
 
     // our test data has a single record; it should be in the local collection
     let list = yield collection.list();
     do_check_eq(list.data.length, 1);
 
     // now sync again; we should now have 2 records
@@ -326,17 +335,17 @@ add_task(function* test_kinto_sync(){
     // sync again; the second records should have been modified
     const before = list.data[0].title;
     result = yield collection.sync();
     do_check_true(result.ok);
     list = yield collection.list();
     const after = list.data[0].title;
     do_check_neq(before, after);
   } finally {
-    yield collection.db.close();
+    yield sqliteHandle.close();
   }
 });
 
 function run_test() {
   // Set up an HTTP Server
   server = new HttpServer();
   server.start(-1);
 
--- a/services/common/tests/unit/test_storage_adapter.js
+++ b/services/common/tests/unit/test_storage_adapter.js
@@ -2,233 +2,232 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 Cu.import("resource://services-common/kinto-offline-client.js");
 Cu.import("resource://services-common/kinto-storage-adapter.js");
 
 // set up what we need to make storage adapters
 const kintoFilename = "kinto.sqlite";
 
-let gFirefoxAdapter = null;
+function do_get_kinto_connection() {
+  return FirefoxAdapter.openConnection({path: kintoFilename});
+}
 
-function do_get_kinto_adapter() {
-  if (gFirefoxAdapter == null) {
-    gFirefoxAdapter = new FirefoxAdapter("test");
-  }
-  return gFirefoxAdapter;
+function do_get_kinto_adapter(sqliteHandle) {
+  return new FirefoxAdapter("test", {sqliteHandle});
 }
 
 function do_get_kinto_db() {
   let profile = do_get_profile();
   let kintoDB = profile.clone();
   kintoDB.append(kintoFilename);
   return kintoDB;
 }
 
 function cleanup_kinto() {
   add_test(function cleanup_kinto_files(){
     let kintoDB = do_get_kinto_db();
     // clean up the db
     kintoDB.remove(false);
-    // force re-creation of the adapter
-    gFirefoxAdapter = null;
     run_next_test();
   });
 }
 
 function test_collection_operations() {
   add_task(function* test_kinto_clear() {
-    let adapter = do_get_kinto_adapter();
-    yield adapter.open();
+    let sqliteHandle = yield do_get_kinto_connection();
+    let adapter = do_get_kinto_adapter(sqliteHandle);
     yield adapter.clear();
-    yield adapter.close();
+    yield sqliteHandle.close();
   });
 
   // test creating new records... and getting them again
   add_task(function* test_kinto_create_new_get_existing() {
-    let adapter = do_get_kinto_adapter();
-    yield adapter.open();
+    let sqliteHandle = yield do_get_kinto_connection();
+    let adapter = do_get_kinto_adapter(sqliteHandle);
     let record = {id:"test-id", foo:"bar"};
     yield adapter.execute((transaction) => transaction.create(record));
     let newRecord = yield adapter.get("test-id");
     // ensure the record is the same as when it was added
     deepEqual(record, newRecord);
-    yield adapter.close();
+    yield sqliteHandle.close();
   });
 
   // test removing records
   add_task(function* test_kinto_can_remove_some_records() {
-    let adapter = do_get_kinto_adapter();
-    yield adapter.open();
+    let sqliteHandle = yield do_get_kinto_connection();
+    let adapter = do_get_kinto_adapter(sqliteHandle);
     // create a second record
     let record = {id:"test-id-2", foo:"baz"};
     yield adapter.execute((transaction) => transaction.create(record));
     let newRecord = yield adapter.get("test-id-2");
     deepEqual(record, newRecord);
     // delete the record
     yield adapter.execute((transaction) => transaction.delete(record.id));
     newRecord = yield adapter.get(record.id);
     // ... and ensure it's no longer there
     do_check_eq(newRecord, undefined);
     // ensure the other record still exists
     newRecord = yield adapter.get("test-id");
     do_check_neq(newRecord, undefined);
-    yield adapter.close();
+    yield sqliteHandle.close();
   });
 
   // test getting records that don't exist
   add_task(function* test_kinto_get_non_existant() {
-    let adapter = do_get_kinto_adapter();
+    let sqliteHandle = yield do_get_kinto_connection();
+    let adapter = do_get_kinto_adapter(sqliteHandle);
     yield adapter.open();
     // Kinto expects adapters to either:
     let newRecord = yield adapter.get("missing-test-id");
     // resolve with an undefined record
     do_check_eq(newRecord, undefined);
-    yield adapter.close();
+    yield sqliteHandle.close();
   });
 
   // test updating records... and getting them again
   add_task(function* test_kinto_update_get_existing() {
-    let adapter = do_get_kinto_adapter();
-    yield adapter.open();
+    let sqliteHandle = yield do_get_kinto_connection();
+    let adapter = do_get_kinto_adapter(sqliteHandle);
     let originalRecord = {id:"test-id", foo:"bar"};
     let updatedRecord = {id:"test-id", foo:"baz"};
     yield adapter.clear();
     yield adapter.execute((transaction) => transaction.create(originalRecord));
     yield adapter.execute((transaction) => transaction.update(updatedRecord));
     // ensure the record exists
     let newRecord = yield adapter.get("test-id");
     // ensure the record is the same as when it was added
     deepEqual(updatedRecord, newRecord);
-    yield adapter.close();
+    yield sqliteHandle.close();
   });
 
   // test listing records
   add_task(function* test_kinto_list() {
-    let adapter = do_get_kinto_adapter();
-    yield adapter.open();
+    let sqliteHandle = yield do_get_kinto_connection();
+    let adapter = do_get_kinto_adapter(sqliteHandle);
     let originalRecord = {id:"test-id-1", foo:"bar"};
     let records = yield adapter.list();
     do_check_eq(records.length, 1);
     yield adapter.execute((transaction) => transaction.create(originalRecord));
     records = yield adapter.list();
     do_check_eq(records.length, 2);
-    yield adapter.close();
+    yield sqliteHandle.close();
   });
 
   // test aborting transaction
   add_task(function* test_kinto_aborting_transaction() {
-    let adapter = do_get_kinto_adapter();
-    yield adapter.open();
+    let sqliteHandle = yield do_get_kinto_connection();
+    let adapter = do_get_kinto_adapter(sqliteHandle);
     yield adapter.clear();
     let record = {id: 1, foo: "bar"};
     let error = null;
     try {
       yield adapter.execute((transaction) => {
         transaction.create(record);
         throw new Error("unexpected");
       });
     } catch (e) {
       error = e;
     }
     do_check_neq(error, null);
     records = yield adapter.list();
     do_check_eq(records.length, 0);
-    yield adapter.close();
+    yield sqliteHandle.close();
   });
 
   // test save and get last modified
   add_task(function* test_kinto_last_modified() {
     const initialValue = 0;
     const intendedValue = 12345678;
 
-    let adapter = do_get_kinto_adapter();
-    yield adapter.open();
+    let sqliteHandle = yield do_get_kinto_connection();
+    let adapter = do_get_kinto_adapter(sqliteHandle);
     let lastModified = yield adapter.getLastModified();
     do_check_eq(lastModified, initialValue);
     let result = yield adapter.saveLastModified(intendedValue);
     do_check_eq(result, intendedValue);
     lastModified = yield adapter.getLastModified();
     do_check_eq(lastModified, intendedValue);
 
     // test saveLastModified parses values correctly
     result = yield adapter.saveLastModified(" " + intendedValue + " blah");
     // should resolve with the parsed int
     do_check_eq(result, intendedValue);
     // and should have saved correctly
     lastModified = yield adapter.getLastModified();
     do_check_eq(lastModified, intendedValue);
-    yield adapter.close();
+    yield sqliteHandle.close();
   });
 
   // test loadDump(records)
   add_task(function* test_kinto_import_records() {
-    let adapter = do_get_kinto_adapter();
-    yield adapter.open();
+    let sqliteHandle = yield do_get_kinto_connection();
+    let adapter = do_get_kinto_adapter(sqliteHandle);
     let record1 = {id: 1, foo: "bar"};
     let record2 = {id: 2, foo: "baz"};
     let impactedRecords = yield adapter.loadDump([
       record1, record2
     ]);
     do_check_eq(impactedRecords.length, 2);
     let newRecord1 = yield adapter.get("1");
     // ensure the record is the same as when it was added
     deepEqual(record1, newRecord1);
     let newRecord2 = yield adapter.get("2");
     // ensure the record is the same as when it was added
     deepEqual(record2, newRecord2);
-    yield adapter.close();
+    yield sqliteHandle.close();
   });
 
   add_task(function* test_kinto_import_records_should_override_existing() {
-    let adapter = do_get_kinto_adapter();
-    yield adapter.open();
+    let sqliteHandle = yield do_get_kinto_connection();
+    let adapter = do_get_kinto_adapter(sqliteHandle);
     yield adapter.clear();
     records = yield adapter.list();
     do_check_eq(records.length, 0);
     let impactedRecords = yield adapter.loadDump([
       {id: 1, foo: "bar"},
       {id: 2, foo: "baz"},
     ]);
     do_check_eq(impactedRecords.length, 2);
     yield adapter.loadDump([
       {id: 1, foo: "baz"},
       {id: 3, foo: "bab"},
     ]);
     records = yield adapter.list();
     do_check_eq(records.length, 3);
     let newRecord1 = yield adapter.get("1");
     deepEqual(newRecord1.foo, "baz");
-    yield adapter.close();
+    yield sqliteHandle.close();
   });
 
   add_task(function* test_import_updates_lastModified() {
-    let adapter = do_get_kinto_adapter();
+    let sqliteHandle = yield do_get_kinto_connection();
+    let adapter = do_get_kinto_adapter(sqliteHandle);
     yield adapter.open();
     yield adapter.loadDump([
       {id: 1, foo: "bar", last_modified: 1457896541},
       {id: 2, foo: "baz", last_modified: 1458796542},
     ]);
     let lastModified = yield adapter.getLastModified();
     do_check_eq(lastModified, 1458796542);
-    yield adapter.close();
+    yield sqliteHandle.close();
   });
 
   add_task(function* test_import_preserves_older_lastModified() {
-    let adapter = do_get_kinto_adapter();
-    yield adapter.open();
+    let sqliteHandle = yield do_get_kinto_connection();
+    let adapter = do_get_kinto_adapter(sqliteHandle);
     yield adapter.saveLastModified(1458796543);
 
     yield adapter.loadDump([
       {id: 1, foo: "bar", last_modified: 1457896541},
       {id: 2, foo: "baz", last_modified: 1458796542},
     ]);
     let lastModified = yield adapter.getLastModified();
     do_check_eq(lastModified, 1458796543);
-    yield adapter.close();
+    yield sqliteHandle.close();
   });
 }
 
 // test kinto db setup and operations in various scenarios
 // test from scratch - no current existing database
 add_test(function test_db_creation() {
   add_test(function test_create_from_scratch() {
     // ensure the file does not exist in the profile