-
Notifications
You must be signed in to change notification settings - Fork 2.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
WIP: Experiment/inject transaction connection #3708
base: master
Are you sure you want to change the base?
Changes from 6 commits
177f6a7
9a2054d
e53ca3c
d8d7089
b3788bd
f55aec1
25e0f51
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,6 +19,11 @@ const { | |
chunk, | ||
} = require('lodash'); | ||
|
||
const ReuseConnection = (connection) => | ||
async function(next) { | ||
return next(connection); | ||
}; | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FYI: This function is currently duplicated in a few places. We can consolidate it as part of the re-implementation. |
||
// So altering the schema in SQLite3 is a major pain. | ||
// We have our own object to deal with the renaming and altering the types | ||
// for sqlite3 things. | ||
|
@@ -282,7 +287,7 @@ assign(SQLite3_DDL.prototype, { | |
return omit(row, mappedFrom); | ||
}); | ||
}, | ||
{ connection: this.connection } | ||
{ withConnection: ReuseConnection(this.connection) } | ||
); | ||
}, | ||
|
||
|
@@ -312,7 +317,7 @@ assign(SQLite3_DDL.prototype, { | |
); | ||
}); | ||
}, | ||
{ connection: this.connection } | ||
{ withConnection: ReuseConnection(this.connection) } | ||
); | ||
}, | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,11 +7,23 @@ const makeKnex = require('./util/make-knex'); | |
const { callbackify } = require('util'); | ||
const { timeout, KnexTimeoutError } = require('./util/timeout'); | ||
const finallyMixin = require('./util/finally-mixin'); | ||
const pipe = require('./util/pipe'); | ||
|
||
const debug = Debug('knex:tx'); | ||
|
||
const { uniqueId, isUndefined } = require('lodash'); | ||
|
||
const ReuseConnection = (connection) => | ||
async function(next) { | ||
return next(connection); | ||
}; | ||
|
||
const InjectTransactionID = (txid) => (next) => | ||
async function(connection) { | ||
connection.__knexTxId = txid; | ||
return next(connection); | ||
}; | ||
|
||
// FYI: This is defined as a function instead of a constant so that | ||
// each Transactor can have its own copy of the default config. | ||
// This will minimize the impact of bugs that might be introduced | ||
|
@@ -163,79 +175,69 @@ class Transaction extends EventEmitter { | |
// Wait for the earlier Transactions to complete before proceeding. | ||
await this._previousSibling; | ||
|
||
return this.acquireConnection(config, (connection) => { | ||
const trxClient = (this.trxClient = makeTxClient( | ||
this, | ||
this.client, | ||
connection | ||
)); | ||
const init = this.client.transacting | ||
? this.savepoint(connection) | ||
: this.begin(connection); | ||
const executionPromise = new Promise((resolver, rejecter) => { | ||
this._resolver = resolver; | ||
this._rejecter = rejecter; | ||
}); | ||
|
||
init | ||
.then(() => { | ||
return makeTransactor(this, connection, trxClient); | ||
}) | ||
.then((transactor) => { | ||
transactor.executionPromise = executionPromise; | ||
|
||
// If we've returned a "thenable" from the transaction container, assume | ||
// the rollback and commit are chained to this object's success / failure. | ||
// Directly thrown errors are treated as automatic rollbacks. | ||
let result; | ||
try { | ||
result = container(transactor); | ||
} catch (err) { | ||
result = Promise.reject(err); | ||
} | ||
if (result && result.then && typeof result.then === 'function') { | ||
result | ||
.then((val) => { | ||
return transactor.commit(val); | ||
}) | ||
.catch((err) => { | ||
return transactor.rollback(err); | ||
}); | ||
} | ||
return null; | ||
}) | ||
.catch((e) => { | ||
return this._rejecter(e); | ||
const { withConnection } = config; | ||
|
||
const withEverything = pipe([ | ||
withConnection, | ||
InjectTransactionID(this.txid), | ||
]); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure if I'll keep the |
||
|
||
return withEverything((conn) => { | ||
return this.acquireConnection(conn, (connection) => { | ||
const trxClient = (this.trxClient = makeTxClient( | ||
this, | ||
this.client, | ||
connection | ||
)); | ||
const init = this.client.transacting | ||
? this.savepoint(connection) | ||
: this.begin(connection); | ||
const executionPromise = new Promise((resolver, rejecter) => { | ||
this._resolver = resolver; | ||
this._rejecter = rejecter; | ||
}); | ||
|
||
return executionPromise; | ||
init | ||
.then(() => { | ||
return makeTransactor(this, connection, trxClient); | ||
}) | ||
.then((transactor) => { | ||
transactor.executionPromise = executionPromise; | ||
|
||
// If we've returned a "thenable" from the transaction container, assume | ||
// the rollback and commit are chained to this object's success / failure. | ||
// Directly thrown errors are treated as automatic rollbacks. | ||
let result; | ||
try { | ||
result = container(transactor); | ||
} catch (err) { | ||
result = Promise.reject(err); | ||
} | ||
if (result && result.then && typeof result.then === 'function') { | ||
result | ||
.then((val) => { | ||
return transactor.commit(val); | ||
}) | ||
.catch((err) => { | ||
return transactor.rollback(err); | ||
}); | ||
} | ||
return null; | ||
}) | ||
.catch((e) => { | ||
return this._rejecter(e); | ||
}); | ||
|
||
return executionPromise; | ||
}); | ||
}); | ||
} | ||
|
||
// Acquire a connection and create a disposer - either using the one passed | ||
// via config or getting one off the client. The disposer will be called once | ||
// the original promise is marked completed. | ||
acquireConnection(config, cb) { | ||
const configConnection = config && config.connection; | ||
return new Promise((resolve, reject) => { | ||
try { | ||
resolve(configConnection || this.client.acquireConnection()); | ||
} catch (e) { | ||
reject(e); | ||
} | ||
}).then(async (connection) => { | ||
try { | ||
connection.__knexTxId = this.txid; | ||
return await cb(connection); | ||
} finally { | ||
if (!configConnection) { | ||
debug('%s: releasing connection', this.txid); | ||
this.client.releaseConnection(connection); | ||
} else { | ||
debug('%s: not releasing external connection', this.txid); | ||
} | ||
} | ||
}); | ||
async acquireConnection(connection, cb) { | ||
return cb(connection); | ||
} | ||
|
||
then(onResolve, onReject) { | ||
|
@@ -269,13 +271,16 @@ function makeTransactor(trx, connection, trxClient) { | |
transactor.isTransaction = true; | ||
transactor.userParams = trx.userParams || {}; | ||
|
||
transactor.transaction = function(container, options) { | ||
if (!options) { | ||
options = { doNotRejectOnRollback: true }; | ||
} else if (isUndefined(options.doNotRejectOnRollback)) { | ||
transactor.transaction = function(container, _config) { | ||
const options = Object.assign({}, _config); | ||
if (isUndefined(options.doNotRejectOnRollback)) { | ||
options.doNotRejectOnRollback = true; | ||
} | ||
|
||
if (isUndefined(options.withConnection)) { | ||
options.withConnection = ReuseConnection(connection); | ||
} | ||
|
||
if (container) { | ||
return trxClient.transaction(container, options, trx); | ||
} else { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
const pipe = (decorators) => (next) => | ||
Array.from(decorators) | ||
.reverse() | ||
.reduce((_next, d) => d(_next), next); | ||
|
||
module.exports = exports = pipe; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops -- I thought I deleted this part already. I'll fix this in a little while. (I currently have the tests running on loop so that we can detect unhandled promise rejections)