Skip to content

Commit b919a7e

Browse files
committed
feat: add ability to set per-request requestTimeout
Adds an optional overrides object that can be passed to Request, Transaction and PreparedStatement objects to provide per-instance config overrides. Currently supports requestTimeout. - Transaction overrides cascade to child requests unless overridden - PreparedStatement overrides apply to prepare, execute and unprepare - Validates timeout is a finite non-negative number - Fixes missing setTimeout in tedious stored procedure path (_execute) Closes #1529
1 parent cfea0bd commit b919a7e

File tree

9 files changed

+153
-22
lines changed

9 files changed

+153
-22
lines changed

README.md

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -793,11 +793,20 @@ pool.close()
793793
## Request
794794

795795
```javascript
796-
const request = new sql.Request(/* [pool or transaction] */)
796+
const request = new sql.Request(/* [pool or transaction], [options] */)
797797
```
798798

799799
If you omit pool/transaction argument, global pool is used instead.
800800

801+
The optional `options` argument allows per-request configuration overrides:
802+
803+
- **requestTimeout** - Override the pool's default request timeout (in ms) for this request only.
804+
805+
```javascript
806+
// Request with a 60-second timeout instead of the pool default
807+
const request = new sql.Request(pool, { requestTimeout: 60000 })
808+
```
809+
801810
### Events
802811

803812
- **recordset(columns)** - Dispatched when metadata for new recordset are parsed.
@@ -1118,11 +1127,13 @@ request.cancel()
11181127
**IMPORTANT:** always use `Transaction` class to create transactions - it ensures that all your requests are executed on one connection. Once you call `begin`, a single connection is acquired from the connection pool and all subsequent requests (initialized with the `Transaction` object) are executed exclusively on this connection. After you call `commit` or `rollback`, connection is then released back to the connection pool.
11191128

11201129
```javascript
1121-
const transaction = new sql.Transaction(/* [pool] */)
1130+
const transaction = new sql.Transaction(/* [pool], [options] */)
11221131
```
11231132

11241133
If you omit connection argument, global connection is used instead.
11251134

1135+
The optional `options` argument allows per-transaction configuration overrides (e.g. `{ requestTimeout: 60000 }`). These are inherited by any requests created from this transaction unless overridden at the request level. Note that the timeout applies to data requests only, not to the `begin`/`commit`/`rollback` operations themselves.
1136+
11261137
__Example__
11271138

11281139
```javascript
@@ -1270,11 +1281,13 @@ __Errors__
12701281
**IMPORTANT:** always use `PreparedStatement` class to create prepared statements - it ensures that all your executions of prepared statement are executed on one connection. Once you call `prepare`, a single connection is acquired from the connection pool and all subsequent executions are executed exclusively on this connection. After you call `unprepare`, the connection is then released back to the connection pool.
12711282

12721283
```javascript
1273-
const ps = new sql.PreparedStatement(/* [pool] */)
1284+
const ps = new sql.PreparedStatement(/* [pool], [options] */)
12741285
```
12751286

12761287
If you omit the connection argument, the global connection is used instead.
12771288

1289+
The optional `options` argument allows per-statement configuration overrides (e.g. `{ requestTimeout: 60000 }`). The timeout is applied to the `prepare`, `execute`, and `unprepare` operations.
1290+
12781291
__Example__
12791292

12801293
```javascript

lib/base/connection-pool.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -570,8 +570,8 @@ class ConnectionPool extends EventEmitter {
570570
* @return {Request}
571571
*/
572572

573-
request () {
574-
return new shared.driver.Request(this)
573+
request (conf) {
574+
return new shared.driver.Request(this, conf)
575575
}
576576

577577
/**
@@ -580,8 +580,8 @@ class ConnectionPool extends EventEmitter {
580580
* @return {Transaction}
581581
*/
582582

583-
transaction () {
584-
return new shared.driver.Transaction(this)
583+
transaction (conf) {
584+
return new shared.driver.Transaction(this, conf)
585585
}
586586

587587
/**

lib/base/prepared-statement.js

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,11 @@ class PreparedStatement extends EventEmitter {
2020
/**
2121
* Creates a new Prepared Statement.
2222
*
23-
* @param {ConnectionPool|Transaction} [holder]
23+
* @param {ConnectionPool|Transaction} [parent]
24+
* @param {{[requestTimeout]: number}} [overrides]
2425
*/
2526

26-
constructor (parent) {
27+
constructor (parent, overrides = {}) {
2728
super()
2829

2930
IDS.add(this, 'PreparedStatement')
@@ -33,6 +34,10 @@ class PreparedStatement extends EventEmitter {
3334
this._handle = 0
3435
this.prepared = false
3536
this.parameters = {}
37+
this.overrides = {}
38+
if (Number.isFinite(overrides?.requestTimeout) && overrides.requestTimeout >= 0) {
39+
this.overrides.requestTimeout = overrides.requestTimeout
40+
}
3641
}
3742

3843
get config () {
@@ -232,7 +237,7 @@ class PreparedStatement extends EventEmitter {
232237
this._acquiredConnection = connection
233238
this._acquiredConfig = config
234239

235-
const req = new shared.driver.Request(this)
240+
const req = new shared.driver.Request(this, this.overrides)
236241
req.stream = false
237242
req.output('handle', TYPES.Int)
238243
req.input('params', TYPES.NVarChar, ((() => {
@@ -294,7 +299,7 @@ class PreparedStatement extends EventEmitter {
294299
*/
295300

296301
_execute (values, callback) {
297-
const req = new shared.driver.Request(this)
302+
const req = new shared.driver.Request(this, this.overrides)
298303
req.stream = this.stream
299304
req.arrayRowMode = this.arrayRowMode
300305
req.input('handle', TYPES.Int, this._handle)
@@ -362,7 +367,7 @@ class PreparedStatement extends EventEmitter {
362367
return setImmediate(callback, new TransactionError("Can't unprepare the statement. There is a request in progress.", 'EREQINPROG'))
363368
}
364369

365-
const req = new shared.driver.Request(this)
370+
const req = new shared.driver.Request(this, this.overrides)
366371
req.stream = false
367372
req.input('handle', TYPES.Int, this._handle)
368373
req.execute('sp_unprepare', err => {

lib/base/request.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,11 @@ class Request extends EventEmitter {
2626
/**
2727
* Create new Request.
2828
*
29-
* @param {Connection|ConnectionPool|Transaction|PreparedStatement} parent If omitted, global connection is used instead.
29+
* @param {Connection|ConnectionPool|Transaction|PreparedStatement} [parent] If omitted, global connection is used instead.
30+
* @param {{[requestTimeout]: number}} [overrides]
3031
*/
3132

32-
constructor (parent) {
33+
constructor (parent, overrides) {
3334
super()
3435

3536
IDS.add(this, 'Request')
@@ -41,6 +42,10 @@ class Request extends EventEmitter {
4142
this.parameters = {}
4243
this.stream = null
4344
this.arrayRowMode = null
45+
this.overrides = {}
46+
if (Number.isFinite(overrides?.requestTimeout) && overrides.requestTimeout >= 0) {
47+
this.overrides.requestTimeout = overrides.requestTimeout
48+
}
4449
}
4550

4651
get paused () {

lib/base/transaction.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,10 @@ class Transaction extends EventEmitter {
2424
* Create new Transaction.
2525
*
2626
* @param {Connection} [parent] If ommited, global connection is used instead.
27+
* @param {{[requestTimeout]: number}} [overrides]
2728
*/
2829

29-
constructor (parent) {
30+
constructor (parent, overrides = {}) {
3031
super()
3132

3233
IDS.add(this, 'Transaction')
@@ -35,6 +36,10 @@ class Transaction extends EventEmitter {
3536
this.parent = parent || globalConnection.pool
3637
this.isolationLevel = Transaction.defaultIsolationLevel
3738
this.name = ''
39+
this.overrides = {}
40+
if (Number.isFinite(overrides?.requestTimeout) && overrides.requestTimeout >= 0) {
41+
this.overrides.requestTimeout = overrides.requestTimeout
42+
}
3843
}
3944

4045
get config () {
@@ -196,11 +201,12 @@ class Transaction extends EventEmitter {
196201
/**
197202
* Returns new request using this transaction.
198203
*
204+
* @param {{[requestTimeout]: number}} [config]
199205
* @return {Request}
200206
*/
201207

202-
request () {
203-
return new shared.driver.Request(this)
208+
request (config) {
209+
return new shared.driver.Request(this, { ...this.overrides, ...config })
204210
}
205211

206212
/**

lib/msnodesqlv8/request.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ class Request extends BaseRequest {
166166
setImmediate(callback, new RequestError("You can't use table variables for bulk insert.", 'ENAME'))
167167
}
168168

169-
this.parent.acquire(this, (err, connection) => {
169+
this.parent.acquire(this, (err, connection, config) => {
170170
let hasReturned = false
171171
if (!err) {
172172
debug('connection(%d): borrowed to request #%d', IDS.get(connection), IDS.get(this))
@@ -232,7 +232,10 @@ class Request extends BaseRequest {
232232
objectid = table.path
233233
}
234234

235-
return connection.queryRaw(`if object_id('${objectid.replace(/'/g, '\'\'')}') is null ${table.declare()}`, function (err) {
235+
return connection.queryRaw({
236+
query_str: `if object_id('${objectid.replace(/'/g, '\'\'')}') is null ${table.declare()}`,
237+
query_timeout: (this.overrides.requestTimeout ?? config.requestTimeout) / 1000 // msnodesqlv8 timeouts are in seconds (<1 second not supported),
238+
}, function (err) {
236239
if (err) { return done(err) }
237240
go()
238241
})
@@ -377,7 +380,7 @@ class Request extends BaseRequest {
377380

378381
const req = connection.queryRaw({
379382
query_str: command,
380-
query_timeout: config.requestTimeout / 1000 // msnodesqlv8 timeouts are in seconds (<1 second not supported)
383+
query_timeout: (this.overrides.requestTimeout ?? config.requestTimeout) / 1000 // msnodesqlv8 timeouts are in seconds (<1 second not supported)
381384
}, params)
382385

383386
this._setCurrentRequest(req)

lib/tedious/request.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,9 @@ class Request extends BaseRequest {
361361

362362
connection.execBulkLoad(bulk, table.rows)
363363
})
364+
if (typeof this.overrides.requestTimeout === 'number') {
365+
req.setTimeout(this.overrides.requestTimeout)
366+
}
364367
this._setCurrentRequest(req)
365368

366369
connection.execSqlBatch(req)
@@ -510,6 +513,10 @@ class Request extends BaseRequest {
510513
}
511514
})
512515

516+
if (typeof this.overrides.requestTimeout === 'number') {
517+
req.setTimeout(this.overrides.requestTimeout)
518+
}
519+
513520
this._setCurrentRequest(req)
514521

515522
req.on('columnMetadata', metadata => {
@@ -859,6 +866,10 @@ class Request extends BaseRequest {
859866
}
860867
})
861868

869+
if (typeof this.overrides.requestTimeout === 'number') {
870+
req.setTimeout(this.overrides.requestTimeout)
871+
}
872+
862873
this._setCurrentRequest(req)
863874

864875
req.on('columnMetadata', metadata => {

lib/tedious/transaction.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ const { IDS } = require('../utils')
66
const TransactionError = require('../error/transaction-error')
77

88
class Transaction extends BaseTransaction {
9-
constructor (parent) {
10-
super(parent)
9+
constructor (parent, overrides) {
10+
super(parent, overrides)
1111

1212
this._abort = () => {
1313
if (!this._rollbackRequested) {

test/common/unit.js

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -863,4 +863,92 @@ describe('connection string auth - tedious', () => {
863863
assert.strictEqual(result, 'UDT_StringArray readonly')
864864
})
865865
})
866+
867+
describe('per-request requestTimeout overrides', () => {
868+
const BaseRequest = require('../../lib/base/request')
869+
const BaseTransaction = require('../../lib/base/transaction')
870+
const BasePreparedStatement = require('../../lib/base/prepared-statement')
871+
872+
describe('Request', () => {
873+
it('stores valid requestTimeout override', () => {
874+
const req = new BaseRequest(null, { requestTimeout: 5000 })
875+
assert.strictEqual(req.overrides.requestTimeout, 5000)
876+
})
877+
878+
it('accepts zero as a valid timeout', () => {
879+
const req = new BaseRequest(null, { requestTimeout: 0 })
880+
assert.strictEqual(req.overrides.requestTimeout, 0)
881+
})
882+
883+
it('ignores NaN', () => {
884+
const req = new BaseRequest(null, { requestTimeout: NaN })
885+
assert.strictEqual(req.overrides.requestTimeout, undefined)
886+
})
887+
888+
it('ignores Infinity', () => {
889+
const req = new BaseRequest(null, { requestTimeout: Infinity })
890+
assert.strictEqual(req.overrides.requestTimeout, undefined)
891+
})
892+
893+
it('ignores negative values', () => {
894+
const req = new BaseRequest(null, { requestTimeout: -1 })
895+
assert.strictEqual(req.overrides.requestTimeout, undefined)
896+
})
897+
898+
it('ignores non-number values', () => {
899+
const req = new BaseRequest(null, { requestTimeout: '5000' })
900+
assert.strictEqual(req.overrides.requestTimeout, undefined)
901+
})
902+
903+
it('defaults to empty overrides when none provided', () => {
904+
const req = new BaseRequest(null)
905+
assert.deepStrictEqual(req.overrides, {})
906+
})
907+
})
908+
909+
describe('Transaction', () => {
910+
it('stores valid requestTimeout override', () => {
911+
const tx = new BaseTransaction(null, { requestTimeout: 10000 })
912+
assert.strictEqual(tx.overrides.requestTimeout, 10000)
913+
})
914+
915+
it('ignores invalid overrides', () => {
916+
const tx = new BaseTransaction(null, { requestTimeout: NaN })
917+
assert.strictEqual(tx.overrides.requestTimeout, undefined)
918+
})
919+
920+
it('cascades overrides to request when no per-request config given', () => {
921+
const pool = new ConnectionPool({ server: 'localhost' })
922+
const tx = pool.transaction({ requestTimeout: 10000 })
923+
const req = tx.request()
924+
assert.strictEqual(req.overrides.requestTimeout, 10000)
925+
})
926+
927+
it('per-request config overrides transaction overrides', () => {
928+
const pool = new ConnectionPool({ server: 'localhost' })
929+
const tx = pool.transaction({ requestTimeout: 10000 })
930+
const req = tx.request({ requestTimeout: 3000 })
931+
assert.strictEqual(req.overrides.requestTimeout, 3000)
932+
})
933+
934+
it('per-request config merges with transaction overrides', () => {
935+
const pool = new ConnectionPool({ server: 'localhost' })
936+
const tx = pool.transaction({ requestTimeout: 10000 })
937+
const req = tx.request({})
938+
assert.strictEqual(req.overrides.requestTimeout, 10000)
939+
})
940+
})
941+
942+
describe('PreparedStatement', () => {
943+
it('stores valid requestTimeout override', () => {
944+
const ps = new BasePreparedStatement(null, { requestTimeout: 8000 })
945+
assert.strictEqual(ps.overrides.requestTimeout, 8000)
946+
})
947+
948+
it('ignores invalid overrides', () => {
949+
const ps = new BasePreparedStatement(null, { requestTimeout: -100 })
950+
assert.strictEqual(ps.overrides.requestTimeout, undefined)
951+
})
952+
})
953+
})
866954
})

0 commit comments

Comments
 (0)