Bug 991682 - Sqlite.jsm API to clone an open db connection. r=Yoric
authorMarco Bonardo <mbonardo@mozilla.com>
Tue, 08 Apr 2014 14:09:26 +0200
changeset 195998 bd137ef426a8f833595a1cf0193492d566c4d3eb
parent 195997 452aee797482d640ca6f63f04af1509470b5880d
child 195999 47e0c3bda71e41e8dc178b66b65205fa7565f5e7
push id3624
push userasasaki@mozilla.com
push dateMon, 09 Jun 2014 21:49:01 +0000
treeherdermozilla-beta@b1a5da15899a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersYoric
bugs991682
milestone31.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 991682 - Sqlite.jsm API to clone an open db connection. r=Yoric
toolkit/modules/Sqlite.jsm
toolkit/modules/tests/xpcshell/test_sqlite.js
--- a/toolkit/modules/Sqlite.jsm
+++ b/toolkit/modules/Sqlite.jsm
@@ -106,29 +106,106 @@ function openConnection(options) {
       createInstance(Ci.nsIWritablePropertyBag);
     options.setProperty("shared", false);
   }
   Services.storage.openAsyncDatabase(file, options, function(status, connection) {
     if (!connection) {
       log.warn("Could not open connection: " + status);
       deferred.reject(new Error("Could not open connection: " + status));
     }
-    log.warn("Connection opened");
+    log.info("Connection opened");
     try {
       deferred.resolve(
         new OpenedConnection(connection.QueryInterface(Ci.mozIStorageAsyncConnection), basename, number,
         openedOptions));
     } catch (ex) {
       log.warn("Could not open database: " + CommonUtils.exceptionStr(ex));
       deferred.reject(ex);
     }
   });
   return deferred.promise;
 }
 
+/**
+ * Creates a clone of an existing and open Storage connection.  The clone has
+ * the same underlying characteristics of the original connection and is
+ * returned in form of on OpenedConnection handle.
+ *
+ * The following parameters can control the cloned connection:
+ *
+ *   connection -- (mozIStorageAsyncConnection) The original Storage connection
+ *       to clone.  It's not possible to clone connections to memory databases.
+ *
+ *   readOnly -- (boolean) - If true the clone will be read-only.  If the
+ *       original connection is already read-only, the clone will be, regardless
+ *       of this option.  If the original connection is using the shared cache,
+ *       this parameter will be ignored and the clone will be as privileged as
+ *       the original connection.
+ *   shrinkMemoryOnConnectionIdleMS -- (integer) If defined, the connection
+ *       will attempt to minimize its memory usage after this many
+ *       milliseconds of connection idle. The connection is idle when no
+ *       statements are executing. There is no default value which means no
+ *       automatic memory minimization will occur. Please note that this is
+ *       *not* a timer on the idle service and this could fire while the
+ *       application is active.
+ *
+ *
+ * @param options
+ *        (Object) Parameters to control connection and clone options.
+ *
+ * @return Promise<OpenedConnection>
+ */
+function cloneStorageConnection(options) {
+  let log = Log.repository.getLogger("Sqlite.ConnectionCloner");
+
+  let source = options && options.connection;
+  if (!source) {
+    throw new TypeError("connection not specified in clone options.");
+  }
+  if (!source instanceof Ci.mozIStorageAsyncConnection) {
+    throw new TypeError("Connection must be a valid Storage connection.")
+  }
+
+  let openedOptions = {};
+
+  if ("shrinkMemoryOnConnectionIdleMS" in options) {
+    if (!Number.isInteger(options.shrinkMemoryOnConnectionIdleMS)) {
+      throw new TypeError("shrinkMemoryOnConnectionIdleMS must be an integer. " +
+                          "Got: " + options.shrinkMemoryOnConnectionIdleMS);
+    }
+    openedOptions.shrinkMemoryOnConnectionIdleMS =
+      options.shrinkMemoryOnConnectionIdleMS;
+  }
+
+  let path = source.databaseFile.path;
+  let basename = OS.Path.basename(path);
+  let number = connectionCounters.get(basename) || 0;
+  connectionCounters.set(basename, number + 1);
+  let identifier = basename + "#" + number;
+
+  log.info("Cloning database: " + path + " (" + identifier + ")");
+  let deferred = Promise.defer();
+
+  source.asyncClone(!!options.readOnly, (status, connection) => {
+    if (!connection) {
+      log.warn("Could not clone connection: " + status);
+      deferred.reject(new Error("Could not clone connection: " + status));
+    }
+    log.info("Connection cloned");
+    try {
+      let conn = connection.QueryInterface(Ci.mozIStorageAsyncConnection);
+      deferred.resolve(new OpenedConnection(conn, basename, number,
+                                            openedOptions));
+    } catch (ex) {
+      log.warn("Could not clone database: " + CommonUtils.exceptionStr(ex));
+      deferred.reject(ex);
+    }
+  });
+  return deferred.promise;
+}
 
 /**
  * Handle on an opened SQLite database.
  *
  * This is essentially a glorified wrapper around mozIStorageConnection.
  * However, it offers some compelling advantages.
  *
  * The main functions on this type are `execute` and `executeCached`. These are
@@ -288,16 +365,45 @@ OpenedConnection.prototype = Object.free
 
     this.execute("ROLLBACK TRANSACTION").then(onRollback, onRollback);
     this._inProgressTransaction.reject(new Error("Connection being closed."));
     this._inProgressTransaction = null;
 
     return deferred.promise;
   },
 
+  /**
+   * Clones this connection to a new Sqlite one.
+   *
+   * The following parameters can control the cloned connection:
+   *
+   * @param readOnly
+   *        (boolean) - If true the clone will be read-only.  If the original
+   *        connection is already read-only, the clone will be, regardless of
+   *        this option.  If the original connection is using the shared cache,
+   *        this parameter will be ignored and the clone will be as privileged as
+   *        the original connection.
+   *
+   * @return Promise<OpenedConnection>
+   */
+  clone: function (readOnly=false) {
+    this._ensureOpen();
+
+    this._log.debug("Request to clone connection.");
+
+    let options = {
+      connection: this._connection,
+      readOnly: readOnly,
+    };
+    if (this._idleShrinkMS)
+      options.shrinkMemoryOnConnectionIdleMS = this._idleShrinkMS;
+
+    return cloneStorageConnection(options);
+  },
+
   _finalize: function (deferred) {
     this._log.debug("Finalizing connection.");
     // Cancel any pending statements.
     for (let [k, statement] of this._pendingStatements) {
       statement.cancel();
     }
     this._pendingStatements.clear();
 
@@ -829,9 +935,10 @@ OpenedConnection.prototype = Object.free
     this._idleShrinkTimer.initWithCallback(this.shrinkMemory.bind(this),
                                            this._idleShrinkMS,
                                            this._idleShrinkTimer.TYPE_ONE_SHOT);
   },
 });
 
 this.Sqlite = {
   openConnection: openConnection,
+  cloneStorageConnection: cloneStorageConnection
 };
--- a/toolkit/modules/tests/xpcshell/test_sqlite.js
+++ b/toolkit/modules/tests/xpcshell/test_sqlite.js
@@ -828,8 +828,81 @@ add_task(function test_direct() {
 
   deferred = Promise.defer();
   db.asyncClose(function () {
     deferred.resolve()
   });
   yield deferred.promise;
 });
 
+/**
+ * Test Sqlite.cloneStorageConnection.
+ */
+add_task(function* test_cloneStorageConnection() {
+  let file = new FileUtils.File(OS.Path.join(OS.Constants.Path.profileDir,
+                                             "test_cloneStorageConnection.sqlite"));
+  let c = yield new Promise((success, failure) => {
+    Services.storage.openAsyncDatabase(file, null, (status, db) => {
+      if (Components.isSuccessCode(status)) {
+        success(db.QueryInterface(Ci.mozIStorageAsyncConnection));
+      } else {
+        failure(new Error(status));
+      }
+    });
+  });
+
+  let clone = yield Sqlite.cloneStorageConnection({ connection: c, readOnly: true });
+  // Just check that it works.
+  yield clone.execute("SELECT 1");
+
+  let clone2 = yield Sqlite.cloneStorageConnection({ connection: c, readOnly: false });
+  // Just check that it works.
+  yield clone2.execute("CREATE TABLE test (id INTEGER PRIMARY KEY)");
+
+  // Closing order should not matter.
+  yield c.asyncClose();
+  yield clone2.close();
+  yield clone.close();
+});
+
+/**
+ * Test Sqlite.cloneStorageConnection invalid argument.
+ */
+add_task(function* test_cloneStorageConnection() {
+  try {
+    let clone = yield Sqlite.cloneStorageConnection({ connection: null });
+    do_throw(new Error("Should throw on invalid connection"));
+  } catch (ex if ex.name == "TypeError") {}
+});
+
+/**
+ * Test clone() method.
+ */
+add_task(function* test_clone() {
+  let c = yield getDummyDatabase("clone");
+
+  let clone = yield c.clone();
+  // Just check that it works.
+  yield clone.execute("SELECT 1");
+  // Closing order should not matter.
+  yield c.close();
+  yield clone.close();
+});
+
+/**
+ * Test clone(readOnly) method.
+ */
+add_task(function* test_readOnly_clone() {
+  let path = OS.Path.join(OS.Constants.Path.profileDir, "test_readOnly_clone.sqlite");
+  let c = yield Sqlite.openConnection({path: path, sharedMemoryCache: false});
+
+  let clone = yield c.clone(true);
+  // Just check that it works.
+  yield clone.execute("SELECT 1");
+  // But should not be able to write.
+  try {
+    yield clone.execute("CREATE TABLE test (id INTEGER PRIMARY KEY)");
+    do_throw(new Error("Should not be able to write to a read-only clone."));
+  } catch (ex) {}
+  // Closing order should not matter.
+  yield c.close();
+  yield clone.close();
+});