Bug 830209 - SQLite.jsm now handles transactions off the main thread; r=mak

This commit is contained in:
Gregory Szorc 2013-01-18 10:11:22 -08:00
parent 544cbd1188
commit b585e3decc
2 changed files with 154 additions and 34 deletions

View File

@ -176,9 +176,11 @@ function OpenedConnection(connection, basename, number) {
}
OpenedConnection.prototype = Object.freeze({
TRANSACTION_DEFERRED: Ci.mozIStorageConnection.TRANSACTION_DEFERRED,
TRANSACTION_IMMEDIATE: Ci.mozIStorageConnection.TRANSACTION_IMMEDIATE,
TRANSACTION_EXCLUSIVE: Ci.mozIStorageConnection.TRANSACTION_EXCLUSIVE,
TRANSACTION_DEFERRED: "DEFERRED",
TRANSACTION_IMMEDIATE: "IMMEDIATE",
TRANSACTION_EXCLUSIVE: "EXCLUSIVE",
TRANSACTION_TYPES: ["DEFERRED", "IMMEDIATE", "EXCLUSIVE"],
get connectionReady() {
return this._open && this._connection.connectionReady;
@ -249,21 +251,35 @@ OpenedConnection.prototype = Object.freeze({
return Promise.resolve();
}
this._log.debug("Closing.");
this._log.debug("Request to close connection.");
// Abort in-progress transaction.
if (this._inProgressTransaction) {
this._log.warn("Transaction in progress at time of close.");
try {
this._connection.rollbackTransaction();
} catch (ex) {
this._log.warn("Error rolling back transaction: " +
CommonUtils.exceptionStr(ex));
}
this._inProgressTransaction.reject(new Error("Connection being closed."));
this._inProgressTransaction = null;
let deferred = Promise.defer();
// We need to take extra care with transactions during shutdown.
//
// If we don't have a transaction in progress, we can proceed with shutdown
// immediately.
if (!this._inProgressTransaction) {
this._finalize(deferred);
return deferred.promise;
}
// Else if we do have a transaction in progress, we forcefully roll it
// back. This is an async task, so we wait on it to finish before
// performing finalization.
this._log.warn("Transaction in progress at time of close. Rolling back.");
let onRollback = this._finalize.bind(this, deferred);
this.execute("ROLLBACK TRANSACTION").then(onRollback, onRollback);
this._inProgressTransaction.reject(new Error("Connection being closed."));
this._inProgressTransaction = null;
return deferred.promise;
},
_finalize: function (deferred) {
this._log.debug("Finalizing connection.");
// Cancel any in-progress statements.
for (let [k, statement] of this._inProgressStatements) {
statement.cancel();
@ -285,8 +301,6 @@ OpenedConnection.prototype = Object.freeze({
// function and asyncClose() finishing. See also bug 726990.
this._open = false;
let deferred = Promise.defer();
this._log.debug("Calling asyncClose().");
this._connection.asyncClose({
complete: function () {
@ -295,8 +309,6 @@ OpenedConnection.prototype = Object.freeze({
deferred.resolve();
}.bind(this),
});
return deferred.promise;
},
/**
@ -423,7 +435,7 @@ OpenedConnection.prototype = Object.freeze({
* Whether a transaction is currently in progress.
*/
get transactionInProgress() {
return this._open && this._connection.transactionInProgress;
return this._open && !!this._inProgressTransaction;
},
/**
@ -450,34 +462,76 @@ OpenedConnection.prototype = Object.freeze({
* One of the TRANSACTION_* constants attached to this type.
*/
executeTransaction: function (func, type=this.TRANSACTION_DEFERRED) {
if (this.TRANSACTION_TYPES.indexOf(type) == -1) {
throw new Error("Unknown transaction type: " + type);
}
this._ensureOpen();
if (this.transactionInProgress) {
if (this._inProgressTransaction) {
throw new Error("A transaction is already active. Only one transaction " +
"can be active at a time.");
}
this._log.debug("Beginning transaction");
this._connection.beginTransactionAs(type);
let deferred = Promise.defer();
this._inProgressTransaction = deferred;
Task.spawn(function doTransaction() {
// It's tempting to not yield here and rely on the implicit serial
// execution of issued statements. However, the yield serves an important
// purpose: catching errors in statement execution.
yield this.execute("BEGIN " + type + " TRANSACTION");
Task.spawn(func(this)).then(
function onSuccess (result) {
this._connection.commitTransaction();
let result;
try {
result = yield Task.spawn(func(this));
} catch (ex) {
// It's possible that a request to close the connection caused the
// error.
// Assertion: close() will unset this._inProgressTransaction when
// called.
if (!this._inProgressTransaction) {
this._log.warn("Connection was closed while performing transaction. " +
"Received error should be due to closed connection: " +
CommonUtils.exceptionStr(ex));
throw ex;
}
this._log.warn("Error during transaction. Rolling back: " +
CommonUtils.exceptionStr(ex));
try {
yield this.execute("ROLLBACK TRANSACTION");
} catch (inner) {
this._log.warn("Could not roll back transaction. This is weird: " +
CommonUtils.exceptionStr(inner));
}
throw ex;
}
// See comment above about connection being closed during transaction.
if (!this._inProgressTransaction) {
this._log.warn("Connection was closed while performing transaction. " +
"Unable to commit.");
throw new Error("Connection closed before transaction committed.");
}
try {
yield this.execute("COMMIT TRANSACTION");
} catch (ex) {
this._log.warn("Error committing transaction: " +
CommonUtils.exceptionStr(ex));
throw ex;
}
throw new Task.Result(result);
}.bind(this)).then(
function onSuccess(result) {
this._inProgressTransaction = null;
this._log.debug("Transaction committed.");
deferred.resolve(result);
}.bind(this),
function onError (error) {
this._log.warn("Error during transaction. Rolling back: " +
CommonUtils.exceptionStr(error));
this._connection.rollbackTransaction();
function onError(error) {
this._inProgressTransaction = null;
deferred.reject(error);
}.bind(this)
);

View File

@ -213,6 +213,22 @@ add_task(function test_on_row_stop_iteration() {
yield c.close();
});
add_task(function test_invalid_transaction_type() {
let c = yield getDummyDatabase("invalid_transaction_type");
let errored = false;
try {
c.executeTransaction(function () {}, "foobar");
} catch (ex) {
errored = true;
do_check_true(ex.message.startsWith("Unknown transaction type"));
} finally {
do_check_true(errored);
}
yield c.close();
});
add_task(function test_execute_transaction_success() {
let c = yield getDummyDatabase("execute_transaction_success");
@ -257,3 +273,53 @@ add_task(function test_execute_transaction_rollback() {
yield c.close();
});
add_task(function test_close_during_transaction() {
let c = yield getDummyDatabase("close_during_transaction");
yield c.execute("INSERT INTO dirs (path) VALUES ('foo')");
let errored = false;
try {
yield c.executeTransaction(function transaction(conn) {
yield c.execute("INSERT INTO dirs (path) VALUES ('bar')");
yield c.close();
});
} catch (ex) {
errored = true;
do_check_eq(ex.message, "Connection being closed.");
} finally {
do_check_true(errored);
}
let c2 = yield getConnection("close_during_transaction");
let rows = yield c2.execute("SELECT * FROM dirs");
do_check_eq(rows.length, 1);
yield c2.close();
});
add_task(function test_detect_multiple_transactions() {
let c = yield getDummyDatabase("detect_multiple_transactions");
yield c.executeTransaction(function main() {
yield c.execute("INSERT INTO dirs (path) VALUES ('foo')");
let errored = false;
try {
yield c.executeTransaction(function child() {
yield c.execute("INSERT INTO dirs (path) VALUES ('bar')");
});
} catch (ex) {
errored = true;
do_check_true(ex.message.startsWith("A transaction is already active."));
} finally {
do_check_true(errored);
}
});
let rows = yield c.execute("SELECT * FROM dirs");
do_check_eq(rows.length, 1);
yield c.close();
});