www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | Submodules | README | LICENSE

commit 9e3e680be80f70a446ceb2e106d458dc2a26b843
parent 4cb5e7e4d5438f95b1140b9971556c5942fa8213
Author: Dan Stillman <dstillman@zotero.org>
Date:   Sat, 25 Apr 2015 02:11:09 -0400

Rework DB transaction handling

Rollback callbacks weren't being properly called, and some other things were in
the wrong place, particularly with nested transactions.

Diffstat:
Mchrome/content/zotero/xpcom/db.js | 138++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Atest/tests/dbTest.js | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 156 insertions(+), 65 deletions(-)

diff --git a/chrome/content/zotero/xpcom/db.js b/chrome/content/zotero/xpcom/db.js @@ -465,6 +465,15 @@ Zotero.DBConnection.prototype.executeTransaction = Zotero.Promise.coroutine(func } } + try { + var result = yield Zotero.Promise.coroutine(func)(); + } + catch (e) { + Zotero.debug("Rolled back nested async DB transaction", 5); + this._asyncTransactionNestingLevel = 0; + throw e; + } + Zotero.debug("Decreasing async DB transaction level to " + --this._asyncTransactionNestingLevel, 5); return result; @@ -489,79 +498,78 @@ Zotero.DBConnection.prototype.executeTransaction = Zotero.Promise.coroutine(func } } var result = yield conn.executeTransaction(func); - try { - Zotero.debug("Committed async DB transaction", 5); - - // Clear transaction time - if (this._transactionDate) { - this._transactionDate = null; - } - - if (options) { - // Function to run once transaction has been committed but before any - // permanent callbacks - if (options.onCommit) { - this._callbacks.current.commit.push(options.onCommit); - } - this._callbacks.current.rollback = []; - - if (options.vacuumOnCommit) { - Zotero.debug('Vacuuming database'); - yield Zotero.DB.queryAsync('VACUUM'); - } - } - - // Run temporary commit callbacks - var f; - while (f = this._callbacks.current.commit.shift()) { - yield Zotero.Promise.resolve(f()); + Zotero.debug("Committed async DB transaction", 5); + + // Clear transaction time + if (this._transactionDate) { + this._transactionDate = null; + } + + if (options) { + // Function to run once transaction has been committed but before any + // permanent callbacks + if (options.onCommit) { + this._callbacks.current.commit.push(options.onCommit); } + this._callbacks.current.rollback = []; - // Run commit callbacks - for (var i=0; i<this._callbacks.commit.length; i++) { - if (this._callbacks.commit[i]) { - yield this._callbacks.commit[i](); - } + if (options.vacuumOnCommit) { + Zotero.debug('Vacuuming database'); + yield Zotero.DB.queryAsync('VACUUM'); } - - setTimeout(resolve, 0); - - return result; } - catch (e) { - Zotero.debug("Rolled back async DB transaction", 5); - Zotero.debug(e, 1); - - try { - if (options) { - // Function to run once transaction has been committed but before any - // permanent callbacks - if (options.onRollback) { - this._callbacks.current.rollback.push(options.onRollback); - } - } - - // Run temporary commit callbacks - var f; - while (f = this._callbacks.current.rollback.shift()) { - yield Zotero.Promise.resolve(f()); - } - - // Run rollback callbacks - for (var i=0; i<this._callbacks.rollback.length; i++) { - if (this._callbacks.rollback[i]) { - yield Zotero.Promise.resolve(this._callbacks.rollback[i]()); - } - } - } - finally { - setTimeout(reject, 0); + + // Run temporary commit callbacks + var f; + while (f = this._callbacks.current.commit.shift()) { + yield Zotero.Promise.resolve(f()); + } + + // Run commit callbacks + for (var i=0; i<this._callbacks.commit.length; i++) { + if (this._callbacks.commit[i]) { + yield this._callbacks.commit[i](); } - - throw e; } + + setTimeout(resolve, 0); + + return result; } } + catch (e) { + Zotero.debug("Rolled back async DB transaction", 5); + Zotero.debug(e, 1); + + if (options) { + // Function to run once transaction has been committed but before any + // permanent callbacks + if (options.onRollback) { + this._callbacks.current.rollback.push(options.onRollback); + } + } + + // Run temporary commit callbacks + var f; + while (f = this._callbacks.current.rollback.shift()) { + yield Zotero.Promise.resolve(f()); + } + + // Run rollback callbacks + for (var i=0; i<this._callbacks.rollback.length; i++) { + if (this._callbacks.rollback[i]) { + yield Zotero.Promise.resolve(this._callbacks.rollback[i]()); + } + } + + if (reject) { + setTimeout(function () { + reject(e); + }, 0); + } + + throw e; + } finally { // Reset options back to their previous values if (options) { diff --git a/test/tests/dbTest.js b/test/tests/dbTest.js @@ -0,0 +1,83 @@ +describe("Zotero.DB", function() { + describe("#executeTransaction()", function () { + it("should roll back on error", function* () { + yield Zotero.DB.queryAsync("CREATE TABLE tmpRollbackOnError (foo INT)"); + yield Zotero.DB.queryAsync("INSERT INTO tmpRollbackOnError VALUES (1)"); + try { + yield Zotero.DB.executeTransaction(function* () { + yield Zotero.DB.queryAsync("INSERT INTO tmpRollbackOnError VALUES (2)"); + throw 'Aborting transaction -- ignore'; + }); + } + catch (e) { + if (typeof e != 'string' || !e.startsWith('Aborting transaction')) throw e; + } + var count = yield Zotero.DB.valueQueryAsync("SELECT COUNT(*) FROM tmpRollbackOnError"); + assert.equal(count, 1); + + var conn = yield Zotero.DB._getConnectionAsync(); + assert.isFalse(conn.transactionInProgress); + + yield Zotero.DB.queryAsync("DROP TABLE tmpRollbackOnError"); + }); + + it("should run onRollback callbacks", function* () { + var callbackRan = false; + yield Zotero.DB.queryAsync("CREATE TABLE tmpOnRollback (foo INT)"); + try { + yield Zotero.DB.executeTransaction( + function* () { + yield Zotero.DB.queryAsync("INSERT INTO tmpOnRollback VALUES (1)"); + throw 'Aborting transaction -- ignore'; + }, + { + onRollback: function () { + callbackRan = true; + } + } + ); + } + catch (e) { + if (typeof e != 'string' || !e.startsWith('Aborting transaction')) throw e; + } + assert.ok(callbackRan); + + yield Zotero.DB.queryAsync("DROP TABLE tmpOnRollback"); + }); + + it("should run onRollback callbacks for nested transactions", function* () { + var callback1Ran = false; + var callback2Ran = false; + yield Zotero.DB.queryAsync("CREATE TABLE tmpOnNestedRollback (foo INT)"); + try { + yield Zotero.DB.executeTransaction(function* () { + yield Zotero.DB.queryAsync("INSERT INTO tmpOnNestedRollback VALUES (1)"); + yield Zotero.DB.executeTransaction( + function* () { + yield Zotero.DB.queryAsync("INSERT INTO tmpOnNestedRollback VALUES (2)"); + + throw 'Aborting transaction -- ignore'; + }, + { + onRollback: function () { + callback1Ran = true; + } + } + ); + }, + { + onRollback: function () { + callback2Ran = true; + } + }); + } + catch (e) { + if (typeof e != 'string' || !e.startsWith('Aborting transaction')) throw e; + } + assert.ok(callback1Ran); + assert.ok(callback2Ran); + + yield Zotero.DB.queryAsync("DROP TABLE tmpOnNestedRollback"); + }); + }) +});