msnodesqlv8
Version:
Microsoft Driver for Node.js SQL Server compatible with all versions of Node.
856 lines (753 loc) • 26.9 kB
JavaScript
'use strict'
const poolModule = (() => {
const util = require('util')
const { EventEmitter } = require('stream')
const { procedureModule } = require('./procedure')
const { driverModule } = require('./driver')
const sqlClientModule = require('./sql-client').sqlCLientModule
const { notifyModule } = require('./notifier')
const { utilModule } = require('./util')
const { tableModule } = require('./table')
const userModule = require('./user').userModule
const { metaModule } = require('./meta')
const cppDriver = new utilModule.Native().cppDriver
class PoolWorkItem {
constructor (id, sql, paramsOrCallback, callback, poolNotifier, workType, chunky) {
this.id = id
this.sql = sql
this.paramsOrCallback = paramsOrCallback
this.callback = callback
this.poolNotifier = poolNotifier
this.workType = workType
this.chunky = chunky
}
}
class PoolDscription {
constructor (id, pool, connection) {
this.id = id
this.pool = pool
this.connection = connection
this.heartbeatSqlResponse = null
this.lastActive = new Date()
this.work = null
this.keepAliveCount = 0
this.recreateCount = 0
this.parkedCount = 0
this.queriesSent = 0
this.beganAt = null
this.totalElapsedQueryMs = 0
}
begin () {
const now = new Date()
this.beganAt = now
this.lastActive = now
this.keepAliveCount = 0
this.queriesSent++
}
free () {
this.totalElapsedQueryMs += new Date() - this.beganAt
}
heartbeatResponse (v) {
this.heartbeatSqlResponse = v
}
heartbeat () {
this.keepAliveCount++ // reset by user query
this.lastActive = new Date()
}
assignConnection (c) {
this.connection = c
this.work = null
this.heartbeatSqlResponse = null
this.lastActive = new Date()
this.keepAliveCount = 0
}
recreate (conn) {
this.connection = conn
this.lastActive = new Date()
this.heartbeatSqlResponse = null
this.recreateCount++
}
park () {
this.connection = null
this.parkedCount++
this.keepAliveCount = 0
this.beganAt = null
}
}
class PoolEventCaster extends EventEmitter {
constructor () {
super()
this.queryObj = null
this.paused = false
this.pendingCancel = false
}
isPaused () {
return this.paused
}
getQueryObj () {
return this.queryObj
}
getQueryId () {
return this.queryObj ?? -1
}
isPendingCancel () {
return this.pendingCancel
}
cancelQuery (cb) {
if (this.queryObj) {
this.queryObj.cancelQuery(cb)
} else {
this.pendingCancel = true
setImmediate(() => {
if (cb) {
cb()
}
})
}
}
pauseQuery () {
this.paused = true
if (this.queryObj) {
this.queryObj.pauseQuery()
}
}
resumeQuery () {
this.paused = false
if (this.queryObj) {
this.queryObj.resumeQuery()
}
}
setQueryObj (q, chunky) {
this.queryObj = q
q.on('submitted', (d) => {
this.emit('submitted', d)
})
if (!chunky.callback) {
q.on('error', (e, more) => {
if (this.listenerCount('error') > 0) {
this.emit('error', e, more)
}
})
}
q.on('done', (r) => {
this.emit('done', r)
})
q.on('row', (r) => {
this.emit('row', r)
})
q.on('column', (i, v) => {
this.emit('column', i, v)
})
q.on('meta', (m) => {
this.emit('meta', m)
})
q.on('info', (e) => {
this.emit('info', e)
})
q.on('output', (e) => {
this.emit('output', e)
})
}
isPrepared () {
return false
}
}
class PoolOptions {
constructor (opt) {
this.floor = Math.max(0, this.getOpt(opt, 'floor', 0))
this.ceiling = Math.max(1, this.getOpt(opt, 'ceiling', 4))
this.heartbeatSecs = Math.max(1, this.getOpt(opt, 'heartbeatSecs', 20))
this.heartbeatSql = this.getOpt(opt, 'heartbeatSql', 'select @@SPID as spid')
this.inactivityTimeoutSecs = Math.max(3, this.getOpt(opt, 'inactivityTimeoutSecs', 60))
this.connectionString = this.getOpt(opt, 'connectionString', '')
this.useUTC = this.getOpt(opt, 'useUTC', null)
this.useNumericString = this.getOpt(opt, 'useNumericString', null)
this.maxPreparedColumnSize = this.getOpt(opt, 'maxPreparedColumnSize', null)
this.floor = Math.min(this.floor, this.ceiling)
this.inactivityTimeoutSecs = Math.max(this.inactivityTimeoutSecs, this.heartbeatSecs)
}
getOpt (src, p, def) {
if (!src) {
return def
}
let ret
if (Object.hasOwnProperty.call(src, p)) {
ret = src[p]
} else {
ret = def
}
return ret
}
}
class PoolPromises {
constructor (pool) {
this.pool = pool
this.open = util.promisify(pool.open)
this.close = util.promisify(pool.close)
this.query = pool.queryAggregator
this.callProc = pool.callprocAggregator
this.getUserTypeTable = pool.getUserTypeTable
this.getTable = pool.getTable
this.getProc = pool.getProc
this.beginTransaction = util.promisify(pool.beginTransaction)
this.commitTransaction = util.promisify(pool.commitTransaction)
this.rollbackTransaction = util.promisify(pool.rollbackTransaction)
}
transaction(cb) {
let connectionDescription
return this.beginTransaction()
.then((description) => cb(connectionDescription = description))
.then(
() => this.commitTransaction(connectionDescription),
err => {
// If no connectionDescription, do nothing, the beginTransaction errored
// and we can report it directly.
if (!connectionDescription) { return Promise.reject(err) }
// Error in cb() we should notify about it
if (this.pool.listenerCount('error') > 0) {
this.pool.emit('error', err)
}
return this.rollbackTransaction(connectionDescription)
.catch((rollbackError) => {
// We encountered error during rollback, emit an error on the pool for it
if (this.pool.listenerCount('error') > 0) {
this.pool.emit('error', rollbackError)
}
})
.then(
() => {
// Return the original error regardless if rollback was
// successful or not.
return Promise.reject(err)
},
)
}
)
}
}
class Pool extends EventEmitter {
constructor (opt) {
super()
const clientPromises = sqlClientModule.promises
const idle = []
const parked = []
const workQueue = []
const pause = []
let busyConnectionCount = 0
let parkingConnectionCount = 0
let opened = false
let hbTimer = null
let pollTimer = null
const _this = this
let descriptionId = 0
let commandId = 0
let pendingCreates = 0
let closed = false
const heartbeatTickMs = 250
const notifierFactory = new notifyModule.NotifyFactory()
const poolProcedureCache = {}
const poolTableCache = {}
const aggregator = new utilModule.QueryAggregator(this)
const userTypes = new userModule.SqlTypes()
const sqlMeta = new metaModule.Meta()
const native = new cppDriver.Connection()
const driverMgr = new driverModule.DriverMgr(native)
const tableMgr = new tableModule.TableMgr(this, sqlMeta, userTypes, poolTableCache)
const procedureManager = new procedureModule.ProcedureMgr(this, notifierFactory, driverMgr, sqlMeta, poolProcedureCache)
const closedError = new Error('pool is closed.')
function parseOptions () {
return new PoolOptions(opt)
}
const options = parseOptions()
function getUseUTC () {
return options.useUTC
}
function setUseUTC (utc) {
options.useUTC = utc
}
function newDescription (c) {
return new PoolDscription(descriptionId++, this, c)
}
function parkedDescription (c) {
if (parked.length > 0) {
const d = parked.pop()
d.assignConnection(c)
return d
} else {
return null
}
}
function getDescription (c) {
return parkedDescription(c) || newDescription(c)
}
function runTheQuery (q, description, work) {
let errored = false
work.poolNotifier.setQueryObj(q, work.chunky)
q.on('submitted', () => {
_this.emit('debug', `[${description.id}] submitted work id ${work.id}`)
_this.emit('submitted', q)
description.work = work
setImmediate(() => {
crank()
})
})
q.on('free', () => {
description.free()
// Transactions can not be freed yet if no errors occured. They need to be freed later
if (!errored && description.work && description.work.workType === workTypeEnum.TRANSACTION) {
_this.emit('debug', `[${description.id}] inside transaction from work id ${work.id}`)
return
}
checkin('work', description)
_this.emit('debug', `[${description.id}] free work id ${work.id}`)
work.poolNotifier.emit('free')
setImmediate(() => {
crank()
})
})
q.on('error', (e, more) => {
errored = true
sendError(e, more)
setImmediate(() => {
crank()
})
})
}
function getTheQuery (description, work) {
let q = null
const connection = description.connection
switch (work.workType) {
case workTypeEnum.QUERY:
q = connection.query(work.sql, work.paramsOrCallback, work.callback)
break
case workTypeEnum.RAW:
case workTypeEnum.COMMITTING:
q = connection.queryRaw(work.sql, work.paramsOrCallback, work.callback)
break
case workTypeEnum.PROC:
q = connection.callproc(work.sql, work.paramsOrCallback, work.callback)
break
case workTypeEnum.TRANSACTION:
q = connection.queryRaw(work.sql, work.paramsOrCallback, function (err) {
work.callback(err, err ? null : description)
})
break
}
return q
}
function item (description, work) {
description.begin()
_this.emit('debug', `[${description.id}] query work id = ${work.id}, workQueue = ${workQueue.length}`)
const q = getTheQuery(description, work)
if (q) {
runTheQuery(q, description, work)
}
}
function doneFree (poolNotifier) {
poolNotifier.emit('done')
poolNotifier.emit('free')
}
/** Move unpaused items to queue */
function promotePause () {
const start = pause.length
for (let i = 0; i < pause.length; i++) {
if (!pause[i].isPaused) {
workQueue.push(pause.splice(i, 1)[0])
i--
}
}
if (start !== pause.length) {
setImmediate(() => { crank() })
}
}
function poll () {
if (pause.length + workQueue.length > 0) {
crank()
}
}
function crank () {
if (closed) {
return
}
void grow().then(() => {
promotePause()
while (workQueue.length > 0 && idle.length > 0) {
const work = workQueue.pop()
if (work.poolNotifier.isPendingCancel()) {
_this.emit('debug', `query work id = ${work.id} has been cancelled waiting in pool to execute, workQueue = ${workQueue.length}`)
doneFree(work.poolNotifier)
} else if (work.poolNotifier.isPaused()) {
pause.unshift(work)
} else {
const description = checkout('work')
item(description, work)
}
}
})
}
const workTypeEnum = {
QUERY: 10,
RAW: 11,
PROC: 12,
TRANSACTION: 13,
COMMITTING: 14,
}
function chunk (paramsOrCallback, callback, workType) {
switch (workType) {
case workTypeEnum.QUERY:
case workTypeEnum.RAW:
case workTypeEnum.COMMITTING:
return notifierFactory.getChunkyArgs(paramsOrCallback, callback)
case workTypeEnum.PROC:
case workTypeEnum.TRANSACTION:
return { params: paramsOrCallback, callback }
}
}
function newWorkItem (sql, paramsOrCallback, callback, notifier, workType) {
return new PoolWorkItem(commandId++, sql, paramsOrCallback, callback, notifier, workType, chunk(paramsOrCallback, callback, workType))
}
async function checkClosedPromise () {
return new Promise((resolve, reject) => {
if (closed) {
reject(closedError)
} else {
resolve(null)
}
})
}
function submit (sql, paramsOrCallback, callback, type) {
const notifier = new PoolEventCaster()
const work = newWorkItem(sql, paramsOrCallback, callback, notifier, type)
if (!closed) {
enqueue(work)
} else {
if (work.chunky.callback) {
setImmediate(() => {
work.chunky.callback(closedError)
})
} else {
sendError(closedError)
setImmediate(() => {
notifier.emit('error', closedError)
doneFree(notifier)
})
}
}
return notifier
}
function query (sql, paramsOrCallback, callback) {
return submit(sql, paramsOrCallback, callback, workTypeEnum.QUERY)
}
function queryRaw (sql, paramsOrCallback, callback) {
return submit(sql, paramsOrCallback, callback, workTypeEnum.RAW)
}
function callproc (sql, paramsOrCallback, callback) {
return submit(sql, paramsOrCallback, callback, workTypeEnum.PROC)
}
function beginTransaction (callback) {
if (!callback || typeof callback !== 'function') {
throw new Error('[msnodesql] Pool beginTransaction called with empty callback.')
}
return submit('BEGIN TRANSACTION', [], callback, workTypeEnum.TRANSACTION)
}
function finishTransaction(sql, description, callback) {
if (!description instanceof PoolDscription) {
throw new Error('[msnodesql] Pool end transaction called with non-description.')
}
const work = description.work
if (!work) {
throw new Error('[msnodesql] Pool end transaction called with unknown or finished transaction.')
}
if (work.workType !== workTypeEnum.TRANSACTION && work.workType !== workTypeEnum.COMMITTING) {
throw new Error('[msnodesql] Pool end transaction called with unknown or finished transaction.')
}
_this.emit('debug', `[${description.id}] closing transaction from ${work.id} with ${sql}`)
work.callback = callback
work.sql = sql
work.workType = workTypeEnum.COMMITTING
item(description, work)
return work.poolNotifier
}
function commitTransaction (description, callback) {
return finishTransaction('IF (@@TRANCOUNT > 0) COMMIT TRANSACTION', description, callback)
}
function rollbackTransaction (description, callback) {
return finishTransaction('IF (@@TRANCOUNT > 0) ROLLBACK TRANSACTION', description, callback)
}
async function getUserTypeTable (name) {
// the table mgr will submit query into pool as if it's a connection
return checkClosedPromise().then(async () => tableMgr.promises.getUserTypeTable(name))
}
async function getTable (name) {
return checkClosedPromise().then(async () => tableMgr.promises.getTable(name))
}
async function getProc (name) {
return checkClosedPromise().then(async () => procedureManager.promises.getProc(name))
}
// returns a promise of aggregated results not a query
async function callprocAggregator (name, params, options) {
return checkClosedPromise().then(async () => aggregator.callProc(name, params, options))
}
async function queryAggregator (sql, params, options) {
return checkClosedPromise().then(async () => aggregator.query(sql, params, options))
}
function enqueue (item) {
if (closed) {
return
}
workQueue.unshift(item)
if (opened) {
setImmediate(() => {
crank()
})
}
}
function getStatus (work, activity, op) {
const s = {
time: new Date(),
parked: parked.length,
idle: idle.length,
busy: busyConnectionCount,
pause: pause.length,
parking: parkingConnectionCount,
workQueue: workQueue.length,
activity,
op
}
if (work) {
s.lastSql = work.sql
s.lastParams = work.chunky.params
}
return s
}
function checkin (activity, description) {
if (closed) {
return
}
idle.unshift(description)
if (busyConnectionCount > 0) {
busyConnectionCount--
}
_this.emit('status', getStatus(description.work, activity, 'checkin'))
description.work = null
_this.emit('debug', `[${description.id}] checkin idle = ${idle.length}, parking = ${parkingConnectionCount}, parked = ${parked.length}, busy = ${busyConnectionCount}, pause = ${pause.length}, workQueue = ${workQueue.length}`)
}
function checkout (activity) {
if (idle.length === 0) {
return null
}
const description = idle.pop()
busyConnectionCount++
_this.emit('status', getStatus(null, activity, 'checkout'))
_this.emit('debug', `[${description.id}] checkout idle = ${idle.length}, parking = ${parkingConnectionCount}, parked = ${parked.length}, busy = ${busyConnectionCount}, pause = ${pause.length}, workQueue = ${workQueue.length}`)
return description
}
async function grow () {
if (closed) {
return
}
const existing = idle.length + busyConnectionCount + pendingCreates + parkingConnectionCount
if (existing === options.ceiling) {
return
}
function connectionOptions (c) {
c.setSharedCache(poolProcedureCache, poolTableCache)
if (options.maxPreparedColumnSize) {
c.setMaxPreparedColumnSize(options.maxPreparedColumnSize)
}
if (options.useUTC === true || options.useUTC === false) {
c.setUseUTC(options.useUTC)
}
if (options.useNumericString === true || options.useNumericString === false) {
c.setUseNumericString(options.useNumericString)
}
}
const toPromise = []
for (let i = existing; i < options.ceiling; ++i) {
++pendingCreates
toPromise.push(clientPromises.open(options.connectionString)
.then(
c => {
--pendingCreates
connectionOptions(c)
checkin('grow', getDescription(c))
},
async e => {
--pendingCreates
return Promise.reject(e)
}
)
)
}
const res = await Promise.all(toPromise)
_this.emit('debug', `grow creates ${res.length} connections for pool idle = ${idle.length}, busy = ${busyConnectionCount}, pending = ${pendingCreates}, parkingConnectionCount = ${parkingConnectionCount}, existing = ${existing}`)
}
function open (cb) {
if (opened) {
return
}
grow().then(() => {
if (cb) {
cb(null, options)
}
if (options.heartbeatSecs) {
hbTimer = setInterval(() => {
park()
heartbeat()
}, heartbeatTickMs, _this)
crank()
}
pollTimer = setInterval(() => {
poll()
}, 200, _this)
opened = true
_this.emit('open', options)
}).catch(e => {
if (cb) {
cb(e, null)
}
sendError(e)
})
}
function park () {
const toParkIndex = idle.findIndex(description => {
const inactivePeriod = description.keepAliveCount * options.heartbeatSecs
return inactivePeriod >= options.inactivityTimeoutSecs
})
if (toParkIndex === -1) {
return
}
const description = idle[toParkIndex]
if (parkDescription(description)) {
idle.splice(toParkIndex, 1)
}
}
function promoteToFront (index) {
if (index < 0 || index >= idle.length) {
return
}
const description = idle[index]
idle.splice(index, 1)
idle.push(description)
}
function sendError (e, more) {
if (_this.listenerCount('error') > 0) {
_this.emit('error', e, more)
}
}
function heartbeat () {
const toHeartBeatIndex = idle.findIndex(d => new Date() - d.lastActive >= options.heartbeatSecs * 1000)
if (toHeartBeatIndex === -1) {
return
}
promoteToFront(toHeartBeatIndex)
const description = checkout('heartbeat')
const q = description.connection.query(options.heartbeatSql)
q.on('column', (i, v) => {
description.heartbeatResponse(v)
})
q.on('done', () => {
description.heartbeat() // reset by user query
checkin('heartbeat', description)
const inactivePeriod = description.keepAliveCount * options.heartbeatSecs
_this.emit('debug', `[${description.id}] heartbeat response = '${description.heartbeatSqlResponse}', ${description.lastActive.toLocaleTimeString()}` +
`, keepAliveCount = ${description.keepAliveCount} inactivePeriod = ${inactivePeriod}, inactivityTimeoutSecs = ${options.inactivityTimeoutSecs}`)
})
q.on('error', (e) => {
sendError(e)
recreate(description)
})
}
function parkDescription (description) {
// need to leave at least floor connections in idle pool
const canPark = Math.max(0, idle.length - options.floor)
if (canPark === 0) {
return false
}
_this.emit('debug', `[${description.id}] close connection and park due to inactivity parked = ${parked.length}, canPark = ${canPark}`)
parkingConnectionCount++
const connPromises = description.connection.promises
connPromises.close().then(() => {
parkingConnectionCount--
description.park()
parked.unshift(description)
_this.emit('debug', `[${description.id}] closed connection and park due to inactivity parked = ${parked.length}, idle = ${idle.length}, busy = ${busyConnectionCount}`)
_this.emit('status', getStatus(null, 'parked', 'parked'))
}).catch(e => {
sendError(e)
})
return true
}
function recreate (description) {
_this.emit('debug', `recreate connection [${description.id}]`)
const toPromise = []
if (description.connection) {
const promisedClose = description.connection.promises.close()
toPromise.push(promisedClose)
}
void Promise.all(toPromise).then(() => {
clientPromises.open(options.connectionString).then(conn => {
description.recreate(conn)
checkin('recreate', description)
}).catch(e => {
sendError(e)
})
})
}
function isClosed () {
return closed
}
function close (cb) {
if (hbTimer) {
clearInterval(hbTimer)
}
if (pollTimer) {
clearInterval(pollTimer)
}
// any parked connection will have been closed
while (parked.length > 0) {
parked.pop()
}
while (workQueue.length > 0) {
workQueue.pop()
}
const toClosePromise = idle.map(description => description.connection.promises.close)
Promise.all(toClosePromise).then(res => {
_this.emit('debug', `closed ${res.length} connections due to pool shutdown busy = ${busyConnectionCount}`)
_this.emit('close')
if (cb) {
cb()
}
}).catch(e => {
if (cb) {
cb()
}
sendError(e)
}).finally(
closed = true
)
}
this.beginTransaction = beginTransaction
this.commitTransaction = commitTransaction
this.rollbackTransaction = rollbackTransaction
this.open = open
this.close = close
this.query = query
this.queryRaw = queryRaw
this.callproc = callproc
this.callprocAggregator = callprocAggregator
this.getUserTypeTable = getUserTypeTable
this.getTable = getTable
this.getProc = getProc
this.queryAggregator = queryAggregator
this.promises = new PoolPromises(this)
this.getUseUTC = getUseUTC
this.setUseUTC = setUseUTC
this.isClosed = isClosed
}
}
return {
Pool
}
})()
exports.poolModule = poolModule