Skip to content

Commit 1211549

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 1b815b1 commit 1211549

9 files changed

Lines changed: 153 additions & 22 deletions

File tree

README.md

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

821821
```javascript
822-
const request = new sql.Request(/* [pool or transaction] */)
822+
const request = new sql.Request(/* [pool or transaction], [options] */)
823823
```
824824

825825
If you omit pool/transaction argument, global pool is used instead.
826826

827+
The optional `options` argument allows per-request configuration overrides:
828+
829+
- **requestTimeout** - Override the pool's default request timeout (in ms) for this request only.
830+
831+
```javascript
832+
// Request with a 60-second timeout instead of the pool default
833+
const request = new sql.Request(pool, { requestTimeout: 60000 })
834+
```
835+
827836
### Events
828837

829838
- **recordset(columns)** - Dispatched when metadata for new recordset are parsed.
@@ -1144,11 +1153,13 @@ request.cancel()
11441153
**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.
11451154

11461155
```javascript
1147-
const transaction = new sql.Transaction(/* [pool] */)
1156+
const transaction = new sql.Transaction(/* [pool], [options] */)
11481157
```
11491158

11501159
If you omit connection argument, global connection is used instead.
11511160

1161+
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.
1162+
11521163
__Example__
11531164

11541165
```javascript
@@ -1296,11 +1307,13 @@ __Errors__
12961307
**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.
12971308

12981309
```javascript
1299-
const ps = new sql.PreparedStatement(/* [pool] */)
1310+
const ps = new sql.PreparedStatement(/* [pool], [options] */)
13001311
```
13011312

13021313
If you omit the connection argument, the global connection is used instead.
13031314

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

13061319
```javascript

lib/base/connection-pool.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -579,8 +579,8 @@ class ConnectionPool extends EventEmitter {
579579
* @return {Request}
580580
*/
581581

582-
request () {
583-
return new shared.driver.Request(this)
582+
request (conf) {
583+
return new shared.driver.Request(this, conf)
584584
}
585585

586586
/**
@@ -589,8 +589,8 @@ class ConnectionPool extends EventEmitter {
589589
* @return {Transaction}
590590
*/
591591

592-
transaction () {
593-
return new shared.driver.Transaction(this)
592+
transaction (conf) {
593+
return new shared.driver.Transaction(this, conf)
594594
}
595595

596596
/**

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
@@ -172,7 +172,7 @@ class Request extends BaseRequest {
172172
setImmediate(callback, new RequestError("You can't use table variables for bulk insert.", 'ENAME'))
173173
}
174174

175-
this.parent.acquire(this, (err, connection) => {
175+
this.parent.acquire(this, (err, connection, config) => {
176176
let hasReturned = false
177177
if (!err) {
178178
debug('connection(%d): borrowed to request #%d', IDS.get(connection), IDS.get(this))
@@ -244,7 +244,10 @@ class Request extends BaseRequest {
244244
objectid = table.path
245245
}
246246

247-
return connection.queryRaw(`if object_id('${objectid.replace(/'/g, '\'\'')}') is null ${table.declare()}`, function (err) {
247+
return connection.queryRaw({
248+
query_str: `if object_id('${objectid.replace(/'/g, '\'\'')}') is null ${table.declare()}`,
249+
query_timeout: (this.overrides.requestTimeout ?? config.requestTimeout) / 1000 // msnodesqlv8 timeouts are in seconds (<1 second not supported),
250+
}, function (err) {
248251
if (err) { return done(err) }
249252
go()
250253
})
@@ -389,7 +392,7 @@ class Request extends BaseRequest {
389392

390393
const req = connection.queryRaw({
391394
query_str: command,
392-
query_timeout: config.requestTimeout / 1000 // msnodesqlv8 timeouts are in seconds (<1 second not supported)
395+
query_timeout: (this.overrides.requestTimeout ?? config.requestTimeout) / 1000 // msnodesqlv8 timeouts are in seconds (<1 second not supported)
393396
}, params)
394397

395398
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
@@ -966,4 +966,92 @@ describe('connection string auth - tedious', () => {
966966
})
967967
})
968968
})
969+
970+
describe('per-request requestTimeout overrides', () => {
971+
const BaseRequest = require('../../lib/base/request')
972+
const BaseTransaction = require('../../lib/base/transaction')
973+
const BasePreparedStatement = require('../../lib/base/prepared-statement')
974+
975+
describe('Request', () => {
976+
it('stores valid requestTimeout override', () => {
977+
const req = new BaseRequest(null, { requestTimeout: 5000 })
978+
assert.strictEqual(req.overrides.requestTimeout, 5000)
979+
})
980+
981+
it('accepts zero as a valid timeout', () => {
982+
const req = new BaseRequest(null, { requestTimeout: 0 })
983+
assert.strictEqual(req.overrides.requestTimeout, 0)
984+
})
985+
986+
it('ignores NaN', () => {
987+
const req = new BaseRequest(null, { requestTimeout: NaN })
988+
assert.strictEqual(req.overrides.requestTimeout, undefined)
989+
})
990+
991+
it('ignores Infinity', () => {
992+
const req = new BaseRequest(null, { requestTimeout: Infinity })
993+
assert.strictEqual(req.overrides.requestTimeout, undefined)
994+
})
995+
996+
it('ignores negative values', () => {
997+
const req = new BaseRequest(null, { requestTimeout: -1 })
998+
assert.strictEqual(req.overrides.requestTimeout, undefined)
999+
})
1000+
1001+
it('ignores non-number values', () => {
1002+
const req = new BaseRequest(null, { requestTimeout: '5000' })
1003+
assert.strictEqual(req.overrides.requestTimeout, undefined)
1004+
})
1005+
1006+
it('defaults to empty overrides when none provided', () => {
1007+
const req = new BaseRequest(null)
1008+
assert.deepStrictEqual(req.overrides, {})
1009+
})
1010+
})
1011+
1012+
describe('Transaction', () => {
1013+
it('stores valid requestTimeout override', () => {
1014+
const tx = new BaseTransaction(null, { requestTimeout: 10000 })
1015+
assert.strictEqual(tx.overrides.requestTimeout, 10000)
1016+
})
1017+
1018+
it('ignores invalid overrides', () => {
1019+
const tx = new BaseTransaction(null, { requestTimeout: NaN })
1020+
assert.strictEqual(tx.overrides.requestTimeout, undefined)
1021+
})
1022+
1023+
it('cascades overrides to request when no per-request config given', () => {
1024+
const pool = new ConnectionPool({ server: 'localhost' })
1025+
const tx = pool.transaction({ requestTimeout: 10000 })
1026+
const req = tx.request()
1027+
assert.strictEqual(req.overrides.requestTimeout, 10000)
1028+
})
1029+
1030+
it('per-request config overrides transaction overrides', () => {
1031+
const pool = new ConnectionPool({ server: 'localhost' })
1032+
const tx = pool.transaction({ requestTimeout: 10000 })
1033+
const req = tx.request({ requestTimeout: 3000 })
1034+
assert.strictEqual(req.overrides.requestTimeout, 3000)
1035+
})
1036+
1037+
it('per-request config merges with transaction overrides', () => {
1038+
const pool = new ConnectionPool({ server: 'localhost' })
1039+
const tx = pool.transaction({ requestTimeout: 10000 })
1040+
const req = tx.request({})
1041+
assert.strictEqual(req.overrides.requestTimeout, 10000)
1042+
})
1043+
})
1044+
1045+
describe('PreparedStatement', () => {
1046+
it('stores valid requestTimeout override', () => {
1047+
const ps = new BasePreparedStatement(null, { requestTimeout: 8000 })
1048+
assert.strictEqual(ps.overrides.requestTimeout, 8000)
1049+
})
1050+
1051+
it('ignores invalid overrides', () => {
1052+
const ps = new BasePreparedStatement(null, { requestTimeout: -100 })
1053+
assert.strictEqual(ps.overrides.requestTimeout, undefined)
1054+
})
1055+
})
1056+
})
9691057
})

0 commit comments

Comments
 (0)